Commit 88bd9bac authored by Mike Greiling's avatar Mike Greiling

Merge branch 'acet-notes-prettified' into 'master'

Prettify notes app

See merge request gitlab-org/gitlab-ce!17812
parents bcc04515 fdc9ae2e
...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue'; ...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores'; import store from '../notes/stores';
export default function initMrNotes() { export default function initMrNotes() {
new Vue({ // eslint-disable-line // eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions', el: '#js-vue-mr-discussions',
components: { components: {
notesApp, notesApp,
}, },
data() { data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
...@@ -28,7 +30,8 @@ export default function initMrNotes() { ...@@ -28,7 +30,8 @@ export default function initMrNotes() {
}, },
}); });
new Vue({ // eslint-disable-line // eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter', el: '#js-vue-discussion-counter',
components: { components: {
discussionCounter, discussionCounter,
......
...@@ -28,7 +28,13 @@ import GLForm from './gl_form'; ...@@ -28,7 +28,13 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import Autosave from './autosave'; import Autosave from './autosave';
import TaskList from './task_list'; import TaskList from './task_list';
import { isInViewport, getPagePath, scrollToElement, isMetaKey, hasVueMRDiscussionsCookie } from './lib/utils/common_utils'; import {
isInViewport,
getPagePath,
scrollToElement,
isMetaKey,
hasVueMRDiscussionsCookie,
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
...@@ -42,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; ...@@ -42,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes { export default class Notes {
static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { static initialize(
notes_url,
note_ids,
last_fetched_at,
view,
enableGFM = true,
) {
if (!this.instance) { if (!this.instance) {
this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM); this.instance = new Notes(
notes_url,
note_ids,
last_fetched_at,
view,
enableGFM,
);
} }
} }
...@@ -82,7 +100,8 @@ export default class Notes { ...@@ -82,7 +100,8 @@ export default class Notes {
this.updatedNotesTrackingMap = {}; this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at; this.last_fetched_at = last_fetched_at;
this.noteable_url = document.URL; this.noteable_url = document.URL;
this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); this.notesCountBadge ||
(this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
this.basePollingInterval = 15000; this.basePollingInterval = 15000;
this.maxPollingSteps = 4; this.maxPollingSteps = 4;
...@@ -93,15 +112,17 @@ export default class Notes { ...@@ -93,15 +112,17 @@ export default class Notes {
this.taskList = new TaskList({ this.taskList = new TaskList({
dataType: 'note', dataType: 'note',
fieldName: 'note', fieldName: 'note',
selector: '.notes' selector: '.notes',
}); });
this.collapseLongCommitList(); this.collapseLongCommitList();
this.setViewType(view); this.setViewType(view);
// We are in the Merge Requests page so we need another edit form for Changes tab // We are in the Merge Requests page so we need another edit form for Changes tab
if (getPagePath(1) === 'merge_requests') { if (getPagePath(1) === 'merge_requests') {
$('.note-edit-form').clone() $('.note-edit-form')
.addClass('mr-note-edit-form').insertAfter('.note-edit-form'); .clone()
.addClass('mr-note-edit-form')
.insertAfter('.note-edit-form');
} }
const hash = getLocationHash(); const hash = getLocationHash();
...@@ -117,7 +138,9 @@ export default class Notes { ...@@ -117,7 +138,9 @@ export default class Notes {
} }
addBinding() { addBinding() {
this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document); this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
// Edit note link // Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this)); this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
...@@ -125,27 +148,55 @@ export default class Notes { ...@@ -125,27 +148,55 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit // Reopen and close actions for Issue/MR combined with note form submit
this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment); this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment); this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons); this.$wrapperEl.on(
'keyup input',
'.js-note-text',
this.updateTargetButtons,
);
// resolve a discussion // resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment); this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general) // remove a note (in general)
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote); this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment // delete note attachment
this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment); this.$wrapperEl.on(
'click',
'.js-note-attachment-delete',
this.removeAttachment,
);
// reset main target form when clicking discard // reset main target form when clicking discard
this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm); this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected // update the file name when an attachment is selected
this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment); this.$wrapperEl.on(
'change',
'.js-note-attachment-input',
this.updateFormAttachment,
);
// reply to diff/discussion notes // reply to diff/discussion notes
this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); this.$wrapperEl.on(
'click',
'.js-discussion-reply-button',
this.onReplyToDiscussionNote,
);
// add diff note // add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote); this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images // add diff note for images
this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote); this.$wrapperEl.on(
'click',
'.js-add-image-diff-note-button',
this.onAddImageDiffNote,
);
// hide diff note form // hide diff note form
this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); this.$wrapperEl.on(
'click',
'.js-close-discussion-note-form',
this.cancelDiscussionForm,
);
// toggle commit list // toggle commit list
this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList); this.$wrapperEl.on(
'click',
'.system-note-commit-list-toggler',
this.toggleCommitList,
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff); this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
// fetch notes when tab becomes visible // fetch notes when tab becomes visible
...@@ -154,9 +205,21 @@ export default class Notes { ...@@ -154,9 +205,21 @@ export default class Notes {
this.$wrapperEl.on('issuable:change', this.refresh); this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote); this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); this.$wrapperEl.on(
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); 'ajax:success',
this.$wrapperEl.on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); '.js-discussion-note-form',
this.addDiscussionNote,
);
this.$wrapperEl.on(
'ajax:success',
'.js-main-target-form',
this.resetMainTargetForm,
);
this.$wrapperEl.on(
'ajax:complete',
'.js-main-target-form',
this.reenableTargetFormSubmitButton,
);
// when a key is clicked on the notes // when a key is clicked on the notes
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText); this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx` // When the URL fragment/hash has changed, `#note_xxx`
...@@ -195,10 +258,16 @@ export default class Notes { ...@@ -195,10 +258,16 @@ export default class Notes {
} }
static initCommentTypeToggle(form) { static initCommentTypeToggle(form) {
const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle'); const dropdownTrigger = form.querySelector(
const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu'); '.js-comment-type-dropdown .dropdown-toggle',
);
const dropdownList = form.querySelector(
'.js-comment-type-dropdown .dropdown-menu',
);
const noteTypeInput = form.querySelector('#note_type'); const noteTypeInput = form.querySelector('#note_type');
const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'); const submitButton = form.querySelector(
'.js-comment-type-dropdown .js-comment-submit-button',
);
const closeButton = form.querySelector('.js-note-target-close'); const closeButton = form.querySelector('.js-note-target-close');
const reopenButton = form.querySelector('.js-note-target-reopen'); const reopenButton = form.querySelector('.js-note-target-reopen');
...@@ -215,7 +284,13 @@ export default class Notes { ...@@ -215,7 +284,13 @@ export default class Notes {
} }
keydownNoteText(e) { keydownNoteText(e) {
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; var $textarea,
discussionNoteForm,
editNote,
myLastNote,
myLastNoteEditBtn,
newText,
originalText;
if (isMetaKey(e)) { if (isMetaKey(e)) {
return; return;
} }
...@@ -227,7 +302,12 @@ export default class Notes { ...@@ -227,7 +302,12 @@ export default class Notes {
if ($textarea.val() !== '') { if ($textarea.val() !== '') {
return; return;
} }
myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes')); myLastNote = $(
`li.note[data-author-id='${
gon.current_user_id
}'][data-editable]:last`,
$textarea.closest('.note, .notes_holder, #notes'),
);
if (myLastNote.length) { if (myLastNote.length) {
myLastNoteEditBtn = myLastNote.find('.js-note-edit'); myLastNoteEditBtn = myLastNote.find('.js-note-edit');
return myLastNoteEditBtn.trigger('click', [true, myLastNote]); return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
...@@ -238,7 +318,9 @@ export default class Notes { ...@@ -238,7 +318,9 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form'); discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) { if (discussionNoteForm.length) {
if ($textarea.val() !== '') { if ($textarea.val() !== '') {
if (!confirm('Are you sure you want to cancel creating this comment?')) { if (
!confirm('Are you sure you want to cancel creating this comment?')
) {
return; return;
} }
} }
...@@ -250,7 +332,9 @@ export default class Notes { ...@@ -250,7 +332,9 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote'); originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val(); newText = $textarea.val();
if (originalText !== newText) { if (originalText !== newText) {
if (!confirm('Are you sure you want to cancel editing this comment?')) { if (
!confirm('Are you sure you want to cancel editing this comment?')
) {
return; return;
} }
} }
...@@ -263,11 +347,14 @@ export default class Notes { ...@@ -263,11 +347,14 @@ export default class Notes {
if (Notes.interval) { if (Notes.interval) {
clearInterval(Notes.interval); clearInterval(Notes.interval);
} }
return Notes.interval = setInterval((function(_this) { return (Notes.interval = setInterval(
(function(_this) {
return function() { return function() {
return _this.refresh(); return _this.refresh();
}; };
})(this), this.pollingInterval); })(this),
this.pollingInterval,
));
} }
refresh() { refresh() {
...@@ -283,18 +370,21 @@ export default class Notes { ...@@ -283,18 +370,21 @@ export default class Notes {
this.refreshing = true; this.refreshing = true;
axios.get(`${this.notes_url}?html=true`, { axios
.get(`${this.notes_url}?html=true`, {
headers: { headers: {
'X-Last-Fetched-At': this.last_fetched_at, 'X-Last-Fetched-At': this.last_fetched_at,
}, },
}).then(({ data }) => { })
.then(({ data }) => {
const notes = data.notes; const notes = data.notes;
this.last_fetched_at = data.last_fetched_at; this.last_fetched_at = data.last_fetched_at;
this.setPollingInterval(data.notes.length); this.setPollingInterval(data.notes.length);
$.each(notes, (i, note) => this.renderNote(note)); $.each(notes, (i, note) => this.renderNote(note));
this.refreshing = false; this.refreshing = false;
}).catch(() => { })
.catch(() => {
this.refreshing = false; this.refreshing = false;
}); });
} }
...@@ -312,7 +402,8 @@ export default class Notes { ...@@ -312,7 +402,8 @@ export default class Notes {
if (shouldReset == null) { if (shouldReset == null) {
shouldReset = true; shouldReset = true;
} }
nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); nthInterval =
this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) { if (shouldReset) {
this.pollingInterval = this.basePollingInterval; this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) { } else if (this.pollingInterval < nthInterval) {
...@@ -331,10 +422,15 @@ export default class Notes { ...@@ -331,10 +422,15 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) { if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0); votesBlock = $('.js-awards-block').eq(0);
loadAwardsHandler().then((awardsHandler) => { loadAwardsHandler()
awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); .then(awardsHandler => {
awardsHandler.addAwardToEmojiBar(
votesBlock,
noteEntity.commands_changes.emoji_award,
);
awardsHandler.scrollToAwards(); awardsHandler.scrollToAwards();
}).catch(() => { })
.catch(() => {
// ignore // ignore
}); });
} }
...@@ -381,11 +477,17 @@ export default class Notes { ...@@ -381,11 +477,17 @@ export default class Notes {
if (!noteEntity.valid) { if (!noteEntity.valid) {
if (noteEntity.errors && noteEntity.errors.commands_only) { if (noteEntity.errors && noteEntity.errors.commands_only) {
if (noteEntity.commands_changes && if (
Object.keys(noteEntity.commands_changes).length > 0) { noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0
) {
$notesList.find('.system-note.being-posted').remove(); $notesList.find('.system-note.being-posted').remove();
} }
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0)); this.addFlash(
noteEntity.errors.commands_only,
'notice',
this.parentTimeline.get(0),
);
this.refresh(); this.refresh();
} }
return; return;
...@@ -407,28 +509,30 @@ export default class Notes { ...@@ -407,28 +509,30 @@ export default class Notes {
this.setupNewNote($newNote); this.setupNewNote($newNote);
this.refresh(); this.refresh();
return this.updateNotesCount(1); return this.updateNotesCount(1);
} } else if (Notes.isUpdatedNote(noteEntity, $note)) {
// The server can send the same update multiple times so we need to make sure to only update once per actual update. // The server can send the same update multiple times so we need to make sure to only update once per actual update.
else if (Notes.isUpdatedNote(noteEntity, $note)) {
const isEditing = $note.hasClass('is-editing'); const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines( const initialContent = normalizeNewlines(
$note.find('.original-note-content').text().trim() $note
.find('.original-note-content')
.text()
.trim(),
); );
const $textarea = $note.find('.js-note-text'); const $textarea = $note.find('.js-note-text');
const currentContent = $textarea.val(); const currentContent = $textarea.val();
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines(noteEntity.note); const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote; const isTextareaUntouched =
currentContent === initialContent ||
currentContent === sanitizedNoteNote;
if (isEditing && isTextareaUntouched) { if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note); $textarea.val(noteEntity.note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
} } else if (isEditing && !isTextareaUntouched) {
else if (isEditing && !isTextareaUntouched) {
this.putConflictEditWarningInPlace(noteEntity, $note); this.putConflictEditWarningInPlace(noteEntity, $note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
} } else {
else {
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
this.setupNewNote($updatedNote); this.setupNewNote($updatedNote);
} }
...@@ -452,17 +556,31 @@ export default class Notes { ...@@ -452,17 +556,31 @@ export default class Notes {
} }
this.note_ids.push(noteEntity.id); this.note_ids.push(noteEntity.id);
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); form =
row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`); $form ||
$(
`.js-discussion-note-form[data-discussion-id="${
noteEntity.discussion_id
}"]`,
);
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
: $(`#${noteEntity.discussion_line_code}`);
if (noteEntity.on_image) { if (noteEntity.on_image) {
row = form; row = form;
} }
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); diffAvatarContainer = row
.prevAll('.line_holder')
.first()
.find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion? // is this the first note of discussion?
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); discussionContainer = $(
`.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
);
if (!discussionContainer.length) { if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes'); discussionContainer = form.closest('.discussion').find('.notes');
} }
...@@ -470,25 +588,42 @@ export default class Notes { ...@@ -470,25 +588,42 @@ export default class Notes {
if (noteEntity.diff_discussion_html) { if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) { if (
!this.isParallelView() ||
row.hasClass('js-temp-notes-holder') ||
noteEntity.on_image
) {
// insert the note and the reply button after the temp row // insert the note and the reply button after the temp row
row.after($discussion); row.after($discussion);
} else { } else {
// Merge new discussion HTML in // Merge new discussion HTML in
var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); var $notes = $discussion.find(
var contentContainerClass = '.' + $notes.closest('.notes_content') `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
);
var contentContainerClass =
'.' +
$notes
.closest('.notes_content')
.attr('class') .attr('class')
.split(' ') .split(' ')
.join('.'); .join('.');
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); row
.find(contentContainerClass + ' .content')
.append($notes.closest('.content').children());
} }
} }
// Init discussion on 'Discussion' page if it is merge request page // Init discussion on 'Discussion' page if it is merge request page
const page = $('body').attr('data-page'); const page = $('body').attr('data-page');
if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { if (
(page && page.indexOf('projects:merge_request') !== -1) ||
!noteEntity.diff_discussion_html
) {
if (!hasVueMRDiscussionsCookie()) { if (!hasVueMRDiscussionsCookie()) {
Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); Notes.animateAppendNote(
noteEntity.discussion_html,
$('.main-notes-list'),
);
} }
} }
} else { } else {
...@@ -496,7 +631,10 @@ export default class Notes { ...@@ -496,7 +631,10 @@ export default class Notes {
Notes.animateAppendNote(noteEntity.html, discussionContainer); Notes.animateAppendNote(noteEntity.html, discussionContainer);
} }
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { if (
typeof gl.diffNotesCompileComponents !== 'undefined' &&
noteEntity.discussion_resolvable
) {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
...@@ -508,7 +646,8 @@ export default class Notes { ...@@ -508,7 +646,8 @@ export default class Notes {
} }
getLineHolder(changesDiscussionContainer) { getLineHolder(changesDiscussionContainer) {
return $(changesDiscussionContainer).closest('.notes_holder') return $(changesDiscussionContainer)
.closest('.notes_holder')
.prevAll('.line_holder') .prevAll('.line_holder')
.first() .first()
.get(0); .get(0);
...@@ -541,8 +680,14 @@ export default class Notes { ...@@ -541,8 +680,14 @@ export default class Notes {
form.find('.js-errors').remove(); form.find('.js-errors').remove();
// reset text and preview // reset text and preview
form.find('.js-md-write-button').click(); form.find('.js-md-write-button').click();
form.find('.js-note-text').val('').trigger('input'); form
form.find('.js-note-text').data('autosave').reset(); .find('.js-note-text')
.val('')
.trigger('input');
form
.find('.js-note-text')
.data('autosave')
.reset();
var event = document.createEvent('Event'); var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false); event.initEvent('autosize:update', true, false);
...@@ -578,7 +723,10 @@ export default class Notes { ...@@ -578,7 +723,10 @@ export default class Notes {
form.find('#note_type').val(''); form.find('#note_type').val('');
form.find('#note_project_id').remove(); form.find('#note_project_id').remove();
form.find('#in_reply_to_discussion_id').remove(); form.find('#in_reply_to_discussion_id').remove();
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); form
.find('.js-comment-resolve-button')
.closest('comment-and-resolve-btn')
.remove();
this.parentTimeline = form.parents('.timeline'); this.parentTimeline = form.parents('.timeline');
if (form.length) { if (form.length) {
...@@ -632,11 +780,17 @@ export default class Notes { ...@@ -632,11 +780,17 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) { } else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes'); formParentTimeline = $form.closest('.discussion-notes').find('.notes');
} }
return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0)); return this.addFlash(
'Your comment could not be submitted! Please check your network connection and try again.',
'alert',
formParentTimeline.get(0),
);
} }
updateNoteError($parentTimeline) { updateNoteError($parentTimeline) {
new Flash('Your comment could not be updated! Please check your network connection and try again.'); new Flash(
'Your comment could not be updated! Please check your network connection and try again.',
);
} }
/** /**
...@@ -685,14 +839,16 @@ export default class Notes { ...@@ -685,14 +839,16 @@ export default class Notes {
} }
checkContentToAllowEditing($el) { checkContentToAllowEditing($el) {
var initialContent = $el.find('.original-note-content').text().trim(); var initialContent = $el
.find('.original-note-content')
.text()
.trim();
var currentContent = $el.find('.js-note-text').val(); var currentContent = $el.find('.js-note-text').val();
var isAllowed = true; var isAllowed = true;
if (currentContent === initialContent) { if (currentContent === initialContent) {
this.removeNoteEditForm($el); this.removeNoteEditForm($el);
} } else {
else {
var $buttons = $el.find('.note-form-actions'); var $buttons = $el.find('.note-form-actions');
var isWidgetVisible = isInViewport($el.get(0)); var isWidgetVisible = isInViewport($el.get(0));
...@@ -754,8 +910,7 @@ export default class Notes { ...@@ -754,8 +910,7 @@ export default class Notes {
this.setupNewNote($newNote); this.setupNewNote($newNote);
// Now that we have taken care of the update, clear it out // Now that we have taken care of the update, clear it out
delete this.updatedNotesTrackingMap[noteId]; delete this.updatedNotesTrackingMap[noteId];
} } else {
else {
$note.find('.js-finish-edit-warning').hide(); $note.find('.js-finish-edit-warning').hide();
this.removeNoteEditForm($note); this.removeNoteEditForm($note);
} }
...@@ -788,7 +943,9 @@ export default class Notes { ...@@ -788,7 +943,9 @@ export default class Notes {
form.removeClass('current-note-edit-form'); form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide(); form.find('.js-finish-edit-warning').hide();
// Replace markdown textarea text with original note text. // Replace markdown textarea text with original note text.
return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote')); return form
.find('.js-note-text')
.val(form.find('form.edit-note').data('originalNote'));
} }
/** /**
...@@ -802,10 +959,12 @@ export default class Notes { ...@@ -802,10 +959,12 @@ export default class Notes {
$note = $(e.currentTarget).closest('.note'); $note = $(e.currentTarget).closest('.note');
noteElId = $note.attr('id'); noteElId = $note.attr('id');
noteId = $note.attr('data-note-id'); noteId = $note.attr('data-note-id');
lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') lineHolder = $(e.currentTarget)
.closest('.notes[data-discussion-id]')
.closest('.notes_holder') .closest('.notes_holder')
.prev('.line_holder'); .prev('.line_holder');
$(`.note[id="${noteElId}"]`).each((function(_this) { $(`.note[id="${noteElId}"]`).each(
(function(_this) {
// A same note appears in the "Discussion" and in the "Changes" tab, we have // A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes, // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
// where $('#noteId') would return only one. // where $('#noteId') would return only one.
...@@ -834,15 +993,21 @@ export default class Notes { ...@@ -834,15 +993,21 @@ export default class Notes {
// The notes tr can contain multiple lists of notes, like on the parallel diff // The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs // notesTr does not exist for image diffs
if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) { if (
notesTr.find('.discussion-notes').length > 1 ||
notesTr.length === 0
) {
const $diffFile = $notes.closest('.diff-file'); const $diffFile = $notes.closest('.diff-file');
if ($diffFile.length > 0) { if ($diffFile.length > 0) {
const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', { const removeBadgeEvent = new CustomEvent(
'removeBadge.imageDiff',
{
detail: { detail: {
// badgeNumber's start with 1 and index starts with 0 // badgeNumber's start with 1 and index starts with 0
badgeNumber: $notes.index() + 1, badgeNumber: $notes.index() + 1,
}, },
}); },
);
$diffFile[0].dispatchEvent(removeBadgeEvent); $diffFile[0].dispatchEvent(removeBadgeEvent);
} }
...@@ -853,7 +1018,8 @@ export default class Notes { ...@@ -853,7 +1018,8 @@ export default class Notes {
} }
} }
}; };
})(this)); })(this),
);
Notes.refreshVueNotes(); Notes.refreshVueNotes();
Notes.checkMergeRequestStatus(); Notes.checkMergeRequestStatus();
...@@ -935,7 +1101,12 @@ export default class Notes { ...@@ -935,7 +1101,12 @@ export default class Notes {
// DiffNote // DiffNote
form.find('#note_position').val(dataHolder.attr('data-position')); form.find('#note_position').val(dataHolder.attr('data-position'));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancelText')); form
.find('.js-note-discard')
.show()
.removeClass('js-note-discard')
.addClass('js-close-discussion-note-form')
.text(form.find('.js-close-discussion-note-form').data('cancelText'));
form.find('.js-note-target-close').remove(); form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove(); form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form); this.setupNoteForm(form);
...@@ -971,7 +1142,7 @@ export default class Notes { ...@@ -971,7 +1142,7 @@ export default class Notes {
this.toggleDiffNote({ this.toggleDiffNote({
target: $link, target: $link,
lineType: link.dataset.lineType, lineType: link.dataset.lineType,
showReplyInput showReplyInput,
}); });
} }
...@@ -987,7 +1158,9 @@ export default class Notes { ...@@ -987,7 +1158,9 @@ export default class Notes {
// Setup comment form // Setup comment form
let newForm; let newForm;
const $noteContainer = $link.closest('.diff-viewer').find('.note-container'); const $noteContainer = $link
.closest('.diff-viewer')
.find('.note-container');
const $form = $noteContainer.find('> .discussion-form'); const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) { if ($form.length === 0) {
...@@ -1000,13 +1173,17 @@ export default class Notes { ...@@ -1000,13 +1173,17 @@ export default class Notes {
this.setupDiscussionNoteForm($link, newForm); this.setupDiscussionNoteForm($link, newForm);
} }
toggleDiffNote({ toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
target, var $link,
lineType, addForm,
forceShow, hasNotes,
showReplyInput = false, newForm,
}) { noteForm,
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; replyButton,
row,
rowCssToAdd,
targetContent,
isDiffCommentAvatar;
$link = $(target); $link = $(target);
row = $link.closest('tr'); row = $link.closest('tr');
const nextRow = row.next(); const nextRow = row.next();
...@@ -1018,11 +1195,13 @@ export default class Notes { ...@@ -1018,11 +1195,13 @@ export default class Notes {
hasNotes = nextRow.is('.notes_holder'); hasNotes = nextRow.is('.notes_holder');
addForm = false; addForm = false;
let lineTypeSelector = ''; let lineTypeSelector = '';
rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane // In parallel view, look inside the correct left/right pane
if (this.isParallelView()) { if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`; lineTypeSelector = `.${lineType}`;
rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; rowCssToAdd =
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
} }
const notesContentSelector = `.notes_content${lineTypeSelector} .content`; const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector); let notesContent = targetRow.find(notesContentSelector);
...@@ -1050,7 +1229,9 @@ export default class Notes { ...@@ -1050,7 +1229,9 @@ export default class Notes {
notesContent = targetRow.find(notesContentSelector); notesContent = targetRow.find(notesContentSelector);
addForm = true; addForm = true;
} else { } else {
const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible'); const isCurrentlyShown = targetRow
.find('.content:not(:empty)')
.is(':visible');
const isForced = forceShow === true || forceShow === false; const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced); const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
...@@ -1077,11 +1258,12 @@ export default class Notes { ...@@ -1077,11 +1258,12 @@ export default class Notes {
row = form.closest('tr'); row = form.closest('tr');
glForm = form.data('glForm'); glForm = form.data('glForm');
glForm.destroy(); glForm.destroy();
form.find('.js-note-text').data('autosave').reset();
// show the reply button (will only work for replies)
form form
.prev('.discussion-reply-holder') .find('.js-note-text')
.show(); .data('autosave')
.reset();
// show the reply button (will only work for replies)
form.prev('.discussion-reply-holder').show();
if (row.is('.js-temp-notes-holder')) { if (row.is('.js-temp-notes-holder')) {
// remove temporary row for diff lines // remove temporary row for diff lines
return row.remove(); return row.remove();
...@@ -1122,7 +1304,9 @@ export default class Notes { ...@@ -1122,7 +1304,9 @@ export default class Notes {
var filename, form; var filename, form;
form = $(this).closest('form'); form = $(this).closest('form');
// get only the basename // get only the basename
filename = $(this).val().replace(/^.*[\\\/]/, ''); filename = $(this)
.val()
.replace(/^.*[\\\/]/, '');
return form.find('.js-attachment-filename').text(filename); return form.find('.js-attachment-filename').text(filename);
} }
...@@ -1194,12 +1378,16 @@ export default class Notes { ...@@ -1194,12 +1378,16 @@ export default class Notes {
this.glForm = new GLForm($editForm.find('form'), this.enableGFM); this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form') $editForm
.find('form')
.attr('action', `${postUrl}?html=true`) .attr('action', `${postUrl}?html=true`)
.attr('data-remote', 'true'); .attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId); $editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType); $editForm.find('.js-form-target-type').val(targetType);
$editForm.find('.js-note-text').focus().val(originalContent); $editForm
.find('.js-note-text')
.focus()
.val(originalContent);
$editForm.find('.js-md-write-button').trigger('click'); $editForm.find('.js-md-write-button').trigger('click');
$editForm.find('.referenced-users').hide(); $editForm.find('.referenced-users').hide();
} }
...@@ -1208,7 +1396,9 @@ export default class Notes { ...@@ -1208,7 +1396,9 @@ export default class Notes {
if ($note.find('.js-conflict-edit-warning').length === 0) { if ($note.find('.js-conflict-edit-warning').length === 0) {
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the This comment has changed since you started editing, please review the
<a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer"> <a href="#note_${
noteEntity.id
}" target="_blank" rel="noopener noreferrer">
updated comment updated comment
</a> </a>
to ensure information is not lost to ensure information is not lost
...@@ -1218,12 +1408,15 @@ export default class Notes { ...@@ -1218,12 +1408,15 @@ export default class Notes {
} }
updateNotesCount(updateCount) { updateNotesCount(updateCount) {
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); return this.notesCountBadge.text(
parseInt(this.notesCountBadge.text(), 10) + updateCount,
);
} }
static renderPlaceholderComponent($container) { static renderPlaceholderComponent($container) {
const el = $container.find('.js-code-placeholder').get(0); const el = $container.find('.js-code-placeholder').get(0);
new Vue({ // eslint-disable-line no-new new Vue({
// eslint-disable-line no-new
el, el,
components: { components: {
SkeletonLoadingContainer, SkeletonLoadingContainer,
...@@ -1248,7 +1441,9 @@ export default class Notes { ...@@ -1248,7 +1441,9 @@ export default class Notes {
$container.find('.line_content').html( $container.find('.line_content').html(
$(` $(`
<div class="nothing-here-block"> <div class="nothing-here-block">
${__('Unable to load the diff.')} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>? ${__(
'Unable to load the diff.',
)} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
</div> </div>
`), `),
); );
...@@ -1266,7 +1461,8 @@ export default class Notes { ...@@ -1266,7 +1461,8 @@ export default class Notes {
const fileHolder = $container.find('.file-holder'); const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath'); const url = fileHolder.data('linesPath');
axios.get(url) axios
.get(url)
.then(({ data }) => { .then(({ data }) => {
Notes.renderDiffContent($container, data); Notes.renderDiffContent($container, data);
}) })
...@@ -1277,9 +1473,14 @@ export default class Notes { ...@@ -1277,9 +1473,14 @@ export default class Notes {
toggleCommitList(e) { toggleCommitList(e) {
const $element = $(e.currentTarget); const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); const $closestSystemCommitList = $element.siblings(
'.system-note-commit-list',
);
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up'); $element
.find('.fa')
.toggleClass('fa-angle-down')
.toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade'); $closestSystemCommitList.toggleClass('hide-shade');
} }
...@@ -1289,11 +1490,17 @@ export default class Notes { ...@@ -1289,11 +1490,17 @@ export default class Notes {
* intrusive. * intrusive.
*/ */
collapseLongCommitList() { collapseLongCommitList() {
const systemNotes = $('#notes-list').find('li.system-note').has('ul'); const systemNotes = $('#notes-list')
.find('li.system-note')
.has('ul');
$.each(systemNotes, function(index, systemNote) { $.each(systemNotes, function(index, systemNote) {
const $systemNote = $(systemNote); const $systemNote = $(systemNote);
const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', ''); const headerMessage = $systemNote
.find('.note-text')
.find('p:first')
.text()
.replace(':', '');
$systemNote.find('.note-header .system-note-message').html(headerMessage); $systemNote.find('.note-header .system-note-message').html(headerMessage);
...@@ -1301,7 +1508,9 @@ export default class Notes { ...@@ -1301,7 +1508,9 @@ export default class Notes {
$systemNote.find('.note-text').addClass('system-note-commit-list'); $systemNote.find('.note-text').addClass('system-note-commit-list');
$systemNote.find('.system-note-commit-list-toggler').show(); $systemNote.find('.system-note-commit-list-toggler').show();
} else { } else {
$systemNote.find('.note-text').addClass('system-note-commit-list hide-shade'); $systemNote
.find('.note-text')
.addClass('system-note-commit-list hide-shade');
} }
}); });
} }
...@@ -1319,14 +1528,10 @@ export default class Notes { ...@@ -1319,14 +1528,10 @@ export default class Notes {
cleanForm($form) { cleanForm($form) {
// Remove JS classes that are not needed here // Remove JS classes that are not needed here
$form $form.find('.js-comment-type-dropdown').removeClass('btn-group');
.find('.js-comment-type-dropdown')
.removeClass('btn-group');
// Remove dropdown // Remove dropdown
$form $form.find('.dropdown-menu').remove();
.find('.dropdown-menu')
.remove();
return $form; return $form;
} }
...@@ -1345,7 +1550,11 @@ export default class Notes { ...@@ -1345,7 +1550,11 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim());
const currentNoteText = normalizeNewlines( const currentNoteText = normalizeNewlines(
$note.find('.original-note-content').first().text().trim() $note
.find('.original-note-content')
.first()
.text()
.trim(),
); );
return sanitizedNoteEntityText !== currentNoteText; return sanitizedNoteEntityText !== currentNoteText;
} }
...@@ -1435,7 +1644,14 @@ export default class Notes { ...@@ -1435,7 +1644,14 @@ export default class Notes {
* Once comment is _actually_ posted on server, we will have final element * Once comment is _actually_ posted on server, we will have final element
* in response that we will show in place of this temporary element. * in response that we will show in place of this temporary element.
*/ */
createPlaceholderNote({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { createPlaceholderNote({
formContent,
uniqueId,
isDiscussionNote,
currentUsername,
currentUserFullname,
currentUserAvatar,
}) {
const discussionClass = isDiscussionNote ? 'discussion' : ''; const discussionClass = isDiscussionNote ? 'discussion' : '';
const $tempNote = $( const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
...@@ -1449,8 +1665,12 @@ export default class Notes { ...@@ -1449,8 +1665,12 @@ export default class Notes {
<div class="note-header"> <div class="note-header">
<div class="note-header-info"> <div class="note-header-info">
<a href="/${_.escape(currentUsername)}"> <a href="/${_.escape(currentUsername)}">
<span class="hidden-xs">${_.escape(currentUsername)}</span> <span class="hidden-xs">${_.escape(
<span class="note-headline-light">${_.escape(currentUsername)}</span> currentUsername,
)}</span>
<span class="note-headline-light">${_.escape(
currentUsername,
)}</span>
</a> </a>
</div> </div>
</div> </div>
...@@ -1461,11 +1681,13 @@ export default class Notes { ...@@ -1461,11 +1681,13 @@ export default class Notes {
</div> </div>
</div> </div>
</div> </div>
</li>` </li>`,
); );
$tempNote.find('.hidden-xs').text(_.escape(currentUserFullname)); $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
$tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`); $tempNote
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
return $tempNote; return $tempNote;
} }
...@@ -1481,7 +1703,7 @@ export default class Notes { ...@@ -1481,7 +1703,7 @@ export default class Notes {
<i>${formContent}</i> <i>${formContent}</i>
</div> </div>
</div> </div>
</li>` </li>`,
); );
return $tempNote; return $tempNote;
...@@ -1513,11 +1735,22 @@ export default class Notes { ...@@ -1513,11 +1735,22 @@ export default class Notes {
const $submitBtn = $(e.target); const $submitBtn = $(e.target);
let $form = $submitBtn.parents('form'); let $form = $submitBtn.parents('form');
const $closeBtn = $form.find('.js-note-target-close'); const $closeBtn = $form.find('.js-note-target-close');
const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion'; const isDiscussionNote =
$submitBtn
.parent()
.find('li.droplab-item-selected')
.attr('id') === 'discussion';
const isMainForm = $form.hasClass('js-main-target-form'); const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form'); const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); const isDiscussionResolve = $submitBtn.hasClass(
const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form); 'js-comment-resolve-button',
);
const {
formData,
formContent,
formAction,
formContentOriginal,
} = this.getFormData($form);
let noteUniqueId; let noteUniqueId;
let systemNoteUniqueId; let systemNoteUniqueId;
let hasQuickActions = false; let hasQuickActions = false;
...@@ -1547,23 +1780,30 @@ export default class Notes { ...@@ -1547,23 +1780,30 @@ export default class Notes {
// Show placeholder note // Show placeholder note
if (tempFormContent) { if (tempFormContent) {
noteUniqueId = _.uniqueId('tempNote_'); noteUniqueId = _.uniqueId('tempNote_');
$notesContainer.append(this.createPlaceholderNote({ $notesContainer.append(
this.createPlaceholderNote({
formContent: tempFormContent, formContent: tempFormContent,
uniqueId: noteUniqueId, uniqueId: noteUniqueId,
isDiscussionNote, isDiscussionNote,
currentUsername: gon.current_username, currentUsername: gon.current_username,
currentUserFullname: gon.current_user_fullname, currentUserFullname: gon.current_user_fullname,
currentUserAvatar: gon.current_user_avatar_url, currentUserAvatar: gon.current_user_avatar_url,
})); }),
);
} }
// Show placeholder system note // Show placeholder system note
if (hasQuickActions) { if (hasQuickActions) {
systemNoteUniqueId = _.uniqueId('tempSystemNote_'); systemNoteUniqueId = _.uniqueId('tempSystemNote_');
$notesContainer.append(this.createPlaceholderSystemNote({ $notesContainer.append(
formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), this.createPlaceholderSystemNote({
formContent: this.getQuickActionDescription(
formContent,
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands),
),
uniqueId: systemNoteUniqueId, uniqueId: systemNoteUniqueId,
})); }),
);
} }
// Clear the form textarea // Clear the form textarea
...@@ -1577,8 +1817,9 @@ export default class Notes { ...@@ -1577,8 +1817,9 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
// Make request to submit comment on server // Make request to submit comment on server
axios.post(`${formAction}?html=true`, formData) axios
.then((res) => { .post(`${formAction}?html=true`, formData)
.then(res => {
const note = res.data; const note = res.data;
// Submission successful! remove placeholder // Submission successful! remove placeholder
...@@ -1595,7 +1836,9 @@ export default class Notes { ...@@ -1595,7 +1836,9 @@ export default class Notes {
// Reset cached commands list when command is applied // Reset cached commands list when command is applied
if (hasQuickActions) { if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); $form
.find('textarea.js-note-text')
.trigger('clear-commands-cache.atwho');
} }
// Clear previous form errors // Clear previous form errors
...@@ -1640,11 +1883,14 @@ export default class Notes { ...@@ -1640,11 +1883,14 @@ export default class Notes {
// append flash-container to the Notes list // append flash-container to the Notes list
if ($notesContainer.length) { if ($notesContainer.length) {
$notesContainer.append('<div class="flash-container" style="display: none;"></div>'); $notesContainer.append(
'<div class="flash-container" style="display: none;"></div>',
);
} }
Notes.refreshVueNotes(); Notes.refreshVueNotes();
} else if (isMainForm) { // Check if this was main thread comment } else if (isMainForm) {
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup // Show final note element on UI and perform form and action buttons cleanup
this.addNote($form, note); this.addNote($form, note);
this.reenableTargetFormSubmitButton(e); this.reenableTargetFormSubmitButton(e);
...@@ -1655,7 +1901,8 @@ export default class Notes { ...@@ -1655,7 +1901,8 @@ export default class Notes {
} }
$form.trigger('ajax:success', [note]); $form.trigger('ajax:success', [note]);
}).catch(() => { })
.catch(() => {
// Submission failed, remove placeholder note and show Flash error message // Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
...@@ -1675,7 +1922,9 @@ export default class Notes { ...@@ -1675,7 +1922,9 @@ export default class Notes {
// Show form again on UI on failure // Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) { if (isDiscussionForm && $notesContainer.length) {
const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); const replyButton = $notesContainer
.parent()
.find('.js-discussion-reply-button');
this.replyToDiscussionNote(replyButton[0]); this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form'); $form = $notesContainer.parent().find('form');
} }
...@@ -1720,12 +1969,19 @@ export default class Notes { ...@@ -1720,12 +1969,19 @@ export default class Notes {
// Show updated comment content temporarily // Show updated comment content temporarily
$noteBodyText.html(formContent); $noteBodyText.html(formContent);
$editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); $editingNote
$editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); .removeClass('is-editing fade-in-full')
.addClass('being-posted fade-in-half');
$editingNote
.find('.note-headline-meta a')
.html(
'<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>',
);
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
// Make request to update comment on server // Make request to update comment on server
axios.post(`${formAction}?html=true`, formData) axios
.post(`${formAction}?html=true`, formData)
.then(({ data }) => { .then(({ data }) => {
// Submission successful! render final note element // Submission successful! render final note element
this.updateNote(data, $editingNote); this.updateNote(data, $editingNote);
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import Autosize from 'autosize'; import Autosize from 'autosize';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Autosave from '../../autosave'; import Autosave from '../../autosave';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility'; import {
import * as constants from '../constants'; capitalizeFirstCharacter,
import eventHub from '../event_hub'; convertToCamelCase,
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; } from '../../lib/utils/text_utility';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import * as constants from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import eventHub from '../event_hub';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import discussionLockedWidget from './discussion_locked_widget.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issuableStateMixin from '../mixins/issuable_state'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default { export default {
name: 'CommentForm', name: 'CommentForm',
components: { components: {
issueWarning, issueWarning,
...@@ -28,9 +31,7 @@ ...@@ -28,9 +31,7 @@
userAvatarLink, userAvatarLink,
loadingButton, loadingButton,
}, },
mixins: [ mixins: [issuableStateMixin],
issuableStateMixin,
],
props: { props: {
noteableType: { noteableType: {
type: String, type: String,
...@@ -53,9 +54,7 @@ ...@@ -53,9 +54,7 @@
'getNotesData', 'getNotesData',
'openState', 'openState',
]), ]),
...mapState([ ...mapState(['isToggleStateButtonLoading']),
'isToggleStateButtonLoading',
]),
noteableDisplayName() { noteableDisplayName() {
return this.noteableType.replace(/_/g, ' '); return this.noteableType.replace(/_/g, ' ');
}, },
...@@ -63,10 +62,15 @@ ...@@ -63,10 +62,15 @@
return this.getUserData.id; return this.getUserData.id;
}, },
commentButtonTitle() { commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; return this.noteType === constants.COMMENT
? 'Comment'
: 'Start discussion';
}, },
isOpen() { isOpen() {
return this.openState === constants.OPENED || this.openState === constants.REOPENED; return (
this.openState === constants.OPENED ||
this.openState === constants.REOPENED
);
}, },
canCreateNote() { canCreateNote() {
return this.getNoteableData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
...@@ -75,23 +79,17 @@ ...@@ -75,23 +79,17 @@
const openOrClose = this.isOpen ? 'close' : 'reopen'; const openOrClose = this.isOpen ? 'close' : 'reopen';
if (this.note.length) { if (this.note.length) {
return sprintf( return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), {
__('%{actionText} & %{openOrClose} %{noteable}'),
{
actionText: this.commentButtonTitle, actionText: this.commentButtonTitle,
openOrClose, openOrClose,
noteable: this.noteableDisplayName, noteable: this.noteableDisplayName,
}, });
);
} }
return sprintf( return sprintf(__('%{openOrClose} %{noteable}'), {
__('%{openOrClose} %{noteable}'),
{
openOrClose: capitalizeFirstCharacter(openOrClose), openOrClose: capitalizeFirstCharacter(openOrClose),
noteable: this.noteableDisplayName, noteable: this.noteableDisplayName,
}, });
);
}, },
actionButtonClassNames() { actionButtonClassNames() {
return { return {
...@@ -131,7 +129,9 @@ ...@@ -131,7 +129,9 @@
mounted() { mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery. // jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => { $(document).on('issuable:change', (e, isClosed) => {
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED); this.toggleIssueLocalState(
isClosed ? constants.CLOSED : constants.REOPENED,
);
}); });
this.initAutoSave(); this.initAutoSave();
...@@ -180,7 +180,7 @@ ...@@ -180,7 +180,7 @@
this.stopPolling(); this.stopPolling();
this.saveNote(noteData) this.saveNote(noteData)
.then((res) => { .then(res => {
this.enableButton(); this.enableButton();
this.restartPolling(); this.restartPolling();
...@@ -205,8 +205,7 @@ ...@@ -205,8 +205,7 @@
.catch(() => { .catch(() => {
this.enableButton(); this.enableButton();
this.discard(false); this.discard(false);
const msg = const msg = `Your comment could not be submitted!
`Your comment could not be submitted!
Please check your network connection and try again.`; Please check your network connection and try again.`;
Flash(msg, 'alert', this.$el); Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content. this.note = noteData.data.note.note; // Restore textarea content.
...@@ -228,7 +227,9 @@ Please check your network connection and try again.`; ...@@ -228,7 +227,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false); this.toggleStateButtonLoading(false);
Flash( Flash(
sprintf( sprintf(
__('Something went wrong while closing the %{issuable}. Please try again later'), __(
'Something went wrong while closing the %{issuable}. Please try again later',
),
{ issuable: this.noteableDisplayName }, { issuable: this.noteableDisplayName },
), ),
); );
...@@ -241,7 +242,9 @@ Please check your network connection and try again.`; ...@@ -241,7 +242,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false); this.toggleStateButtonLoading(false);
Flash( Flash(
sprintf( sprintf(
__('Something went wrong while reopening the %{issuable}. Please try again later'), __(
'Something went wrong while reopening the %{issuable}. Please try again later',
),
{ issuable: this.noteableDisplayName }, { issuable: this.noteableDisplayName },
), ),
); );
...@@ -278,12 +281,15 @@ Please check your network connection and try again.`; ...@@ -278,12 +281,15 @@ Please check your network connection and try again.`;
}, },
initAutoSave() { initAutoSave() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); const noteableType = capitalizeFirstCharacter(
convertToCamelCase(this.noteableType),
this.autosave = new Autosave(
$(this.$refs.textarea),
['Note', noteableType, this.getNoteableData.id],
); );
this.autosave = new Autosave($(this.$refs.textarea), [
'Note',
noteableType,
this.getNoteableData.id,
]);
} }
}, },
initTaskList() { initTaskList() {
...@@ -299,7 +305,7 @@ Please check your network connection and try again.`; ...@@ -299,7 +305,7 @@ Please check your network connection and try again.`;
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
ClipboardButton, ClipboardButton,
Icon, Icon,
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
return this.diffFile.discussionPath ? 'a' : 'span'; return this.diffFile.discussionPath ? 'a' : 'span';
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index'; import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
export default { export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
}, },
...@@ -37,7 +37,11 @@ ...@@ -37,7 +37,11 @@
if (this.isImageDiff) { if (this.isImageDiff) {
const canCreateNote = false; const canCreateNote = false;
const renderCommentBadge = true; const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); imageDiffHelper.initImageDiff(
this.$refs.fileHolder,
canCreateNote,
renderCommentBadge,
);
} else { } else {
const fileHolder = $(this.$refs.fileHolder); const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => { this.$nextTick(() => {
...@@ -50,7 +54,7 @@ ...@@ -50,7 +54,7 @@
return html.outerHTML ? 'tr' : 'template'; return html.outerHTML ? 'tr' : 'template';
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg'; import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg'; import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg'; import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg'; import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils'; import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -49,7 +49,9 @@ ...@@ -49,7 +49,9 @@
}, },
methods: { methods: {
jumpToFirstDiscussion() { jumpToFirstDiscussion() {
const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`); const el = document.querySelector(
`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
);
const activeTab = window.mrTabs.currentAction; const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') { if (activeTab === 'commits' || activeTab === 'pipelines') {
...@@ -61,7 +63,7 @@ ...@@ -61,7 +63,7 @@
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable'; import Issuable from '~/vue_shared/mixins/issuable';
export default { export default {
components: { components: {
Icon, Icon,
}, },
mixins: [ mixins: [Issuable],
Issuable, };
],
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg'; import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'NoteActions', name: 'NoteActions',
directives: { directives: {
tooltip, tooltip,
...@@ -70,9 +70,7 @@ ...@@ -70,9 +70,7 @@
}, },
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['getUserDataByProp']),
'getUserDataByProp',
]),
shouldShowActionsDropdown() { shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse); return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
}, },
...@@ -115,7 +113,7 @@ ...@@ -115,7 +113,7 @@
this.$emit('handleResolve'); this.$emit('handleResolve');
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
name: 'NoteAttachment', name: 'NoteAttachment',
props: { props: {
attachment: { attachment: {
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
required: true, required: true,
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash'; import Flash from '../../flash';
import { glEmojiTag } from '../../emoji'; import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -30,9 +30,7 @@ ...@@ -30,9 +30,7 @@
}, },
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['getUserData']),
'getUserData',
]),
// `this.awards` is an array with emojis but they are not grouped by emoji name. See below. // `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
// [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ] // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
// This method will group emojis by their name as an Object. See below. // This method will group emojis by their name as an Object. See below.
...@@ -79,9 +77,7 @@ ...@@ -79,9 +77,7 @@
this.emojiSmiley = emojiSmiley; this.emojiSmiley = emojiSmiley;
}, },
methods: { methods: {
...mapActions([ ...mapActions(['toggleAwardRequest']),
'toggleAwardRequest',
]),
getAwardHTML(name) { getAwardHTML(name) {
return glEmojiTag(name); return glEmojiTag(name);
}, },
...@@ -96,30 +92,43 @@ ...@@ -96,30 +92,43 @@
const restrictedEmojis = ['thumbsup', 'thumbsdown']; const restrictedEmojis = ['thumbsup', 'thumbsdown'];
// Users can not add :+1: and :-1: to their own notes // Users can not add :+1: and :-1: to their own notes
if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { if (
this.getUserData.id === this.noteAuthorId &&
restrictedEmojis.indexOf(awardName) > -1
) {
isAllowed = false; isAllowed = false;
} }
return this.getUserData.id && isAllowed; return this.getUserData.id && isAllowed;
}, },
hasReactionByCurrentUser(awardList) { hasReactionByCurrentUser(awardList) {
return awardList.filter(award => award.user.id === this.getUserData.id).length; return awardList.filter(award => award.user.id === this.getUserData.id)
.length;
}, },
awardTitle(awardsList) { awardTitle(awardsList) {
const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); const hasReactionByCurrentUser = this.hasReactionByCurrentUser(
awardsList,
);
const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
let awardList = awardsList; let awardList = awardsList;
// Filter myself from list if I am awarded. // Filter myself from list if I am awarded.
if (hasReactionByCurrentUser) { if (hasReactionByCurrentUser) {
awardList = awardList.filter(award => award.user.id !== this.getUserData.id); awardList = awardList.filter(
award => award.user.id !== this.getUserData.id,
);
} }
// Get only 9-10 usernames to show in tooltip text. // Get only 9-10 usernames to show in tooltip text.
const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); const namesToShow = awardList
.slice(0, TOOLTIP_NAME_COUNT)
.map(award => award.user.name);
// Get the remaining list to use in `and x more` text. // Get the remaining list to use in `and x more` text.
const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); const remainingAwardList = awardList.slice(
TOOLTIP_NAME_COUNT,
awardList.length,
);
// Add myself to the begining of the list so title will start with You. // Add myself to the begining of the list so title will start with You.
if (hasReactionByCurrentUser) { if (hasReactionByCurrentUser) {
...@@ -130,14 +139,17 @@ ...@@ -130,14 +139,17 @@
// We have 10+ awarded user, join them with comma and add `and x more`. // We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) { if (remainingAwardList.length) {
title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; title = `${namesToShow.join(', ')}, and ${
remainingAwardList.length
} more.`;
} else if (namesToShow.length > 1) { } else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text. // Join all names with comma but not the last one, it will be added with and text.
title = namesToShow.slice(0, namesToShow.length - 1).join(', '); title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
// If we have more than 2 users we need an extra comma before and text. // If we have more than 2 users we need an extra comma before and text.
title += namesToShow.length > 2 ? ',' : ''; title += namesToShow.length > 2 ? ',' : '';
title += ` and ${namesToShow.slice(-1)}`; // Append and text title += ` and ${namesToShow.slice(-1)}`; // Append and text
} else { // We have only 2 users so join them with and. } else {
// We have only 2 users so join them with and.
title = namesToShow.join(' and '); title = namesToShow.join(' and ');
} }
...@@ -169,11 +181,12 @@ ...@@ -169,11 +181,12 @@
awardName: parsedName, awardName: parsedName,
}; };
this.toggleAwardRequest(data) this.toggleAwardRequest(data).catch(() =>
.catch(() => Flash('Something went wrong on our end.')); Flash('Something went wrong on our end.'),
);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import noteEditedText from './note_edited_text.vue'; import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue'; import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue'; import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue'; import noteForm from './note_form.vue';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
components: { components: {
noteEditedText, noteEditedText,
noteAwardsList, noteAwardsList,
noteAttachment, noteAttachment,
noteForm, noteForm,
}, },
mixins: [ mixins: [autosave],
autosave,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -77,7 +75,7 @@ ...@@ -77,7 +75,7 @@
this.$emit('cancelFormEdition', shouldConfirm, isDirty); this.$emit('cancelFormEdition', shouldConfirm, isDirty);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
name: 'EditedNoteText', name: 'EditedNoteText',
components: { components: {
timeAgoTooltip, timeAgoTooltip,
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
default: 'edited-text', default: 'edited-text',
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
name: 'IssueNoteForm', name: 'IssueNoteForm',
components: { components: {
issueWarning, issueWarning,
markdownField, markdownField,
}, },
mixins: [ mixins: [issuableStateMixin, resolvable],
issuableStateMixin,
resolvable,
],
props: { props: {
noteBody: { noteBody: {
type: String, type: String,
...@@ -69,7 +66,9 @@ ...@@ -69,7 +66,9 @@
return this.getNotesDataByProp('markdownDocsPath'); return this.getNotesDataByProp('markdownDocsPath');
}, },
quickActionsDocsPath() { quickActionsDocsPath() {
return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined; return !this.isEditing
? this.getNotesDataByProp('quickActionsDocsPath')
: undefined;
}, },
currentUserId() { currentUserId() {
return this.getUserDataByProp('id'); return this.getUserDataByProp('id');
...@@ -91,24 +90,29 @@ ...@@ -91,24 +90,29 @@
this.$refs.textarea.focus(); this.$refs.textarea.focus();
}, },
methods: { methods: {
...mapActions([ ...mapActions(['toggleResolveNote']),
'toggleResolveNote',
]),
handleUpdate(shouldResolve) { handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved; const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true; this.isSubmitting = true;
this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { this.$emit(
'handleFormUpdate',
this.updatedNoteBody,
this.$refs.editNoteForm,
() => {
this.isSubmitting = false; this.isSubmitting = false;
if (shouldResolve) { if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState); this.resolveHandler(beforeSubmitDiscussionState);
} }
}); },
);
}, },
editMyLastNote() { editMyLastNote() {
if (this.updatedNoteBody === '') { if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody); const lastNoteInDiscussion = this.getDiscussionLastNote(
this.updatedNoteBody,
);
if (lastNoteInDiscussion) { if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', { eventHub.$emit('enterEditMode', {
...@@ -119,10 +123,14 @@ ...@@ -119,10 +123,14 @@
}, },
cancelHandler(shouldConfirm = false) { cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed // Sends information about confirm message and if the textarea has changed
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody); this.$emit(
'cancelFormEdition',
shouldConfirm,
this.noteBody !== this.updatedNoteBody,
);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
timeAgoTooltip, timeAgoTooltip,
}, },
...@@ -49,9 +49,7 @@ ...@@ -49,9 +49,7 @@
}, },
}, },
methods: { methods: {
...mapActions([ ...mapActions(['setTargetNoteHash']),
'setTargetNoteHash',
]),
handleToggle() { handleToggle() {
this.$emit('toggleHandler'); this.$emit('toggleHandler');
}, },
...@@ -59,7 +57,7 @@ ...@@ -59,7 +57,7 @@
this.setTargetNoteHash(this.noteTimestampLink); this.setTargetNoteHash(this.noteTimestampLink);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
computed: { computed: {
...mapGetters([ ...mapGetters(['getNotesDataByProp']),
'getNotesDataByProp',
]),
registerLink() { registerLink() {
return this.getNotesDataByProp('registerPath'); return this.getNotesDataByProp('registerPath');
}, },
...@@ -13,7 +11,7 @@ ...@@ -13,7 +11,7 @@
return this.getNotesDataByProp('newSessionPath'); return this.getNotesDataByProp('newSessionPath');
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash'; import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants'; import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue'; import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue'; import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue'; import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { scrollToElement } from '../../lib/utils/common_utils'; import { scrollToElement } from '../../lib/utils/common_utils';
export default { export default {
components: { components: {
noteableNote, noteableNote,
diffWithNote, diffWithNote,
...@@ -34,11 +34,7 @@ ...@@ -34,11 +34,7 @@
directives: { directives: {
tooltip, tooltip,
}, },
mixins: [ mixins: [autosave, noteable, resolvable],
autosave,
noteable,
resolvable,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -99,7 +95,9 @@ ...@@ -99,7 +95,9 @@
return this.unresolvedDiscussions.length > 0; return this.unresolvedDiscussions.length > 0;
}, },
wrapperComponent() { wrapperComponent() {
return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div'; return this.discussion.diffDiscussion && this.discussion.diffFile
? diffWithNote
: 'div';
}, },
wrapperClass() { wrapperClass() {
return this.isDiffDiscussion ? '' : 'panel panel-default'; return this.isDiffDiscussion ? '' : 'panel panel-default';
...@@ -151,8 +149,10 @@ ...@@ -151,8 +149,10 @@
}, },
cancelReplyForm(shouldConfirm) { cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) { if (shouldConfirm && this.$refs.noteForm.isDirty) {
const msg = 'Are you sure you want to cancel creating this comment?';
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel creating this comment?')) { if (!confirm(msg)) {
return; return;
} }
} }
...@@ -178,7 +178,7 @@ ...@@ -178,7 +178,7 @@
this.resetAutoSave(); this.resetAutoSave();
callback(); callback();
}) })
.catch((err) => { .catch(err => {
this.removePlaceholderNotes(); this.removePlaceholderNotes();
this.isReplying = true; this.isReplying = true;
this.$nextTick(() => { this.$nextTick(() => {
...@@ -204,7 +204,7 @@ Please check your network connection and try again.`; ...@@ -204,7 +204,7 @@ Please check your network connection and try again.`;
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore'; import { escape } from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue'; import noteActions from './note_actions.vue';
import noteBody from './note_body.vue'; import noteBody from './note_body.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
components: { components: {
userAvatarLink, userAvatarLink,
noteHeader, noteHeader,
noteActions, noteActions,
noteBody, noteBody,
}, },
mixins: [ mixins: [noteable, resolvable],
noteable,
resolvable,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -37,10 +34,7 @@ ...@@ -37,10 +34,7 @@
}; };
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['targetNoteHash', 'getUserData']),
'targetNoteHash',
'getUserData',
]),
author() { author() {
return this.note.author; return this.note.author;
}, },
...@@ -53,7 +47,9 @@ ...@@ -53,7 +47,9 @@
}; };
}, },
canReportAsAbuse() { canReportAsAbuse() {
return this.note.report_abuse_path && this.author.id !== this.getUserData.id; return (
this.note.report_abuse_path && this.author.id !== this.getUserData.id
);
}, },
noteAnchorId() { noteAnchorId() {
return `note_${this.note.id}`; return `note_${this.note.id}`;
...@@ -89,7 +85,9 @@ ...@@ -89,7 +85,9 @@
this.isDeleting = false; this.isDeleting = false;
}) })
.catch(() => { .catch(() => {
Flash('Something went wrong while deleting your note. Please try again.'); Flash(
'Something went wrong while deleting your note. Please try again.',
);
this.isDeleting = false; this.isDeleting = false;
}); });
} }
...@@ -120,7 +118,8 @@ ...@@ -120,7 +118,8 @@
this.isRequesting = false; this.isRequesting = false;
this.isEditing = true; this.isEditing = true;
this.$nextTick(() => { this.$nextTick(() => {
const msg = 'Something went wrong while editing your comment. Please try again.'; const msg =
'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el); Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText); this.recoverNoteContent(noteText);
callback(); callback();
...@@ -130,7 +129,8 @@ ...@@ -130,7 +129,8 @@
formCancelHandler(shouldConfirm, isDirty) { formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) { if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel editing this comment?')) return; if (!confirm('Are you sure you want to cancel editing this comment?'))
return;
} }
this.$refs.noteBody.resetAutoSave(); this.$refs.noteBody.resetAutoSave();
if (this.oldContent) { if (this.oldContent) {
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
this.$refs.noteBody.$refs.noteForm.note.note = noteText; this.$refs.noteBody.$refs.noteForm.note.note = noteText;
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility'; import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import store from '../stores/'; import store from '../stores/';
import * as constants from '../constants'; import * as constants from '../constants';
import noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue'; import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue'; import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default { export default {
name: 'NotesApp', name: 'NotesApp',
components: { components: {
noteableNote, noteableNote,
...@@ -47,16 +47,14 @@ ...@@ -47,16 +47,14 @@
}; };
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
'notes',
'getNotesDataByProp',
'discussionCount',
]),
noteableType() { noteableType() {
// FIXME -- @fatihacet Get this from JSON data. // FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE; return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
: ISSUE_NOTEABLE_TYPE;
}, },
allNotes() { allNotes() {
if (this.isLoading) { if (this.isLoading) {
...@@ -79,9 +77,11 @@ ...@@ -79,9 +77,11 @@
const parentElement = this.$el.parentElement; const parentElement = this.$el.parentElement;
if (parentElement && if (
parentElement.classList.contains('js-vue-notes-event')) { parentElement &&
parentElement.addEventListener('toggleAward', (event) => { parentElement.classList.contains('js-vue-notes-event')
) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail; const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId }); this.actionToggleAward({ awardName, noteId });
}); });
...@@ -131,7 +131,9 @@ ...@@ -131,7 +131,9 @@
.then(() => this.checkLocationHash()) .then(() => this.checkLocationHash())
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
Flash('Something went wrong while fetching comments. Please try again.'); Flash(
'Something went wrong while fetching comments. Please try again.',
);
}); });
}, },
initPolling() { initPolling() {
...@@ -154,7 +156,7 @@ ...@@ -154,7 +156,7 @@
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
......
import Vue from 'vue'; import Vue from 'vue';
import notesApp from './components/notes_app.vue'; import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener(
'DOMContentLoaded',
() =>
new Vue({
el: '#js-vue-notes', el: '#js-vue-notes',
components: { components: {
notesApp, notesApp,
...@@ -9,13 +12,17 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -9,13 +12,17 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
data() { data() {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData); const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? { let currentUserData = {};
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id, id: parsedUserData.id,
name: parsedUserData.name, name: parsedUserData.name,
username: parsedUserData.username, username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path, path: parsedUserData.path,
} : {}; };
}
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
...@@ -32,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -32,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); }),
);
...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; ...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default { export default {
methods: { methods: {
initAutoSave(noteableType) { initAutoSave(noteableType) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]); this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
capitalizeFirstCharacter(noteableType),
this.note.id,
]);
}, },
resetAutoSave() { resetAutoSave() {
this.autosave.reset(); this.autosave.reset();
......
...@@ -12,7 +12,8 @@ export default { ...@@ -12,7 +12,8 @@ export default {
discussionResolved() { discussionResolved() {
const { notes, resolved } = this.note; const { notes, resolved } = this.note;
if (notes) { // Decide resolved state using store. Only valid for discussions. if (notes) {
// Decide resolved state using store. Only valid for discussions.
return notes.every(note => note.resolved && !note.system); return notes.every(note => note.resolved && !note.system);
} }
...@@ -26,7 +27,9 @@ export default { ...@@ -26,7 +27,9 @@ export default {
return __('Comment and resolve discussion'); return __('Comment and resolve discussion');
} }
return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); return this.discussionResolved
? __('Unresolve discussion')
: __('Resolve discussion');
}, },
}, },
methods: { methods: {
...@@ -42,7 +45,9 @@ export default { ...@@ -42,7 +45,9 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isResolving = false; this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.'); const msg = __(
'Something went wrong while resolving this discussion. Please try again.',
);
Flash(msg, 'alert', this.$el); Flash(msg, 'alert', this.$el);
}); });
}, },
......
...@@ -22,7 +22,9 @@ export default { ...@@ -22,7 +22,9 @@ export default {
}, },
toggleResolveNote(endpoint, isResolved) { toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME; const method = isResolved
? UNRESOLVE_NOTE_METHOD_NAME
: RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint); return Vue.http[method](endpoint);
}, },
......
...@@ -12,47 +12,57 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; ...@@ -12,47 +12,57 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll; let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); export const setNotesData = ({ commit }, data) =>
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); commit(types.SET_NOTES_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); export const setNoteableData = ({ commit }, data) =>
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); commit(types.SET_NOTEABLE_DATA, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); export const setUserData = ({ commit }, data) =>
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); commit(types.SET_USER_DATA, data);
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const setLastFetchedAt = ({ commit }, data) =>
commit(types.SET_LAST_FETCHED_AT, data);
export const fetchNotes = ({ commit }, path) => service export const setInitialNotes = ({ commit }, data) =>
commit(types.SET_INITIAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) =>
commit(types.SET_TARGET_NOTE_HASH, data);
export const toggleDiscussion = ({ commit }, data) =>
commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) =>
service
.fetchNotes(path) .fetchNotes(path)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
commit(types.SET_INITIAL_NOTES, res); commit(types.SET_INITIAL_NOTES, res);
}); });
export const deleteNote = ({ commit }, note) => service export const deleteNote = ({ commit }, note) =>
.deleteNote(note.path) service.deleteNote(note.path).then(() => {
.then(() => {
commit(types.DELETE_NOTE, note); commit(types.DELETE_NOTE, note);
}); });
export const updateNote = ({ commit }, { endpoint, note }) => service export const updateNote = ({ commit }, { endpoint, note }) =>
service
.updateNote(endpoint, note) .updateNote(endpoint, note)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
commit(types.UPDATE_NOTE, res); commit(types.UPDATE_NOTE, res);
}); });
export const replyToDiscussion = ({ commit }, { endpoint, data }) => service export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
service
.replyToDiscussion(endpoint, data) .replyToDiscussion(endpoint, data)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
return res; return res;
}); });
export const createNewNote = ({ commit }, { endpoint, data }) => service export const createNewNote = ({ commit }, { endpoint, data }) =>
service
.createNewNote(endpoint, data) .createNewNote(endpoint, data)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
if (!res.errors) { if (!res.errors) {
commit(types.ADD_NEW_NOTE, res); commit(types.ADD_NEW_NOTE, res);
} }
...@@ -62,11 +72,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service ...@@ -62,11 +72,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) => export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES); commit(types.REMOVE_PLACEHOLDER_NOTES);
export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service export const toggleResolveNote = (
{ commit },
{ endpoint, isResolved, discussion },
) =>
service
.toggleResolveNote(endpoint, isResolved) .toggleResolveNote(endpoint, isResolved)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; const mutationType = discussion
? types.UPDATE_DISCUSSION
: types.UPDATE_NOTE;
commit(mutationType, res); commit(mutationType, res);
}); });
...@@ -76,7 +92,7 @@ export const closeIssue = ({ commit, dispatch, state }) => { ...@@ -76,7 +92,7 @@ export const closeIssue = ({ commit, dispatch, state }) => {
return service return service
.toggleIssueState(state.notesData.closePath) .toggleIssueState(state.notesData.closePath)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then(data => {
commit(types.CLOSE_ISSUE); commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data); dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false); dispatch('toggleStateButtonLoading', false);
...@@ -88,7 +104,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => { ...@@ -88,7 +104,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
return service return service
.toggleIssueState(state.notesData.reopenPath) .toggleIssueState(state.notesData.reopenPath)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then(data => {
commit(types.REOPEN_ISSUE); commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data); dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false); dispatch('toggleStateButtonLoading', false);
...@@ -99,10 +115,12 @@ export const toggleStateButtonLoading = ({ commit }, value) => ...@@ -99,10 +115,12 @@ export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value); commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
export const emitStateChangedEvent = ({ commit, getters }, data) => { export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: { const event = new CustomEvent('issuable_vue_app:change', {
detail: {
data, data,
isClosed: getters.openState === constants.CLOSED, isClosed: getters.openState === constants.CLOSED,
} }); },
});
document.dispatchEvent(event); document.dispatchEvent(event);
}; };
...@@ -144,8 +162,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -144,8 +162,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}); });
} }
return dispatch(methodToDispatch, noteData) return dispatch(methodToDispatch, noteData).then(res => {
.then((res) => {
const { errors } = res; const { errors } = res;
const commandsChanges = res.commands_changes; const commandsChanges = res.commands_changes;
...@@ -161,8 +178,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -161,8 +178,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const votesBlock = $('.js-awards-block').eq(0); const votesBlock = $('.js-awards-block').eq(0);
loadAwardsHandler() loadAwardsHandler()
.then((awardsHandler) => { .then(awardsHandler => {
awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); awardsHandler.addAwardToEmojiBar(
votesBlock,
commandsChanges.emoji_award,
);
awardsHandler.scrollToAwards(); awardsHandler.scrollToAwards();
}) })
.catch(() => { .catch(() => {
...@@ -174,7 +194,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -174,7 +194,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}); });
} }
if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { if (
commandsChanges.spend_time != null ||
commandsChanges.time_estimate != null
) {
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
} }
} }
...@@ -192,11 +215,17 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { ...@@ -192,11 +215,17 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
if (resp.notes && resp.notes.length) { if (resp.notes && resp.notes.length) {
const { notesById } = getters; const { notesById } = getters;
resp.notes.forEach((note) => { resp.notes.forEach(note => {
if (notesById[note.id]) { if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note); commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { } else if (
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); note.type === constants.DISCUSSION_NOTE ||
note.type === constants.DIFF_NOTE
) {
const discussion = utils.findNoteObjectById(
state.notes,
note.discussion_id,
);
if (discussion) { if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
...@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => { ...@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
resource: service, resource: service,
method: 'poll', method: 'poll',
data: state, data: state,
successCallback: resp => resp.json() successCallback: resp =>
resp
.json()
.then(data => pollSuccessCallBack(data, commit, state, getters)), .then(data => pollSuccessCallBack(data, commit, state, getters)),
errorCallback: () => Flash('Something went wrong while fetching latest comments.'), errorCallback: () =>
Flash('Something went wrong while fetching latest comments.'),
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
...@@ -248,15 +280,22 @@ export const restartPolling = () => { ...@@ -248,15 +280,22 @@ export const restartPolling = () => {
}; };
export const fetchData = ({ commit, state, getters }) => { export const fetchData = ({ commit, state, getters }) => {
const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt }; const requestData = {
endpoint: state.notesData.notesPath,
lastFetchedAt: state.lastFetchedAt,
};
service.poll(requestData) service
.poll(requestData)
.then(resp => resp.json) .then(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters)) .then(data => pollSuccessCallBack(data, commit, state, getters))
.catch(() => Flash('Something went wrong while fetching latest comments.')); .catch(() => Flash('Something went wrong while fetching latest comments.'));
}; };
export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => { export const toggleAward = (
{ commit, state, getters, dispatch },
{ awardName, noteId },
) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
}; };
......
...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop]; ...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const openState = state => state.noteableData.state; export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop =>
state.userData && state.userData[prop];
export const notesById = state => state.notes.reduce((acc, note) => { export const notesById = state =>
state.notes.reduce((acc, note) => {
note.notes.every(n => Object.assign(acc, { [n.id]: n })); note.notes.every(n => Object.assign(acc, { [n.id]: n }));
return acc; return acc;
}, {}); }, {});
const reverseNotes = array => array.slice(0).reverse(); const reverseNotes = array => array.slice(0).reverse();
const isLastNote = (note, state) => !note.system && const isLastNote = (note, state) =>
state.userData && note.author && !note.system &&
state.userData &&
note.author &&
note.author.id === state.userData.id; note.author.id === state.userData.id;
export const getCurrentUserLastNote = state => _.flatten( export const getCurrentUserLastNote = state =>
reverseNotes(state.notes) _.flatten(
.map(note => reverseNotes(note.notes)), reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
).find(el => isLastNote(el, state)); ).find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) export const getDiscussionLastNote = state => discussion =>
.find(el => isLastNote(el, state)); reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = (state) => { export const discussionCount = state => {
const discussions = state.notes.filter(n => !n.individual_note); const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length; return discussions.length;
...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => { ...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]); return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
}; };
export const resolvedDiscussionsById = (state) => { export const resolvedDiscussionsById = state => {
const map = {}; const map = {};
state.notes.forEach((n) => { state.notes.forEach(n => {
if (n.notes) { if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system); const resolved = n.notes.every(note => note.resolved && !note.system);
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
[types.ADD_NEW_NOTE](state, note) { [types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note; const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id); const [exists] = state.notes.filter(n => n.id === note.discussion_id);
const isDiscussion = (type === constants.DISCUSSION_NOTE); const isDiscussion = type === constants.DISCUSSION_NOTE;
if (!exists) { if (!exists) {
const noteData = { const noteData = {
...@@ -63,13 +63,15 @@ export default { ...@@ -63,13 +63,15 @@ export default {
const note = notes[i]; const note = notes[i];
const children = note.notes; const children = note.notes;
if (children.length && !note.individual_note) { // remove placeholder from discussions if (children.length && !note.individual_note) {
// remove placeholder from discussions
for (let j = children.length - 1; j >= 0; j -= 1) { for (let j = children.length - 1; j >= 0; j -= 1) {
if (children[j].isPlaceholderNote) { if (children[j].isPlaceholderNote) {
children.splice(j, 1); children.splice(j, 1);
} }
} }
} else if (note.isPlaceholderNote) { // remove placeholders from state root } else if (note.isPlaceholderNote) {
// remove placeholders from state root
notes.splice(i, 1); notes.splice(i, 1);
} }
} }
...@@ -89,10 +91,10 @@ export default { ...@@ -89,10 +91,10 @@ export default {
[types.SET_INITIAL_NOTES](state, notesData) { [types.SET_INITIAL_NOTES](state, notesData) {
const notes = []; const notes = [];
notesData.forEach((note) => { notesData.forEach(note => {
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) { if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => { note.notes.forEach(n => {
notes.push({ notes.push({
...note, ...note,
notes: [n], // override notes array to only have one item to mimick individual_note notes: [n], // override notes array to only have one item to mimick individual_note
...@@ -103,7 +105,7 @@ export default { ...@@ -103,7 +105,7 @@ export default {
notes.push({ notes.push({
...note, ...note,
expanded: (oldNote ? oldNote.expanded : note.expanded), expanded: oldNote ? oldNote.expanded : note.expanded,
}); });
} }
}); });
...@@ -128,7 +130,9 @@ export default { ...@@ -128,7 +130,9 @@ export default {
notesArr.push({ notesArr.push({
individual_note: true, individual_note: true,
isPlaceholderNote: true, isPlaceholderNote: true,
placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE, placeholderType: data.isSystemNote
? constants.SYSTEM_NOTE
: constants.NOTE,
notes: [ notes: [
{ {
body: data.noteBody, body: data.noteBody,
...@@ -141,12 +145,16 @@ export default { ...@@ -141,12 +145,16 @@ export default {
const { awardName, note } = data; const { awardName, note } = data;
const { id, name, username } = state.userData; const { id, name, username } = state.userData;
const hasEmojiAwardedByCurrentUser = note.award_emoji const hasEmojiAwardedByCurrentUser = note.award_emoji.filter(
.filter(emoji => emoji.name === data.awardName && emoji.user.id === id); emoji => emoji.name === data.awardName && emoji.user.id === id,
);
if (hasEmojiAwardedByCurrentUser.length) { if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it. // If current user has awarded this emoji, remove it.
note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1); note.award_emoji.splice(
note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
1,
);
} else { } else {
note.award_emoji.push({ note.award_emoji.push({
name: awardName, name: awardName,
......
...@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache'; ...@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; export const findNoteObjectById = (notes, id) =>
notes.filter(n => n.id === id)[0];
export const getQuickActionText = (note) => { export const getQuickActionText = note => {
let text = 'Applying command'; let text = 'Applying command';
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; const quickActions =
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter((command) => { const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`); const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(note); return commandRegex.test(note);
}); });
...@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => { ...@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); export const stripQuickActions = note =>
note.replace(REGEX_QUICK_ACTIONS, '').trim();
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