Commit 9fe127ff authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge branch 'master' into droplab-templating-xss-fix

parents 8619042d 309bab43
...@@ -67,6 +67,7 @@ stages: ...@@ -67,6 +67,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation" - knapsack rspec "--color --format documentation"
artifacts: artifacts:
...@@ -87,6 +88,7 @@ stages: ...@@ -87,6 +88,7 @@ stages:
- export CI_NODE_TOTAL=${JOB_NAME[2]} - export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
......
...@@ -73,6 +73,9 @@ gem 'grape', '~> 0.19.0' ...@@ -73,6 +73,9 @@ gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
# Pagination # Pagination
gem 'kaminari', '~> 0.17.0' gem 'kaminari', '~> 0.17.0'
......
...@@ -346,6 +346,8 @@ GEM ...@@ -346,6 +346,8 @@ GEM
tilt tilt
hashdiff (0.3.2) hashdiff (0.3.2)
hashie (3.5.5) hashie (3.5.5)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
health_check (2.6.0) health_check (2.6.0)
rails (>= 4.0) rails (>= 4.0)
hipchat (1.5.2) hipchat (1.5.2)
...@@ -915,6 +917,7 @@ DEPENDENCIES ...@@ -915,6 +917,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
haml_lint (~> 0.21.0) haml_lint (~> 0.21.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0) html-pipeline (~> 1.11.0)
...@@ -1035,4 +1038,4 @@ DEPENDENCIES ...@@ -1035,4 +1038,4 @@ DEPENDENCIES
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
1.14.5 1.14.6
/* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import emojiMap from 'emojis/digests.json'; import emojiMap from 'emojis/digests.json';
...@@ -124,6 +126,8 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { ...@@ -124,6 +126,8 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
} }
const $menu = $('.emoji-menu'); const $menu = $('.emoji-menu');
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) { if ($menu.length) {
if ($menu.is('.is-visible')) { if ($menu.is('.is-visible')) {
$addBtn.removeClass('is-active'); $addBtn.removeClass('is-active');
...@@ -147,6 +151,8 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { ...@@ -147,6 +151,8 @@ AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) {
}, 200); }, 200);
}); });
} }
$thumbsBtn.toggleClass('disabled', $userAuthored);
}; };
// Create the emoji menu with the first category of emojis. // Create the emoji menu with the first category of emojis.
...@@ -259,7 +265,8 @@ AwardsHandler.prototype.addAward = function addAward( ...@@ -259,7 +265,8 @@ AwardsHandler.prototype.addAward = function addAward(
callback, callback,
) { ) {
const normalizedEmoji = this.normalizeEmojiName(emoji); const normalizedEmoji = this.normalizeEmojiName(emoji);
this.postEmoji(awardUrl, normalizedEmoji, () => { const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined; return typeof callback === 'function' ? callback() : undefined;
}); });
...@@ -324,6 +331,10 @@ AwardsHandler.prototype.isActive = function isActive($emojiButton) { ...@@ -324,6 +331,10 @@ AwardsHandler.prototype.isActive = function isActive($emojiButton) {
return $emojiButton.hasClass('active'); return $emojiButton.hasClass('active');
}; };
AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) {
return $button.hasClass('js-user-authored');
};
AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) { AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) {
const counter = $('.js-counter', $emojiButton); const counter = $('.js-counter', $emojiButton);
const counterNumber = parseInt(counter.text(), 10); const counterNumber = parseInt(counter.text(), 10);
...@@ -428,20 +439,35 @@ AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) { ...@@ -428,20 +439,35 @@ AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) {
}); });
}; };
AwardsHandler.prototype.postEmoji = function postEmoji(awardUrl, emoji, callback) { AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) {
return $.post(awardUrl, { if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton);
} else {
$.post(awardUrl, {
name: emoji, name: emoji,
}, (data) => { }, (data) => {
if (data.ok) { if (data.ok) {
callback(); callback();
} }
}); }).fail(() => new Flash('Something went wrong on our end.'));
}
}; };
AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) { AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) {
return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`);
}; };
AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) {
const oldTitle = this.getAwardTooltip($emojiButton);
const newTitle = 'You cannot vote on your own issue, MR and note';
gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show');
// Restore tooltip back to award list
return setTimeout(() => {
$emojiButton.tooltip('hide');
gl.utils.updateTooltipTitle($emojiButton, oldTitle);
}, 2800);
};
AwardsHandler.prototype.scrollToAwards = function scrollToAwards() { AwardsHandler.prototype.scrollToAwards = function scrollToAwards() {
const options = { const options = {
scrollTop: $('.awards').offset().top - 110, scrollTop: $('.awards').offset().top - 110,
......
...@@ -125,7 +125,7 @@ $(() => { ...@@ -125,7 +125,7 @@ $(() => {
}, },
dismissOverviewDialog() { dismissOverviewDialog() {
this.isOverviewDialogDismissed = true; this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1'); Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
}, },
}, },
}); });
......
...@@ -66,7 +66,10 @@ window.DropzoneInput = (function() { ...@@ -66,7 +66,10 @@ window.DropzoneInput = (function() {
form_textarea.focus(); form_textarea.focus();
}, },
success: function(header, response) { success: function(header, response) {
pasteText(response.link.markdown); const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
const shouldPad = processingFileCount >= 1;
pasteText(response.link.markdown, shouldPad);
}, },
error: function(temp) { error: function(temp) {
var checkIfMsgExists, errorAlert; var checkIfMsgExists, errorAlert;
...@@ -123,9 +126,10 @@ window.DropzoneInput = (function() { ...@@ -123,9 +126,10 @@ window.DropzoneInput = (function() {
} }
return false; return false;
}; };
pasteText = function(text) { pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text + "\n\n"; var formattedText = text;
if (shouldPad) formattedText += "\n\n";
caretStart = $(child)[0].selectionStart; caretStart = $(child)[0].selectionStart;
caretEnd = $(child)[0].selectionEnd; caretEnd = $(child)[0].selectionEnd;
textEnd = $(child).val().length; textEnd = $(child).val().length;
......
...@@ -47,6 +47,10 @@ ...@@ -47,6 +47,10 @@
} }
}; };
gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) {
return $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
};
w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) {
event_name = event_name || 'input'; event_name = event_name || 'input';
var closest_submit, field, that; var closest_submit, field, that;
......
...@@ -18,7 +18,7 @@ export default class UserCallout { ...@@ -18,7 +18,7 @@ export default class UserCallout {
dismissCallout(e) { dismissCallout(e) {
const $currentTarget = $(e.currentTarget); const $currentTarget = $(e.currentTarget);
Cookies.set(USER_CALLOUT_COOKIE, 'true'); Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 });
if ($currentTarget.hasClass('close')) { if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove(); this.userCalloutBody.remove();
......
...@@ -38,6 +38,15 @@ ...@@ -38,6 +38,15 @@
height: 300px; height: 300px;
overflow-y: scroll; overflow-y: scroll;
} }
.disabled {
cursor: default;
opacity: 0.5;
&:hover {
transform: none;
}
}
} }
.emoji-search { .emoji-search {
...@@ -154,6 +163,17 @@ ...@@ -154,6 +163,17 @@
} }
} }
&.user-authored {
cursor: default;
opacity: 0.65;
&:hover,
&:active {
background-color: $white-light;
border-color: $border-color;
}
}
&.btn { &.btn {
&:focus { &:focus {
outline: 0; outline: 0;
......
...@@ -352,6 +352,15 @@ ul.notes { ...@@ -352,6 +352,15 @@ ul.notes {
font-size: 14px; font-size: 14px;
} }
.note-header {
display: flex;
justify-content: space-between;
}
.note-header-info {
min-width: 0;
}
.note-headline-light { .note-headline-light {
display: inline; display: inline;
...@@ -371,21 +380,27 @@ ul.notes { ...@@ -371,21 +380,27 @@ ul.notes {
} }
} }
.note-headline-meta {
display: inline-block;
white-space: nowrap;
}
/** /**
* Actions for Discussions/Notes * Actions for Discussions/Notes
*/ */
.discussion-actions, .discussion-actions {
.note-actions {
float: right; float: right;
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
} }
.note-actions { .note-actions {
position: absolute; flex-shrink: 0;
right: 0; // For PhantomJS that does not support flex
top: 0; float: right;
margin-left: 10px;
color: $gray-darkest;
.note-action-button { .note-action-button {
margin-left: 8px; margin-left: 8px;
......
...@@ -110,6 +110,14 @@ module IssuesHelper ...@@ -110,6 +110,14 @@ module IssuesHelper
end end
end end
def award_user_authored_class(award)
if award == 'thumbsdown' || award == 'thumbsup'
'user-authored js-user-authored'
else
''
end
end
def awards_sort(awards) def awards_sort(awards)
awards.sort_by do |award, notes| awards.sort_by do |award, notes|
if award == "thumbsup" if award == "thumbsup"
......
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline) - grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
- user_authored = awardable.user_authored?(current_user)
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards| - awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: (award_state_class(awards, current_user)), class: [(award_state_class(awards, current_user)), (award_user_authored_class(emoji) if user_authored)],
data: { placement: "bottom", title: award_user_list(awards, current_user) } } data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji) = emoji_icon(emoji)
%span.award-control-text.js-counter %span.award-control-text.js-counter
...@@ -12,6 +13,7 @@ ...@@ -12,6 +13,7 @@
.award-menu-holder.js-award-holder .award-menu-holder.js-award-holder
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button', %button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add emoji', 'aria-label': 'Add emoji',
class: ("js-user-authored" if user_authored),
data: { title: 'Add emoji', placement: "bottom" } } data: { title: 'Add emoji', placement: "bottom" } }
%span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
......
...@@ -12,12 +12,14 @@ ...@@ -12,12 +12,14 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content .timeline-content
.note-header .note-header
%a.visible-xs{ href: user_path(note.author) } .note-header-info
= note.author.to_reference %a{ href: user_path(note.author) }
= link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
.note-headline-light
%span.hidden-xs %span.hidden-xs
= sanitize(note.author.name)
%span.note-headline-light
= note.author.to_reference = note.author.to_reference
%span.note-headline-light
%span.note-headline-meta
- unless note.system - unless note.system
commented commented
- if note.system - if note.system
...@@ -59,7 +61,8 @@ ...@@ -59,7 +61,8 @@
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do - user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin') = icon('spinner spin')
%span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
......
---
title: 'Frontend prevent authored votes'
merge_request: 6260
author: Barthc
---
title: Only add newlines between multiple uploads
merge_request: 10545
author:
---
title: Add hashie-forbidden_attributes gem
merge_request: 10579
author: Andy Brown
...@@ -8,7 +8,12 @@ Rails.application.configure do ...@@ -8,7 +8,12 @@ Rails.application.configure do
# test suite. You never need to work with it otherwise. Remember that # test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped # your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there! # and recreated between test runs. Don't rely on the data there!
config.cache_classes = false
# Enabling caching of classes slows start-up time because all controllers
# are loaded at initalization, but it reduces memory and load because files
# are not reloaded with every request. For example, caching is not necessary
# for loading database migrations but useful for handling Knapsack specs.
config.cache_classes = ENV['CACHE_CLASSES'] == 'true'
# Configure static asset server for tests with Cache-Control for performance # Configure static asset server for tests with Cache-Control for performance
config.assets.digest = false config.assets.digest = false
......
...@@ -601,10 +601,10 @@ describe 'Issues', feature: true do ...@@ -601,10 +601,10 @@ describe 'Issues', feature: true do
expect(page.find_field("issue_description").value).to have_content 'banana_sample' expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end end
it 'adds double newline to end of attachment markdown' do it "doesn't add double newline to end of a single attachment markdown" do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("issue_description").value).to match /\n\n$/ expect(page.find_field("issue_description").value).not_to match /\n\n$/
end end
end end
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import AwardsHandler from '~/awards_handler'; import AwardsHandler from '~/awards_handler';
require('~/lib/utils/common_utils');
(function() { (function() {
var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
...@@ -28,7 +30,7 @@ import AwardsHandler from '~/awards_handler'; ...@@ -28,7 +30,7 @@ import AwardsHandler from '~/awards_handler';
loadFixtures('issues/issue_with_comment.html.raw'); loadFixtures('issues/issue_with_comment.html.raw');
awardsHandler = new AwardsHandler; awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) { return function(button, url, emoji, cb) {
return cb(); return cb();
}; };
})(this)); })(this));
...@@ -115,6 +117,27 @@ import AwardsHandler from '~/awards_handler'; ...@@ -115,6 +117,27 @@ import AwardsHandler from '~/awards_handler';
return expect($emojiButton.next('.js-counter').text()).toBe('4'); return expect($emojiButton.next('.js-counter').text()).toBe('4');
}); });
}); });
describe('::userAuthored', function() {
it('should update tooltip to user authored title', function() {
var $thumbsUpEmoji, $votesBlock;
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
return expect($thumbsUpEmoji.data("original-title")).toBe("You cannot vote on your own issue, MR and note");
});
it('should restore tooltip back to initial vote list', function() {
var $thumbsUpEmoji, $votesBlock;
jasmine.clock().install();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.userAuthored($thumbsUpEmoji);
jasmine.clock().tick(2801);
jasmine.clock().uninstall();
return expect($thumbsUpEmoji.data("original-title")).toBe("sam");
});
});
describe('::getAwardUrl', function() { describe('::getAwardUrl', function() {
return it('returns the url for request', function() { return it('returns the url for request', function() {
return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment