diff options
Diffstat (limited to 'app')
26 files changed, 2149 insertions, 1792 deletions
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index 972fdb2b791..096c4ef5f31 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue'; import store from '../notes/stores'; export default function initMrNotes() { - new Vue({ // eslint-disable-line + // eslint-disable-next-line no-new + new Vue({ el: '#js-vue-mr-discussions', components: { notesApp, }, data() { - const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; + const notesDataset = document.getElementById('js-vue-mr-discussions') + .dataset; return { noteableData: JSON.parse(notesDataset.noteableData), currentUserData: JSON.parse(notesDataset.currentUserData), @@ -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', components: { discussionCounter, diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 6d1b2f452c0..659ae575219 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -28,7 +28,13 @@ import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; import Autosave from './autosave'; 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 { localTimeAgo } from './lib/utils/datetime_utility'; @@ -42,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; 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) { - 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 { this.updatedNotesTrackingMap = {}; this.last_fetched_at = last_fetched_at; 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.maxPollingSteps = 4; @@ -93,15 +112,17 @@ export default class Notes { this.taskList = new TaskList({ dataType: 'note', fieldName: 'note', - selector: '.notes' + selector: '.notes', }); this.collapseLongCommitList(); this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab if (getPagePath(1) === 'merge_requests') { - $('.note-edit-form').clone() - .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); + $('.note-edit-form') + .clone() + .addClass('mr-note-edit-form') + .insertAfter('.note-edit-form'); } const hash = getLocationHash(); @@ -117,7 +138,9 @@ export default class Notes { } addBinding() { - this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document); + this.$wrapperEl = hasVueMRDiscussionsCookie() + ? $(document).find('.diffs') + : $(document); // Edit note link this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this)); @@ -125,27 +148,55 @@ export default class Notes { // 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-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 this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment); // remove a note (in general) this.$wrapperEl.on('click', '.js-note-delete', this.removeNote); // 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 this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm); // 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 - this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); + this.$wrapperEl.on( + 'click', + '.js-discussion-reply-button', + this.onReplyToDiscussionNote, + ); // add diff note this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote); // 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 - 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 - 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); // fetch notes when tab becomes visible @@ -154,9 +205,21 @@ export default class Notes { this.$wrapperEl.on('issuable:change', this.refresh); // 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-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); + this.$wrapperEl.on( + 'ajax:success', + '.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 this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText); // When the URL fragment/hash has changed, `#note_xxx` @@ -195,10 +258,16 @@ export default class Notes { } static initCommentTypeToggle(form) { - const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle'); - const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu'); + const dropdownTrigger = form.querySelector( + '.js-comment-type-dropdown .dropdown-toggle', + ); + const dropdownList = form.querySelector( + '.js-comment-type-dropdown .dropdown-menu', + ); 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 reopenButton = form.querySelector('.js-note-target-reopen'); @@ -215,7 +284,13 @@ export default class Notes { } keydownNoteText(e) { - var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; + var $textarea, + discussionNoteForm, + editNote, + myLastNote, + myLastNoteEditBtn, + newText, + originalText; if (isMetaKey(e)) { return; } @@ -227,7 +302,12 @@ export default class Notes { if ($textarea.val() !== '') { 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) { myLastNoteEditBtn = myLastNote.find('.js-note-edit'); return myLastNoteEditBtn.trigger('click', [true, myLastNote]); @@ -238,7 +318,9 @@ export default class Notes { discussionNoteForm = $textarea.closest('.js-discussion-note-form'); if (discussionNoteForm.length) { 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; } } @@ -250,7 +332,9 @@ export default class Notes { originalText = $textarea.closest('form').data('originalNote'); newText = $textarea.val(); 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; } } @@ -263,11 +347,14 @@ export default class Notes { if (Notes.interval) { clearInterval(Notes.interval); } - return Notes.interval = setInterval((function(_this) { - return function() { - return _this.refresh(); - }; - })(this), this.pollingInterval); + return (Notes.interval = setInterval( + (function(_this) { + return function() { + return _this.refresh(); + }; + })(this), + this.pollingInterval, + )); } refresh() { @@ -283,20 +370,23 @@ export default class Notes { this.refreshing = true; - axios.get(`${this.notes_url}?html=true`, { - headers: { - 'X-Last-Fetched-At': this.last_fetched_at, - }, - }).then(({ data }) => { - const notes = data.notes; - this.last_fetched_at = data.last_fetched_at; - this.setPollingInterval(data.notes.length); - $.each(notes, (i, note) => this.renderNote(note)); - - this.refreshing = false; - }).catch(() => { - this.refreshing = false; - }); + axios + .get(`${this.notes_url}?html=true`, { + headers: { + 'X-Last-Fetched-At': this.last_fetched_at, + }, + }) + .then(({ data }) => { + const notes = data.notes; + this.last_fetched_at = data.last_fetched_at; + this.setPollingInterval(data.notes.length); + $.each(notes, (i, note) => this.renderNote(note)); + + this.refreshing = false; + }) + .catch(() => { + this.refreshing = false; + }); } /** @@ -312,7 +402,8 @@ export default class Notes { if (shouldReset == null) { shouldReset = true; } - nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); + nthInterval = + this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); if (shouldReset) { this.pollingInterval = this.basePollingInterval; } else if (this.pollingInterval < nthInterval) { @@ -331,12 +422,17 @@ export default class Notes { if ('emoji_award' in noteEntity.commands_changes) { votesBlock = $('.js-awards-block').eq(0); - loadAwardsHandler().then((awardsHandler) => { - awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); - awardsHandler.scrollToAwards(); - }).catch(() => { - // ignore - }); + loadAwardsHandler() + .then(awardsHandler => { + awardsHandler.addAwardToEmojiBar( + votesBlock, + noteEntity.commands_changes.emoji_award, + ); + awardsHandler.scrollToAwards(); + }) + .catch(() => { + // ignore + }); } } } @@ -381,11 +477,17 @@ export default class Notes { if (!noteEntity.valid) { if (noteEntity.errors && noteEntity.errors.commands_only) { - if (noteEntity.commands_changes && - Object.keys(noteEntity.commands_changes).length > 0) { + if ( + noteEntity.commands_changes && + Object.keys(noteEntity.commands_changes).length > 0 + ) { $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(); } return; @@ -407,28 +509,30 @@ export default class Notes { this.setupNewNote($newNote); this.refresh(); return this.updateNotesCount(1); - } - // 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)) { + } 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. const isEditing = $note.hasClass('is-editing'); 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 currentContent = $textarea.val(); // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way const sanitizedNoteNote = normalizeNewlines(noteEntity.note); - const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote; + const isTextareaUntouched = + currentContent === initialContent || + currentContent === sanitizedNoteNote; if (isEditing && isTextareaUntouched) { $textarea.val(noteEntity.note); this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else if (isEditing && !isTextareaUntouched) { + } else if (isEditing && !isTextareaUntouched) { this.putConflictEditWarningInPlace(noteEntity, $note); this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else { + } else { const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); this.setupNewNote($updatedNote); } @@ -452,17 +556,31 @@ export default class Notes { } this.note_ids.push(noteEntity.id); - form = $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}`); + form = + $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) { row = form; } 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? - discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); + discussionContainer = $( + `.notes[data-discussion-id="${noteEntity.discussion_id}"]`, + ); if (!discussionContainer.length) { discussionContainer = form.closest('.discussion').find('.notes'); } @@ -470,25 +588,42 @@ export default class Notes { if (noteEntity.diff_discussion_html) { 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 row.after($discussion); } else { // Merge new discussion HTML in - var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); - var contentContainerClass = '.' + $notes.closest('.notes_content') - .attr('class') - .split(' ') - .join('.'); - - row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); + var $notes = $discussion.find( + `.notes[data-discussion-id="${noteEntity.discussion_id}"]`, + ); + var contentContainerClass = + '.' + + $notes + .closest('.notes_content') + .attr('class') + .split(' ') + .join('.'); + + row + .find(contentContainerClass + ' .content') + .append($notes.closest('.content').children()); } } // Init discussion on 'Discussion' page if it is merge request 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()) { - Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); + Notes.animateAppendNote( + noteEntity.discussion_html, + $('.main-notes-list'), + ); } } } else { @@ -496,7 +631,10 @@ export default class Notes { Notes.animateAppendNote(noteEntity.html, discussionContainer); } - if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { + if ( + typeof gl.diffNotesCompileComponents !== 'undefined' && + noteEntity.discussion_resolvable + ) { gl.diffNotesCompileComponents(); this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); @@ -508,7 +646,8 @@ export default class Notes { } getLineHolder(changesDiscussionContainer) { - return $(changesDiscussionContainer).closest('.notes_holder') + return $(changesDiscussionContainer) + .closest('.notes_holder') .prevAll('.line_holder') .first() .get(0); @@ -541,8 +680,14 @@ export default class Notes { form.find('.js-errors').remove(); // reset text and preview form.find('.js-md-write-button').click(); - form.find('.js-note-text').val('').trigger('input'); - form.find('.js-note-text').data('autosave').reset(); + form + .find('.js-note-text') + .val('') + .trigger('input'); + form + .find('.js-note-text') + .data('autosave') + .reset(); var event = document.createEvent('Event'); event.initEvent('autosize:update', true, false); @@ -578,7 +723,10 @@ export default class Notes { form.find('#note_type').val(''); form.find('#note_project_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'); if (form.length) { @@ -632,11 +780,17 @@ export default class Notes { } else if ($form.hasClass('js-discussion-note-form')) { 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) { - 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 { } 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 isAllowed = true; if (currentContent === initialContent) { this.removeNoteEditForm($el); - } - else { + } else { var $buttons = $el.find('.note-form-actions'); var isWidgetVisible = isInViewport($el.get(0)); @@ -754,8 +910,7 @@ export default class Notes { this.setupNewNote($newNote); // Now that we have taken care of the update, clear it out delete this.updatedNotesTrackingMap[noteId]; - } - else { + } else { $note.find('.js-finish-edit-warning').hide(); this.removeNoteEditForm($note); } @@ -788,7 +943,9 @@ export default class Notes { form.removeClass('current-note-edit-form'); form.find('.js-finish-edit-warning').hide(); // 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,58 +959,67 @@ export default class Notes { $note = $(e.currentTarget).closest('.note'); noteElId = $note.attr('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') .prev('.line_holder'); - $(`.note[id="${noteElId}"]`).each((function(_this) { - // 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, - // where $('#noteId') would return only one. - return function(i, el) { - var $note, $notes; - $note = $(el); - $notes = $note.closest('.discussion-notes'); - const discussionId = $('.notes', $notes).data('discussionId'); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - if (gl.diffNoteApps[noteElId]) { - gl.diffNoteApps[noteElId].$destroy(); + $(`.note[id="${noteElId}"]`).each( + (function(_this) { + // 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, + // where $('#noteId') would return only one. + return function(i, el) { + var $note, $notes; + $note = $(el); + $notes = $note.closest('.discussion-notes'); + const discussionId = $('.notes', $notes).data('discussionId'); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (gl.diffNoteApps[noteElId]) { + gl.diffNoteApps[noteElId].$destroy(); + } } - } - - $note.remove(); - - // check if this is the last note for this line - if ($notes.find('.note').length === 0) { - var notesTr = $notes.closest('tr'); - - // "Discussions" tab - $notes.closest('.timeline-entry').remove(); - - $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); - - // The notes tr can contain multiple lists of notes, like on the parallel diff - // notesTr does not exist for image diffs - if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) { - const $diffFile = $notes.closest('.diff-file'); - if ($diffFile.length > 0) { - const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', { - detail: { - // badgeNumber's start with 1 and index starts with 0 - badgeNumber: $notes.index() + 1, - }, - }); - $diffFile[0].dispatchEvent(removeBadgeEvent); + $note.remove(); + + // check if this is the last note for this line + if ($notes.find('.note').length === 0) { + var notesTr = $notes.closest('tr'); + + // "Discussions" tab + $notes.closest('.timeline-entry').remove(); + + $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue'); + + // The notes tr can contain multiple lists of notes, like on the parallel diff + // notesTr does not exist for image diffs + if ( + notesTr.find('.discussion-notes').length > 1 || + notesTr.length === 0 + ) { + const $diffFile = $notes.closest('.diff-file'); + if ($diffFile.length > 0) { + const removeBadgeEvent = new CustomEvent( + 'removeBadge.imageDiff', + { + detail: { + // badgeNumber's start with 1 and index starts with 0 + badgeNumber: $notes.index() + 1, + }, + }, + ); + + $diffFile[0].dispatchEvent(removeBadgeEvent); + } + + $notes.remove(); + } else if (notesTr.length > 0) { + notesTr.remove(); } - - $notes.remove(); - } else if (notesTr.length > 0) { - notesTr.remove(); } - } - }; - })(this)); + }; + })(this), + ); Notes.refreshVueNotes(); Notes.checkMergeRequestStatus(); @@ -935,7 +1101,12 @@ export default class Notes { // DiffNote 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-new-discussion').remove(); this.setupNoteForm(form); @@ -971,7 +1142,7 @@ export default class Notes { this.toggleDiffNote({ target: $link, lineType: link.dataset.lineType, - showReplyInput + showReplyInput, }); } @@ -987,7 +1158,9 @@ export default class Notes { // Setup comment form 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'); if ($form.length === 0) { @@ -1000,13 +1173,17 @@ export default class Notes { this.setupDiscussionNoteForm($link, newForm); } - toggleDiffNote({ - target, - lineType, - forceShow, - showReplyInput = false, - }) { - var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; + toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) { + var $link, + addForm, + hasNotes, + newForm, + noteForm, + replyButton, + row, + rowCssToAdd, + targetContent, + isDiffCommentAvatar; $link = $(target); row = $link.closest('tr'); const nextRow = row.next(); @@ -1018,11 +1195,13 @@ export default class Notes { hasNotes = nextRow.is('.notes_holder'); addForm = false; 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 if (this.isParallelView()) { 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`; let notesContent = targetRow.find(notesContentSelector); @@ -1050,7 +1229,9 @@ export default class Notes { notesContent = targetRow.find(notesContentSelector); addForm = true; } 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 showNow = forceShow === true || (!isCurrentlyShown && !isForced); @@ -1077,11 +1258,12 @@ export default class Notes { row = form.closest('tr'); glForm = form.data('glForm'); glForm.destroy(); - form.find('.js-note-text').data('autosave').reset(); - // show the reply button (will only work for replies) form - .prev('.discussion-reply-holder') - .show(); + .find('.js-note-text') + .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')) { // remove temporary row for diff lines return row.remove(); @@ -1122,7 +1304,9 @@ export default class Notes { var filename, form; form = $(this).closest('form'); // get only the basename - filename = $(this).val().replace(/^.*[\\\/]/, ''); + filename = $(this) + .val() + .replace(/^.*[\\\/]/, ''); return form.find('.js-attachment-filename').text(filename); } @@ -1194,12 +1378,16 @@ export default class Notes { this.glForm = new GLForm($editForm.find('form'), this.enableGFM); - $editForm.find('form') + $editForm + .find('form') .attr('action', `${postUrl}?html=true`) .attr('data-remote', 'true'); $editForm.find('.js-form-target-id').val(targetId); $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('.referenced-users').hide(); } @@ -1208,7 +1396,9 @@ export default class Notes { if ($note.find('.js-conflict-edit-warning').length === 0) { const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> 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 </a> to ensure information is not lost @@ -1218,12 +1408,15 @@ export default class Notes { } 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) { const el = $container.find('.js-code-placeholder').get(0); - new Vue({ // eslint-disable-line no-new + new Vue({ + // eslint-disable-line no-new el, components: { SkeletonLoadingContainer, @@ -1248,7 +1441,9 @@ export default class Notes { $container.find('.line_content').html( $(` <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> `), ); @@ -1266,7 +1461,8 @@ export default class Notes { const fileHolder = $container.find('.file-holder'); const url = fileHolder.data('linesPath'); - axios.get(url) + axios + .get(url) .then(({ data }) => { Notes.renderDiffContent($container, data); }) @@ -1277,9 +1473,14 @@ export default class Notes { toggleCommitList(e) { 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'); } @@ -1289,11 +1490,17 @@ export default class Notes { * intrusive. */ 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) { 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); @@ -1301,7 +1508,9 @@ export default class Notes { $systemNote.find('.note-text').addClass('system-note-commit-list'); $systemNote.find('.system-note-commit-list-toggler').show(); } 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 { cleanForm($form) { // Remove JS classes that are not needed here - $form - .find('.js-comment-type-dropdown') - .removeClass('btn-group'); + $form.find('.js-comment-type-dropdown').removeClass('btn-group'); // Remove dropdown - $form - .find('.dropdown-menu') - .remove(); + $form.find('.dropdown-menu').remove(); return $form; } @@ -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 const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); const currentNoteText = normalizeNewlines( - $note.find('.original-note-content').first().text().trim() + $note + .find('.original-note-content') + .first() + .text() + .trim(), ); return sanitizedNoteEntityText !== currentNoteText; } @@ -1435,7 +1644,14 @@ export default class Notes { * Once comment is _actually_ posted on server, we will have final 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 $tempNote = $( `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> @@ -1449,8 +1665,12 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="hidden-xs">${_.escape(currentUsername)}</span> - <span class="note-headline-light">${_.escape(currentUsername)}</span> + <span class="hidden-xs">${_.escape( + currentUsername, + )}</span> + <span class="note-headline-light">${_.escape( + currentUsername, + )}</span> </a> </div> </div> @@ -1461,11 +1681,13 @@ export default class Notes { </div> </div> </div> - </li>` + </li>`, ); $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; } @@ -1481,7 +1703,7 @@ export default class Notes { <i>${formContent}</i> </div> </div> - </li>` + </li>`, ); return $tempNote; @@ -1513,11 +1735,22 @@ export default class Notes { const $submitBtn = $(e.target); let $form = $submitBtn.parents('form'); 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 isDiscussionForm = $form.hasClass('js-discussion-note-form'); - const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); - const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form); + const isDiscussionResolve = $submitBtn.hasClass( + 'js-comment-resolve-button', + ); + const { + formData, + formContent, + formAction, + formContentOriginal, + } = this.getFormData($form); let noteUniqueId; let systemNoteUniqueId; let hasQuickActions = false; @@ -1547,23 +1780,30 @@ export default class Notes { // Show placeholder note if (tempFormContent) { noteUniqueId = _.uniqueId('tempNote_'); - $notesContainer.append(this.createPlaceholderNote({ - formContent: tempFormContent, - uniqueId: noteUniqueId, - isDiscussionNote, - currentUsername: gon.current_username, - currentUserFullname: gon.current_user_fullname, - currentUserAvatar: gon.current_user_avatar_url, - })); + $notesContainer.append( + this.createPlaceholderNote({ + formContent: tempFormContent, + uniqueId: noteUniqueId, + isDiscussionNote, + currentUsername: gon.current_username, + currentUserFullname: gon.current_user_fullname, + currentUserAvatar: gon.current_user_avatar_url, + }), + ); } // Show placeholder system note if (hasQuickActions) { systemNoteUniqueId = _.uniqueId('tempSystemNote_'); - $notesContainer.append(this.createPlaceholderSystemNote({ - formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), - uniqueId: systemNoteUniqueId, - })); + $notesContainer.append( + this.createPlaceholderSystemNote({ + formContent: this.getQuickActionDescription( + formContent, + AjaxCache.get(gl.GfmAutoComplete.dataSources.commands), + ), + uniqueId: systemNoteUniqueId, + }), + ); } // Clear the form textarea @@ -1577,8 +1817,9 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to submit comment on server - axios.post(`${formAction}?html=true`, formData) - .then((res) => { + axios + .post(`${formAction}?html=true`, formData) + .then(res => { const note = res.data; // Submission successful! remove placeholder @@ -1595,7 +1836,9 @@ export default class Notes { // Reset cached commands list when command is applied 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 @@ -1640,11 +1883,14 @@ export default class Notes { // append flash-container to the Notes list 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(); - } 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 this.addNote($form, note); this.reenableTargetFormSubmitButton(e); @@ -1655,7 +1901,8 @@ export default class Notes { } $form.trigger('ajax:success', [note]); - }).catch(() => { + }) + .catch(() => { // Submission failed, remove placeholder note and show Flash error message $notesContainer.find(`#${noteUniqueId}`).remove(); @@ -1675,7 +1922,9 @@ export default class Notes { // Show form again on UI on failure 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]); $form = $notesContainer.parent().find('form'); } @@ -1720,12 +1969,19 @@ export default class Notes { // Show updated comment content temporarily $noteBodyText.html(formContent); - $editingNote.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>'); + $editingNote + .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 */ // Make request to update comment on server - axios.post(`${formAction}?html=true`, formData) + axios + .post(`${formAction}?html=true`, formData) .then(({ data }) => { // Submission successful! render final note element this.updateNote(data, $editingNote); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 42bc383f4d2..90dcafd75b7 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -1,305 +1,311 @@ <script> - import $ from 'jquery'; - import { mapActions, mapGetters, mapState } from 'vuex'; - import _ from 'underscore'; - import Autosize from 'autosize'; - import { __, sprintf } from '~/locale'; - import Flash from '../../flash'; - import Autosave from '../../autosave'; - import TaskList from '../../task_list'; - import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility'; - import * as constants from '../constants'; - import eventHub from '../event_hub'; - import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; - import markdownField from '../../vue_shared/components/markdown/field.vue'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - 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'; +import $ from 'jquery'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import _ from 'underscore'; +import Autosize from 'autosize'; +import { __, sprintf } from '~/locale'; +import Flash from '../../flash'; +import Autosave from '../../autosave'; +import TaskList from '../../task_list'; +import { + capitalizeFirstCharacter, + convertToCamelCase, +} from '../../lib/utils/text_utility'; +import * as constants from '../constants'; +import eventHub from '../event_hub'; +import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; +import markdownField from '../../vue_shared/components/markdown/field.vue'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +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 { - name: 'CommentForm', - components: { - issueWarning, - noteSignedOutWidget, - discussionLockedWidget, - markdownField, - userAvatarLink, - loadingButton, +export default { + name: 'CommentForm', + components: { + issueWarning, + noteSignedOutWidget, + discussionLockedWidget, + markdownField, + userAvatarLink, + loadingButton, + }, + mixins: [issuableStateMixin], + props: { + noteableType: { + type: String, + required: true, }, - mixins: [ - issuableStateMixin, - ], - props: { - noteableType: { - type: String, - required: true, - }, + }, + data() { + return { + note: '', + noteType: constants.COMMENT, + isSubmitting: false, + isSubmitButtonDisabled: true, + }; + }, + computed: { + ...mapGetters([ + 'getCurrentUserLastNote', + 'getUserData', + 'getNoteableData', + 'getNotesData', + 'openState', + ]), + ...mapState(['isToggleStateButtonLoading']), + noteableDisplayName() { + return this.noteableType.replace(/_/g, ' '); }, - data() { - return { - note: '', - noteType: constants.COMMENT, - isSubmitting: false, - isSubmitButtonDisabled: true, - }; + isLoggedIn() { + return this.getUserData.id; + }, + commentButtonTitle() { + return this.noteType === constants.COMMENT + ? 'Comment' + : 'Start discussion'; }, - computed: { - ...mapGetters([ - 'getCurrentUserLastNote', - 'getUserData', - 'getNoteableData', - 'getNotesData', - 'openState', - ]), - ...mapState([ - 'isToggleStateButtonLoading', - ]), - noteableDisplayName() { - return this.noteableType.replace(/_/g, ' '); - }, - isLoggedIn() { - return this.getUserData.id; - }, - commentButtonTitle() { - return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; - }, - isOpen() { - return this.openState === constants.OPENED || this.openState === constants.REOPENED; - }, - canCreateNote() { - return this.getNoteableData.current_user.can_create_note; - }, - issueActionButtonTitle() { - const openOrClose = this.isOpen ? 'close' : 'reopen'; + isOpen() { + return ( + this.openState === constants.OPENED || + this.openState === constants.REOPENED + ); + }, + canCreateNote() { + return this.getNoteableData.current_user.can_create_note; + }, + issueActionButtonTitle() { + const openOrClose = this.isOpen ? 'close' : 'reopen'; - if (this.note.length) { - return sprintf( - __('%{actionText} & %{openOrClose} %{noteable}'), - { - actionText: this.commentButtonTitle, - openOrClose, - noteable: this.noteableDisplayName, - }, - ); - } + if (this.note.length) { + return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), { + actionText: this.commentButtonTitle, + openOrClose, + noteable: this.noteableDisplayName, + }); + } - return sprintf( - __('%{openOrClose} %{noteable}'), - { - openOrClose: capitalizeFirstCharacter(openOrClose), - noteable: this.noteableDisplayName, - }, - ); - }, - actionButtonClassNames() { - return { - 'btn-reopen': !this.isOpen, - 'btn-close': this.isOpen, - 'js-note-target-close': this.isOpen, - 'js-note-target-reopen': !this.isOpen, - }; - }, - markdownDocsPath() { - return this.getNotesData.markdownDocsPath; - }, - quickActionsDocsPath() { - return this.getNotesData.quickActionsDocsPath; - }, - markdownPreviewPath() { - return this.getNoteableData.preview_note_path; - }, - author() { - return this.getUserData; - }, - canUpdateIssue() { - return this.getNoteableData.current_user.can_update; - }, - endpoint() { - return this.getNoteableData.create_note_path; - }, + return sprintf(__('%{openOrClose} %{noteable}'), { + openOrClose: capitalizeFirstCharacter(openOrClose), + noteable: this.noteableDisplayName, + }); }, - watch: { - note(newNote) { - this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); - }, - isSubmitting(newValue) { - this.setIsSubmitButtonDisabled(this.note, newValue); - }, + actionButtonClassNames() { + return { + 'btn-reopen': !this.isOpen, + 'btn-close': this.isOpen, + 'js-note-target-close': this.isOpen, + 'js-note-target-reopen': !this.isOpen, + }; }, - mounted() { - // jQuery is needed here because it is a custom event being dispatched with jQuery. - $(document).on('issuable:change', (e, isClosed) => { - this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED); - }); + markdownDocsPath() { + return this.getNotesData.markdownDocsPath; + }, + quickActionsDocsPath() { + return this.getNotesData.quickActionsDocsPath; + }, + markdownPreviewPath() { + return this.getNoteableData.preview_note_path; + }, + author() { + return this.getUserData; + }, + canUpdateIssue() { + return this.getNoteableData.current_user.can_update; + }, + endpoint() { + return this.getNoteableData.create_note_path; + }, + }, + watch: { + note(newNote) { + this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); + }, + isSubmitting(newValue) { + this.setIsSubmitButtonDisabled(this.note, newValue); + }, + }, + mounted() { + // jQuery is needed here because it is a custom event being dispatched with jQuery. + $(document).on('issuable:change', (e, isClosed) => { + this.toggleIssueLocalState( + isClosed ? constants.CLOSED : constants.REOPENED, + ); + }); - this.initAutoSave(); - this.initTaskList(); + this.initAutoSave(); + this.initTaskList(); + }, + methods: { + ...mapActions([ + 'saveNote', + 'stopPolling', + 'restartPolling', + 'removePlaceholderNotes', + 'closeIssue', + 'reopenIssue', + 'toggleIssueLocalState', + 'toggleStateButtonLoading', + ]), + setIsSubmitButtonDisabled(note, isSubmitting) { + if (!_.isEmpty(note) && !isSubmitting) { + this.isSubmitButtonDisabled = false; + } else { + this.isSubmitButtonDisabled = true; + } }, - methods: { - ...mapActions([ - 'saveNote', - 'stopPolling', - 'restartPolling', - 'removePlaceholderNotes', - 'closeIssue', - 'reopenIssue', - 'toggleIssueLocalState', - 'toggleStateButtonLoading', - ]), - setIsSubmitButtonDisabled(note, isSubmitting) { - if (!_.isEmpty(note) && !isSubmitting) { - this.isSubmitButtonDisabled = false; - } else { - this.isSubmitButtonDisabled = true; - } - }, - handleSave(withIssueAction) { - this.isSubmitting = true; + handleSave(withIssueAction) { + this.isSubmitting = true; - if (this.note.length) { - const noteData = { - endpoint: this.endpoint, - flashContainer: this.$el, - data: { - note: { - noteable_type: this.noteableType, - noteable_id: this.getNoteableData.id, - note: this.note, - }, + if (this.note.length) { + const noteData = { + endpoint: this.endpoint, + flashContainer: this.$el, + data: { + note: { + noteable_type: this.noteableType, + noteable_id: this.getNoteableData.id, + note: this.note, }, - }; + }, + }; - if (this.noteType === constants.DISCUSSION) { - noteData.data.note.type = constants.DISCUSSION_NOTE; - } + if (this.noteType === constants.DISCUSSION) { + noteData.data.note.type = constants.DISCUSSION_NOTE; + } - this.note = ''; // Empty textarea while being requested. Repopulate in catch - this.resizeTextarea(); - this.stopPolling(); + this.note = ''; // Empty textarea while being requested. Repopulate in catch + this.resizeTextarea(); + this.stopPolling(); - this.saveNote(noteData) - .then((res) => { - this.enableButton(); - this.restartPolling(); + this.saveNote(noteData) + .then(res => { + this.enableButton(); + this.restartPolling(); - if (res.errors) { - if (res.errors.commands_only) { - this.discard(); - } else { - Flash( - 'Something went wrong while adding your comment. Please try again.', - 'alert', - this.$refs.commentForm, - ); - } - } else { + if (res.errors) { + if (res.errors.commands_only) { this.discard(); + } else { + Flash( + 'Something went wrong while adding your comment. Please try again.', + 'alert', + this.$refs.commentForm, + ); } + } else { + this.discard(); + } - if (withIssueAction) { - this.toggleIssueState(); - } - }) - .catch(() => { - this.enableButton(); - this.discard(false); - const msg = - `Your comment could not be submitted! + if (withIssueAction) { + this.toggleIssueState(); + } + }) + .catch(() => { + this.enableButton(); + this.discard(false); + const msg = `Your comment could not be submitted! Please check your network connection and try again.`; - Flash(msg, 'alert', this.$el); - this.note = noteData.data.note.note; // Restore textarea content. - this.removePlaceholderNotes(); - }); - } else { - this.toggleIssueState(); - } - }, - enableButton() { - this.isSubmitting = false; - }, - toggleIssueState() { - if (this.isOpen) { - this.closeIssue() - .then(() => this.enableButton()) - .catch(() => { - this.enableButton(); - this.toggleStateButtonLoading(false); - Flash( - sprintf( - __('Something went wrong while closing the %{issuable}. Please try again later'), - { issuable: this.noteableDisplayName }, + Flash(msg, 'alert', this.$el); + this.note = noteData.data.note.note; // Restore textarea content. + this.removePlaceholderNotes(); + }); + } else { + this.toggleIssueState(); + } + }, + enableButton() { + this.isSubmitting = false; + }, + toggleIssueState() { + if (this.isOpen) { + this.closeIssue() + .then(() => this.enableButton()) + .catch(() => { + this.enableButton(); + this.toggleStateButtonLoading(false); + Flash( + sprintf( + __( + 'Something went wrong while closing the %{issuable}. Please try again later', ), - ); - }); - } else { - this.reopenIssue() - .then(() => this.enableButton()) - .catch(() => { - this.enableButton(); - this.toggleStateButtonLoading(false); - Flash( - sprintf( - __('Something went wrong while reopening the %{issuable}. Please try again later'), - { issuable: this.noteableDisplayName }, + { issuable: this.noteableDisplayName }, + ), + ); + }); + } else { + this.reopenIssue() + .then(() => this.enableButton()) + .catch(() => { + this.enableButton(); + this.toggleStateButtonLoading(false); + Flash( + sprintf( + __( + 'Something went wrong while reopening the %{issuable}. Please try again later', ), - ); - }); - } - }, - discard(shouldClear = true) { - // `blur` is needed to clear slash commands autocomplete cache if event fired. - // `focus` is needed to remain cursor in the textarea. - this.$refs.textarea.blur(); - this.$refs.textarea.focus(); + { issuable: this.noteableDisplayName }, + ), + ); + }); + } + }, + discard(shouldClear = true) { + // `blur` is needed to clear slash commands autocomplete cache if event fired. + // `focus` is needed to remain cursor in the textarea. + this.$refs.textarea.blur(); + this.$refs.textarea.focus(); - if (shouldClear) { - this.note = ''; - this.resizeTextarea(); - this.$refs.markdownField.previewMarkdown = false; - } + if (shouldClear) { + this.note = ''; + this.resizeTextarea(); + this.$refs.markdownField.previewMarkdown = false; + } - this.autosave.reset(); - }, - setNoteType(type) { - this.noteType = type; - }, - editCurrentUserLastNote() { - if (this.note === '') { - const lastNote = this.getCurrentUserLastNote; + this.autosave.reset(); + }, + setNoteType(type) { + this.noteType = type; + }, + editCurrentUserLastNote() { + if (this.note === '') { + const lastNote = this.getCurrentUserLastNote; - if (lastNote) { - eventHub.$emit('enterEditMode', { - noteId: lastNote.id, - }); - } + if (lastNote) { + eventHub.$emit('enterEditMode', { + noteId: lastNote.id, + }); } - }, - initAutoSave() { - if (this.isLoggedIn) { - const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); + } + }, + initAutoSave() { + if (this.isLoggedIn) { + const noteableType = capitalizeFirstCharacter( + convertToCamelCase(this.noteableType), + ); - this.autosave = new Autosave( - $(this.$refs.textarea), - ['Note', noteableType, this.getNoteableData.id], - ); - } - }, - initTaskList() { - return new TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes', - }); - }, - resizeTextarea() { - this.$nextTick(() => { - Autosize.update(this.$refs.textarea); - }); - }, + this.autosave = new Autosave($(this.$refs.textarea), [ + 'Note', + noteableType, + this.getNoteableData.id, + ]); + } + }, + initTaskList() { + return new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); + }, + resizeTextarea() { + this.$nextTick(() => { + Autosize.update(this.$refs.textarea); + }); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue index 3bcde17f07c..94d9dc69964 100644 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ b/app/assets/javascripts/notes/components/diff_file_header.vue @@ -1,24 +1,24 @@ <script> - import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - import Icon from '~/vue_shared/components/icon.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import Icon from '~/vue_shared/components/icon.vue'; - export default { - components: { - ClipboardButton, - Icon, +export default { + components: { + ClipboardButton, + Icon, + }, + props: { + diffFile: { + type: Object, + required: true, }, - props: { - diffFile: { - type: Object, - required: true, - }, + }, + computed: { + titleTag() { + return this.diffFile.discussionPath ? 'a' : 'span'; }, - computed: { - titleTag() { - return this.diffFile.discussionPath ? 'a' : 'span'; - }, - }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 1dba84fac18..ee01ec85bbb 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -1,56 +1,60 @@ <script> - import $ from 'jquery'; - import syntaxHighlight from '~/syntax_highlight'; - import imageDiffHelper from '~/image_diff/helpers/index'; - import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; - import DiffFileHeader from './diff_file_header.vue'; +import $ from 'jquery'; +import syntaxHighlight from '~/syntax_highlight'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import DiffFileHeader from './diff_file_header.vue'; - export default { - components: { - DiffFileHeader, +export default { + components: { + DiffFileHeader, + }, + props: { + discussion: { + type: Object, + required: true, }, - props: { - discussion: { - type: Object, - required: true, - }, + }, + computed: { + isImageDiff() { + return !this.diffFile.text; }, - computed: { - isImageDiff() { - return !this.diffFile.text; - }, - diffFileClass() { - const { text } = this.diffFile; - return text ? 'text-file' : 'js-image-file'; - }, - diffRows() { - return $(this.discussion.truncatedDiffLines); - }, - diffFile() { - return convertObjectPropsToCamelCase(this.discussion.diffFile); - }, - imageDiffHtml() { - return this.discussion.imageDiffHtml; - }, + diffFileClass() { + const { text } = this.diffFile; + return text ? 'text-file' : 'js-image-file'; }, - mounted() { - if (this.isImageDiff) { - const canCreateNote = false; - const renderCommentBadge = true; - imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); - } else { - const fileHolder = $(this.$refs.fileHolder); - this.$nextTick(() => { - syntaxHighlight(fileHolder); - }); - } + diffRows() { + return $(this.discussion.truncatedDiffLines); }, - methods: { - rowTag(html) { - return html.outerHTML ? 'tr' : 'template'; - }, + diffFile() { + return convertObjectPropsToCamelCase(this.discussion.diffFile); }, - }; + imageDiffHtml() { + return this.discussion.imageDiffHtml; + }, + }, + mounted() { + if (this.isImageDiff) { + const canCreateNote = false; + const renderCommentBadge = true; + imageDiffHelper.initImageDiff( + this.$refs.fileHolder, + canCreateNote, + renderCommentBadge, + ); + } else { + const fileHolder = $(this.$refs.fileHolder); + this.$nextTick(() => { + syntaxHighlight(fileHolder); + }); + } + }, + methods: { + rowTag(html) { + return html.outerHTML ? 'tr' : 'template'; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 0158f58b569..d492d1cd001 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,67 +1,69 @@ <script> - import { mapGetters } from 'vuex'; - import resolveSvg from 'icons/_icon_resolve_discussion.svg'; - import resolvedSvg from 'icons/_icon_status_success_solid.svg'; - import mrIssueSvg from 'icons/_icon_mr_issue.svg'; - import nextDiscussionSvg from 'icons/_next_discussion.svg'; - import { pluralize } from '../../lib/utils/text_utility'; - import { scrollToElement } from '../../lib/utils/common_utils'; - import tooltip from '../../vue_shared/directives/tooltip'; +import { mapGetters } from 'vuex'; +import resolveSvg from 'icons/_icon_resolve_discussion.svg'; +import resolvedSvg from 'icons/_icon_status_success_solid.svg'; +import mrIssueSvg from 'icons/_icon_mr_issue.svg'; +import nextDiscussionSvg from 'icons/_next_discussion.svg'; +import { pluralize } from '../../lib/utils/text_utility'; +import { scrollToElement } from '../../lib/utils/common_utils'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + computed: { + ...mapGetters([ + 'getUserData', + 'getNoteableData', + 'discussionCount', + 'unresolvedDiscussions', + 'resolvedDiscussionCount', + ]), + isLoggedIn() { + return this.getUserData.id; }, - computed: { - ...mapGetters([ - 'getUserData', - 'getNoteableData', - 'discussionCount', - 'unresolvedDiscussions', - 'resolvedDiscussionCount', - ]), - isLoggedIn() { - return this.getUserData.id; - }, - hasNextButton() { - return this.isLoggedIn && !this.allResolved; - }, - countText() { - return pluralize('discussion', this.discussionCount); - }, - allResolved() { - return this.resolvedDiscussionCount === this.discussionCount; - }, - resolveAllDiscussionsIssuePath() { - return this.getNoteableData.create_issue_to_resolve_discussions_path; - }, - firstUnresolvedDiscussionId() { - const item = this.unresolvedDiscussions[0] || {}; - - return item.id; - }, + hasNextButton() { + return this.isLoggedIn && !this.allResolved; + }, + countText() { + return pluralize('discussion', this.discussionCount); + }, + allResolved() { + return this.resolvedDiscussionCount === this.discussionCount; }, - created() { - this.resolveSvg = resolveSvg; - this.resolvedSvg = resolvedSvg; - this.mrIssueSvg = mrIssueSvg; - this.nextDiscussionSvg = nextDiscussionSvg; + resolveAllDiscussionsIssuePath() { + return this.getNoteableData.create_issue_to_resolve_discussions_path; + }, + firstUnresolvedDiscussionId() { + const item = this.unresolvedDiscussions[0] || {}; + + return item.id; }, - methods: { - jumpToFirstDiscussion() { - const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`); - const activeTab = window.mrTabs.currentAction; + }, + created() { + this.resolveSvg = resolveSvg; + this.resolvedSvg = resolvedSvg; + this.mrIssueSvg = mrIssueSvg; + this.nextDiscussionSvg = nextDiscussionSvg; + }, + methods: { + jumpToFirstDiscussion() { + const el = document.querySelector( + `[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`, + ); + const activeTab = window.mrTabs.currentAction; - if (activeTab === 'commits' || activeTab === 'pipelines') { - window.mrTabs.activateTab('show'); - } + if (activeTab === 'commits' || activeTab === 'pipelines') { + window.mrTabs.activateTab('show'); + } - if (el) { - scrollToElement(el); - } - }, + if (el) { + scrollToElement(el); + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index fc0722042cc..13283b187d1 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -1,15 +1,13 @@ <script> - import Icon from '~/vue_shared/components/icon.vue'; - import Issuable from '~/vue_shared/mixins/issuable'; +import Icon from '~/vue_shared/components/icon.vue'; +import Issuable from '~/vue_shared/mixins/issuable'; - export default { - components: { - Icon, - }, - mixins: [ - Issuable, - ], - }; +export default { + components: { + Icon, + }, + mixins: [Issuable], +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index c26aa6fa15d..a7e2d857013 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,121 +1,119 @@ <script> - import { mapGetters } from 'vuex'; - import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; - import emojiSmile from 'icons/_emoji_smile.svg'; - import emojiSmiley from 'icons/_emoji_smiley.svg'; - import editSvg from 'icons/_icon_pencil.svg'; - import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; - import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; - import ellipsisSvg from 'icons/_ellipsis_v.svg'; - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; +import { mapGetters } from 'vuex'; +import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; +import emojiSmile from 'icons/_emoji_smile.svg'; +import emojiSmiley from 'icons/_emoji_smiley.svg'; +import editSvg from 'icons/_icon_pencil.svg'; +import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; +import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; +import ellipsisSvg from 'icons/_ellipsis_v.svg'; +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; - export default { - name: 'NoteActions', - directives: { - tooltip, - }, - components: { - loadingIcon, - }, - props: { - authorId: { - type: Number, - required: true, - }, - noteId: { - type: Number, - required: true, - }, - accessLevel: { - type: String, - required: false, - default: '', - }, - reportAbusePath: { - type: String, - required: true, - }, - canEdit: { - type: Boolean, - required: true, - }, - canDelete: { - type: Boolean, - required: true, - }, - resolvable: { - type: Boolean, - required: false, - default: false, - }, - isResolved: { - type: Boolean, - required: false, - default: false, - }, - isResolving: { - type: Boolean, - required: false, - default: false, - }, - resolvedBy: { - type: Object, - required: false, - default: () => ({}), - }, - canReportAsAbuse: { - type: Boolean, - required: true, - }, - }, - computed: { - ...mapGetters([ - 'getUserDataByProp', - ]), - shouldShowActionsDropdown() { - return this.currentUserId && (this.canEdit || this.canReportAsAbuse); - }, - canAddAwardEmoji() { - return this.currentUserId; - }, - isAuthoredByCurrentUser() { - return this.authorId === this.currentUserId; - }, - currentUserId() { - return this.getUserDataByProp('id'); - }, - resolveButtonTitle() { - let title = 'Mark as resolved'; +export default { + name: 'NoteActions', + directives: { + tooltip, + }, + components: { + loadingIcon, + }, + props: { + authorId: { + type: Number, + required: true, + }, + noteId: { + type: Number, + required: true, + }, + accessLevel: { + type: String, + required: false, + default: '', + }, + reportAbusePath: { + type: String, + required: true, + }, + canEdit: { + type: Boolean, + required: true, + }, + canDelete: { + type: Boolean, + required: true, + }, + resolvable: { + type: Boolean, + required: false, + default: false, + }, + isResolved: { + type: Boolean, + required: false, + default: false, + }, + isResolving: { + type: Boolean, + required: false, + default: false, + }, + resolvedBy: { + type: Object, + required: false, + default: () => ({}), + }, + canReportAsAbuse: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters(['getUserDataByProp']), + shouldShowActionsDropdown() { + return this.currentUserId && (this.canEdit || this.canReportAsAbuse); + }, + canAddAwardEmoji() { + return this.currentUserId; + }, + isAuthoredByCurrentUser() { + return this.authorId === this.currentUserId; + }, + currentUserId() { + return this.getUserDataByProp('id'); + }, + resolveButtonTitle() { + let title = 'Mark as resolved'; - if (this.resolvedBy) { - title = `Resolved by ${this.resolvedBy.name}`; - } + if (this.resolvedBy) { + title = `Resolved by ${this.resolvedBy.name}`; + } - return title; - }, - }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - this.editSvg = editSvg; - this.ellipsisSvg = ellipsisSvg; - this.resolveDiscussionSvg = resolveDiscussionSvg; - this.resolvedDiscussionSvg = resolvedDiscussionSvg; - }, - methods: { - onEdit() { - this.$emit('handleEdit'); - }, - onDelete() { - this.$emit('handleDelete'); - }, - onResolve() { - this.$emit('handleResolve'); - }, - }, - }; + return title; + }, + }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + this.editSvg = editSvg; + this.ellipsisSvg = ellipsisSvg; + this.resolveDiscussionSvg = resolveDiscussionSvg; + this.resolvedDiscussionSvg = resolvedDiscussionSvg; + }, + methods: { + onEdit() { + this.$emit('handleEdit'); + }, + onDelete() { + this.$emit('handleDelete'); + }, + onResolve() { + this.$emit('handleResolve'); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue index 618b807b9cc..34ecbd00c63 100644 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ b/app/assets/javascripts/notes/components/note_attachment.vue @@ -1,13 +1,13 @@ <script> - export default { - name: 'NoteAttachment', - props: { - attachment: { - type: Object, - required: true, - }, +export default { + name: 'NoteAttachment', + props: { + attachment: { + type: Object, + required: true, }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index caa9701e03f..6cb8229e268 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -1,179 +1,192 @@ <script> - import { mapActions, mapGetters } from 'vuex'; - import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; - import emojiSmile from 'icons/_emoji_smile.svg'; - import emojiSmiley from 'icons/_emoji_smiley.svg'; - import Flash from '../../flash'; - import { glEmojiTag } from '../../emoji'; - import tooltip from '../../vue_shared/directives/tooltip'; - - export default { - directives: { - tooltip, +import { mapActions, mapGetters } from 'vuex'; +import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; +import emojiSmile from 'icons/_emoji_smile.svg'; +import emojiSmiley from 'icons/_emoji_smiley.svg'; +import Flash from '../../flash'; +import { glEmojiTag } from '../../emoji'; +import tooltip from '../../vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + awards: { + type: Array, + required: true, }, - props: { - awards: { - type: Array, - required: true, - }, - toggleAwardPath: { - type: String, - required: true, - }, - noteAuthorId: { - type: Number, - required: true, - }, - noteId: { - type: Number, - required: true, - }, + toggleAwardPath: { + type: String, + required: true, }, - computed: { - ...mapGetters([ - 'getUserData', - ]), - // `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 } ] - // This method will group emojis by their name as an Object. See below. - // { - // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], - // bar: [ { name: bar, user: user1 } ] - // } - // We need to do this otherwise we will render the same emoji over and over again. - groupedAwards() { - const awards = this.awards.reduce((acc, award) => { - if (Object.prototype.hasOwnProperty.call(acc, award.name)) { - acc[award.name].push(award); - } else { - Object.assign(acc, { [award.name]: [award] }); - } - - return acc; - }, {}); - - const orderedAwards = {}; - const { thumbsdown, thumbsup } = awards; - // Always show thumbsup and thumbsdown first - if (thumbsup) { - orderedAwards.thumbsup = thumbsup; - delete awards.thumbsup; - } - if (thumbsdown) { - orderedAwards.thumbsdown = thumbsdown; - delete awards.thumbsdown; - } - - return Object.assign({}, orderedAwards, awards); - }, - isAuthoredByMe() { - return this.noteAuthorId === this.getUserData.id; - }, - isLoggedIn() { - return this.getUserData.id; - }, + noteAuthorId: { + type: Number, + required: true, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; + noteId: { + type: Number, + required: true, }, - methods: { - ...mapActions([ - 'toggleAwardRequest', - ]), - getAwardHTML(name) { - return glEmojiTag(name); - }, - getAwardClassBindings(awardList, awardName) { - return { - active: this.hasReactionByCurrentUser(awardList), - disabled: !this.canInteractWithEmoji(awardList, awardName), - }; - }, - canInteractWithEmoji(awardList, awardName) { - let isAllowed = true; - const restrictedEmojis = ['thumbsup', 'thumbsdown']; - - // Users can not add :+1: and :-1: to their own notes - if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) { - isAllowed = false; - } - - return this.getUserData.id && isAllowed; - }, - hasReactionByCurrentUser(awardList) { - return awardList.filter(award => award.user.id === this.getUserData.id).length; - }, - awardTitle(awardsList) { - const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); - const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; - let awardList = awardsList; - - // Filter myself from list if I am awarded. - if (hasReactionByCurrentUser) { - awardList = awardList.filter(award => award.user.id !== this.getUserData.id); - } - - // Get only 9-10 usernames to show in tooltip text. - const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); - - // Get the remaining list to use in `and x more` text. - const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); - - // Add myself to the begining of the list so title will start with You. - if (hasReactionByCurrentUser) { - namesToShow.unshift('You'); - } - - let title = ''; - - // We have 10+ awarded user, join them with comma and add `and x more`. - if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; - } else if (namesToShow.length > 1) { - // 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(', '); - // If we have more than 2 users we need an extra comma before and text. - title += namesToShow.length > 2 ? ',' : ''; - title += ` and ${namesToShow.slice(-1)}`; // Append and text - } else { // We have only 2 users so join them with and. - title = namesToShow.join(' and '); - } - - return title; - }, - handleAward(awardName) { - if (!this.isLoggedIn) { - return; - } - - let parsedName; - - // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string - switch (awardName) { - case '100': - parsedName = 100; - break; - case '1234': - parsedName = 1234; - break; - default: - parsedName = awardName; - break; + }, + computed: { + ...mapGetters(['getUserData']), + // `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 } ] + // This method will group emojis by their name as an Object. See below. + // { + // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ], + // bar: [ { name: bar, user: user1 } ] + // } + // We need to do this otherwise we will render the same emoji over and over again. + groupedAwards() { + const awards = this.awards.reduce((acc, award) => { + if (Object.prototype.hasOwnProperty.call(acc, award.name)) { + acc[award.name].push(award); + } else { + Object.assign(acc, { [award.name]: [award] }); } - const data = { - endpoint: this.toggleAwardPath, - noteId: this.noteId, - awardName: parsedName, - }; - - this.toggleAwardRequest(data) - .catch(() => Flash('Something went wrong on our end.')); - }, + return acc; + }, {}); + + const orderedAwards = {}; + const { thumbsdown, thumbsup } = awards; + // Always show thumbsup and thumbsdown first + if (thumbsup) { + orderedAwards.thumbsup = thumbsup; + delete awards.thumbsup; + } + if (thumbsdown) { + orderedAwards.thumbsdown = thumbsdown; + delete awards.thumbsdown; + } + + return Object.assign({}, orderedAwards, awards); + }, + isAuthoredByMe() { + return this.noteAuthorId === this.getUserData.id; + }, + isLoggedIn() { + return this.getUserData.id; + }, + }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + }, + methods: { + ...mapActions(['toggleAwardRequest']), + getAwardHTML(name) { + return glEmojiTag(name); + }, + getAwardClassBindings(awardList, awardName) { + return { + active: this.hasReactionByCurrentUser(awardList), + disabled: !this.canInteractWithEmoji(awardList, awardName), + }; + }, + canInteractWithEmoji(awardList, awardName) { + let isAllowed = true; + const restrictedEmojis = ['thumbsup', 'thumbsdown']; + + // Users can not add :+1: and :-1: to their own notes + if ( + this.getUserData.id === this.noteAuthorId && + restrictedEmojis.indexOf(awardName) > -1 + ) { + isAllowed = false; + } + + return this.getUserData.id && isAllowed; + }, + hasReactionByCurrentUser(awardList) { + return awardList.filter(award => award.user.id === this.getUserData.id) + .length; + }, + awardTitle(awardsList) { + const hasReactionByCurrentUser = this.hasReactionByCurrentUser( + awardsList, + ); + const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; + let awardList = awardsList; + + // Filter myself from list if I am awarded. + if (hasReactionByCurrentUser) { + awardList = awardList.filter( + award => award.user.id !== this.getUserData.id, + ); + } + + // Get only 9-10 usernames to show in tooltip text. + const namesToShow = awardList + .slice(0, TOOLTIP_NAME_COUNT) + .map(award => award.user.name); + + // Get the remaining list to use in `and x more` text. + const remainingAwardList = awardList.slice( + TOOLTIP_NAME_COUNT, + awardList.length, + ); + + // Add myself to the begining of the list so title will start with You. + if (hasReactionByCurrentUser) { + namesToShow.unshift('You'); + } + + let title = ''; + + // We have 10+ awarded user, join them with comma and add `and x more`. + if (remainingAwardList.length) { + title = `${namesToShow.join(', ')}, and ${ + remainingAwardList.length + } more.`; + } else if (namesToShow.length > 1) { + // 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(', '); + // If we have more than 2 users we need an extra comma before and text. + title += namesToShow.length > 2 ? ',' : ''; + title += ` and ${namesToShow.slice(-1)}`; // Append and text + } else { + // We have only 2 users so join them with and. + title = namesToShow.join(' and '); + } + + return title; + }, + handleAward(awardName) { + if (!this.isLoggedIn) { + return; + } + + let parsedName; + + // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string + switch (awardName) { + case '100': + parsedName = 100; + break; + case '1234': + parsedName = 1234; + break; + default: + parsedName = awardName; + break; + } + + const data = { + endpoint: this.toggleAwardPath, + noteId: this.noteId, + awardName: parsedName, + }; + + this.toggleAwardRequest(data).catch(() => + Flash('Something went wrong on our end.'), + ); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index a94f1a28a4c..069f94c5845 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,83 +1,81 @@ <script> - import $ from 'jquery'; - import noteEditedText from './note_edited_text.vue'; - import noteAwardsList from './note_awards_list.vue'; - import noteAttachment from './note_attachment.vue'; - import noteForm from './note_form.vue'; - import TaskList from '../../task_list'; - import autosave from '../mixins/autosave'; +import $ from 'jquery'; +import noteEditedText from './note_edited_text.vue'; +import noteAwardsList from './note_awards_list.vue'; +import noteAttachment from './note_attachment.vue'; +import noteForm from './note_form.vue'; +import TaskList from '../../task_list'; +import autosave from '../mixins/autosave'; - export default { - components: { - noteEditedText, - noteAwardsList, - noteAttachment, - noteForm, +export default { + components: { + noteEditedText, + noteAwardsList, + noteAttachment, + noteForm, + }, + mixins: [autosave], + props: { + note: { + type: Object, + required: true, }, - mixins: [ - autosave, - ], - props: { - note: { - type: Object, - required: true, - }, - canEdit: { - type: Boolean, - required: true, - }, - isEditing: { - type: Boolean, - required: false, - default: false, - }, + canEdit: { + type: Boolean, + required: true, }, - computed: { - noteBody() { - return this.note.note; - }, + isEditing: { + type: Boolean, + required: false, + default: false, }, - mounted() { - this.renderGFM(); - this.initTaskList(); + }, + computed: { + noteBody() { + return this.note.note; + }, + }, + mounted() { + this.renderGFM(); + this.initTaskList(); + + if (this.isEditing) { + this.initAutoSave(this.note.noteable_type); + } + }, + updated() { + this.initTaskList(); + this.renderGFM(); - if (this.isEditing) { + if (this.isEditing) { + if (!this.autosave) { this.initAutoSave(this.note.noteable_type); + } else { + this.setAutoSave(); } + } + }, + methods: { + renderGFM() { + $(this.$refs['note-body']).renderGFM(); }, - updated() { - this.initTaskList(); - this.renderGFM(); - - if (this.isEditing) { - if (!this.autosave) { - this.initAutoSave(this.note.noteable_type); - } else { - this.setAutoSave(); - } + initTaskList() { + if (this.canEdit) { + this.taskList = new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); } }, - methods: { - renderGFM() { - $(this.$refs['note-body']).renderGFM(); - }, - initTaskList() { - if (this.canEdit) { - this.taskList = new TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes', - }); - } - }, - handleFormUpdate(note, parentElement, callback) { - this.$emit('handleFormUpdate', note, parentElement, callback); - }, - formCancelHandler(shouldConfirm, isDirty) { - this.$emit('cancelFormEdition', shouldConfirm, isDirty); - }, + handleFormUpdate(note, parentElement, callback) { + this.$emit('handleFormUpdate', note, parentElement, callback); + }, + formCancelHandler(shouldConfirm, isDirty) { + this.$emit('cancelFormEdition', shouldConfirm, isDirty); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index ae2e52554d2..4ddca918495 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -1,32 +1,32 @@ <script> - import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; - export default { - name: 'EditedNoteText', - components: { - timeAgoTooltip, +export default { + name: 'EditedNoteText', + components: { + timeAgoTooltip, + }, + props: { + actionText: { + type: String, + required: true, }, - props: { - actionText: { - type: String, - required: true, - }, - editedAt: { - type: String, - required: true, - }, - editedBy: { - type: Object, - required: false, - default: () => ({}), - }, - className: { - type: String, - required: false, - default: 'edited-text', - }, + editedAt: { + type: String, + required: true, }, - }; + editedBy: { + type: Object, + required: false, + default: () => ({}), + }, + className: { + type: String, + required: false, + default: 'edited-text', + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 1a13fdbeb7c..c59a2e7a406 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,128 +1,136 @@ <script> - import { mapGetters, mapActions } from 'vuex'; - import eventHub from '../event_hub'; - import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; - import markdownField from '../../vue_shared/components/markdown/field.vue'; - import issuableStateMixin from '../mixins/issuable_state'; - import resolvable from '../mixins/resolvable'; +import { mapGetters, mapActions } from 'vuex'; +import eventHub from '../event_hub'; +import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; +import markdownField from '../../vue_shared/components/markdown/field.vue'; +import issuableStateMixin from '../mixins/issuable_state'; +import resolvable from '../mixins/resolvable'; - export default { - name: 'IssueNoteForm', - components: { - issueWarning, - markdownField, +export default { + name: 'IssueNoteForm', + components: { + issueWarning, + markdownField, + }, + mixins: [issuableStateMixin, resolvable], + props: { + noteBody: { + type: String, + required: false, + default: '', }, - mixins: [ - issuableStateMixin, - resolvable, - ], - props: { - noteBody: { - type: String, - required: false, - default: '', - }, - noteId: { - type: Number, - required: false, - default: 0, - }, - saveButtonTitle: { - type: String, - required: false, - default: 'Save comment', - }, - note: { - type: Object, - required: false, - default: () => ({}), - }, - isEditing: { - type: Boolean, - required: true, - }, + noteId: { + type: Number, + required: false, + default: 0, }, - data() { - return { - updatedNoteBody: this.noteBody, - conflictWhileEditing: false, - isSubmitting: false, - isResolving: false, - resolveAsThread: true, - }; + saveButtonTitle: { + type: String, + required: false, + default: 'Save comment', }, - computed: { - ...mapGetters([ - 'getDiscussionLastNote', - 'getNoteableData', - 'getNoteableDataByProp', - 'getNotesDataByProp', - 'getUserDataByProp', - ]), - noteHash() { - return `#note_${this.noteId}`; - }, - markdownPreviewPath() { - return this.getNoteableDataByProp('preview_note_path'); - }, - markdownDocsPath() { - return this.getNotesDataByProp('markdownDocsPath'); - }, - quickActionsDocsPath() { - return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined; - }, - currentUserId() { - return this.getUserDataByProp('id'); - }, - isDisabled() { - return !this.updatedNoteBody.length || this.isSubmitting; - }, + note: { + type: Object, + required: false, + default: () => ({}), }, - watch: { - noteBody() { - if (this.updatedNoteBody === this.noteBody) { - this.updatedNoteBody = this.noteBody; - } else { - this.conflictWhileEditing = true; - } - }, + isEditing: { + type: Boolean, + required: true, + }, + }, + data() { + return { + updatedNoteBody: this.noteBody, + conflictWhileEditing: false, + isSubmitting: false, + isResolving: false, + resolveAsThread: true, + }; + }, + computed: { + ...mapGetters([ + 'getDiscussionLastNote', + 'getNoteableData', + 'getNoteableDataByProp', + 'getNotesDataByProp', + 'getUserDataByProp', + ]), + noteHash() { + return `#note_${this.noteId}`; + }, + markdownPreviewPath() { + return this.getNoteableDataByProp('preview_note_path'); + }, + markdownDocsPath() { + return this.getNotesDataByProp('markdownDocsPath'); + }, + quickActionsDocsPath() { + return !this.isEditing + ? this.getNotesDataByProp('quickActionsDocsPath') + : undefined; }, - mounted() { - this.$refs.textarea.focus(); + currentUserId() { + return this.getUserDataByProp('id'); }, - methods: { - ...mapActions([ - 'toggleResolveNote', - ]), - handleUpdate(shouldResolve) { - const beforeSubmitDiscussionState = this.discussionResolved; - this.isSubmitting = true; + isDisabled() { + return !this.updatedNoteBody.length || this.isSubmitting; + }, + }, + watch: { + noteBody() { + if (this.updatedNoteBody === this.noteBody) { + this.updatedNoteBody = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, + }, + mounted() { + this.$refs.textarea.focus(); + }, + methods: { + ...mapActions(['toggleResolveNote']), + handleUpdate(shouldResolve) { + const beforeSubmitDiscussionState = this.discussionResolved; + this.isSubmitting = true; - this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { + this.$emit( + 'handleFormUpdate', + this.updatedNoteBody, + this.$refs.editNoteForm, + () => { this.isSubmitting = false; if (shouldResolve) { this.resolveHandler(beforeSubmitDiscussionState); } - }); - }, - editMyLastNote() { - if (this.updatedNoteBody === '') { - const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody); + }, + ); + }, + editMyLastNote() { + if (this.updatedNoteBody === '') { + const lastNoteInDiscussion = this.getDiscussionLastNote( + this.updatedNoteBody, + ); - if (lastNoteInDiscussion) { - eventHub.$emit('enterEditMode', { - noteId: lastNoteInDiscussion.id, - }); - } + if (lastNoteInDiscussion) { + eventHub.$emit('enterEditMode', { + noteId: lastNoteInDiscussion.id, + }); } - }, - cancelHandler(shouldConfirm = false) { - // Sends information about confirm message and if the textarea has changed - this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody); - }, + } + }, + cancelHandler(shouldConfirm = false) { + // Sends information about confirm message and if the textarea has changed + this.$emit( + 'cancelFormEdition', + shouldConfirm, + this.noteBody !== this.updatedNoteBody, + ); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 4743d95b951..c3d1ef1fcc6 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -1,65 +1,63 @@ <script> - import { mapActions } from 'vuex'; - import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; +import { mapActions } from 'vuex'; +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; - export default { - components: { - timeAgoTooltip, +export default { + components: { + timeAgoTooltip, + }, + props: { + author: { + type: Object, + required: true, }, - props: { - author: { - type: Object, - required: true, - }, - createdAt: { - type: String, - required: true, - }, - actionText: { - type: String, - required: false, - default: '', - }, - actionTextHtml: { - type: String, - required: false, - default: '', - }, - noteId: { - type: Number, - required: true, - }, - includeToggle: { - type: Boolean, - required: false, - default: false, - }, - expanded: { - type: Boolean, - required: false, - default: true, - }, + createdAt: { + type: String, + required: true, }, - computed: { - toggleChevronClass() { - return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; - }, - noteTimestampLink() { - return `#note_${this.noteId}`; - }, + actionText: { + type: String, + required: false, + default: '', }, - methods: { - ...mapActions([ - 'setTargetNoteHash', - ]), - handleToggle() { - this.$emit('toggleHandler'); - }, - updateTargetNoteHash() { - this.setTargetNoteHash(this.noteTimestampLink); - }, + actionTextHtml: { + type: String, + required: false, + default: '', }, - }; + noteId: { + type: Number, + required: true, + }, + includeToggle: { + type: Boolean, + required: false, + default: false, + }, + expanded: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + toggleChevronClass() { + return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; + }, + noteTimestampLink() { + return `#note_${this.noteId}`; + }, + }, + methods: { + ...mapActions(['setTargetNoteHash']), + handleToggle() { + this.$emit('toggleHandler'); + }, + updateTargetNoteHash() { + this.setTargetNoteHash(this.noteTimestampLink); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue index 45d3c2de355..91f7c269757 100644 --- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue @@ -1,19 +1,17 @@ <script> - import { mapGetters } from 'vuex'; +import { mapGetters } from 'vuex'; - export default { - computed: { - ...mapGetters([ - 'getNotesDataByProp', - ]), - registerLink() { - return this.getNotesDataByProp('registerPath'); - }, - signInLink() { - return this.getNotesDataByProp('newSessionPath'); - }, +export default { + computed: { + ...mapGetters(['getNotesDataByProp']), + registerLink() { + return this.getNotesDataByProp('registerPath'); }, - }; + signInLink() { + return this.getNotesDataByProp('newSessionPath'); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 76bb53eaf2f..cf579c5d4dc 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,210 +1,210 @@ <script> - import { mapActions, mapGetters } from 'vuex'; - import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; - import nextDiscussionsSvg from 'icons/_next_discussion.svg'; - import Flash from '../../flash'; - import { SYSTEM_NOTE } from '../constants'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import noteableNote from './noteable_note.vue'; - import noteHeader from './note_header.vue'; - import noteSignedOutWidget from './note_signed_out_widget.vue'; - import noteEditedText from './note_edited_text.vue'; - import noteForm from './note_form.vue'; - import diffWithNote from './diff_with_note.vue'; - import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; - import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; - import autosave from '../mixins/autosave'; - import noteable from '../mixins/noteable'; - import resolvable from '../mixins/resolvable'; - import tooltip from '../../vue_shared/directives/tooltip'; - import { scrollToElement } from '../../lib/utils/common_utils'; +import { mapActions, mapGetters } from 'vuex'; +import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; +import nextDiscussionsSvg from 'icons/_next_discussion.svg'; +import Flash from '../../flash'; +import { SYSTEM_NOTE } from '../constants'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import noteableNote from './noteable_note.vue'; +import noteHeader from './note_header.vue'; +import noteSignedOutWidget from './note_signed_out_widget.vue'; +import noteEditedText from './note_edited_text.vue'; +import noteForm from './note_form.vue'; +import diffWithNote from './diff_with_note.vue'; +import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; +import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; +import autosave from '../mixins/autosave'; +import noteable from '../mixins/noteable'; +import resolvable from '../mixins/resolvable'; +import tooltip from '../../vue_shared/directives/tooltip'; +import { scrollToElement } from '../../lib/utils/common_utils'; - export default { - components: { - noteableNote, - diffWithNote, - userAvatarLink, - noteHeader, - noteSignedOutWidget, - noteEditedText, - noteForm, - placeholderNote, - placeholderSystemNote, +export default { + components: { + noteableNote, + diffWithNote, + userAvatarLink, + noteHeader, + noteSignedOutWidget, + noteEditedText, + noteForm, + placeholderNote, + placeholderSystemNote, + }, + directives: { + tooltip, + }, + mixins: [autosave, noteable, resolvable], + props: { + note: { + type: Object, + required: true, }, - directives: { - tooltip, - }, - mixins: [ - autosave, - noteable, - resolvable, - ], - props: { - note: { - type: Object, - required: true, - }, - }, - data() { + }, + data() { + return { + isReplying: false, + isResolving: false, + resolveAsThread: true, + }; + }, + computed: { + ...mapGetters([ + 'getNoteableData', + 'discussionCount', + 'resolvedDiscussionCount', + 'unresolvedDiscussions', + ]), + discussion() { return { - isReplying: false, - isResolving: false, - resolveAsThread: true, + ...this.note.notes[0], + truncatedDiffLines: this.note.truncated_diff_lines, + diffFile: this.note.diff_file, + diffDiscussion: this.note.diff_discussion, + imageDiffHtml: this.note.image_diff_html, }; }, - computed: { - ...mapGetters([ - 'getNoteableData', - 'discussionCount', - 'resolvedDiscussionCount', - 'unresolvedDiscussions', - ]), - discussion() { - return { - ...this.note.notes[0], - truncatedDiffLines: this.note.truncated_diff_lines, - diffFile: this.note.diff_file, - diffDiscussion: this.note.diff_discussion, - imageDiffHtml: this.note.image_diff_html, - }; - }, - author() { - return this.discussion.author; - }, - canReply() { - return this.getNoteableData.current_user.can_create_note; - }, - newNotePath() { - return this.getNoteableData.create_note_path; - }, - lastUpdatedBy() { - const { notes } = this.note; + author() { + return this.discussion.author; + }, + canReply() { + return this.getNoteableData.current_user.can_create_note; + }, + newNotePath() { + return this.getNoteableData.create_note_path; + }, + lastUpdatedBy() { + const { notes } = this.note; - if (notes.length > 1) { - return notes[notes.length - 1].author; - } + if (notes.length > 1) { + return notes[notes.length - 1].author; + } - return null; - }, - lastUpdatedAt() { - const { notes } = this.note; + return null; + }, + lastUpdatedAt() { + const { notes } = this.note; - if (notes.length > 1) { - return notes[notes.length - 1].created_at; - } + if (notes.length > 1) { + return notes[notes.length - 1].created_at; + } - return null; - }, - hasUnresolvedDiscussion() { - return this.unresolvedDiscussions.length > 0; - }, - wrapperComponent() { - return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div'; - }, - wrapperClass() { - return this.isDiffDiscussion ? '' : 'panel panel-default'; - }, + return null; + }, + hasUnresolvedDiscussion() { + return this.unresolvedDiscussions.length > 0; + }, + wrapperComponent() { + return this.discussion.diffDiscussion && this.discussion.diffFile + ? diffWithNote + : 'div'; }, - mounted() { - if (this.isReplying) { + wrapperClass() { + return this.isDiffDiscussion ? '' : 'panel panel-default'; + }, + }, + mounted() { + if (this.isReplying) { + this.initAutoSave(this.discussion.noteable_type); + } + }, + updated() { + if (this.isReplying) { + if (!this.autosave) { this.initAutoSave(this.discussion.noteable_type); + } else { + this.setAutoSave(); } - }, - updated() { - if (this.isReplying) { - if (!this.autosave) { - this.initAutoSave(this.discussion.noteable_type); - } else { - this.setAutoSave(); + } + }, + created() { + this.resolveDiscussionsSvg = resolveDiscussionsSvg; + this.nextDiscussionsSvg = nextDiscussionsSvg; + }, + methods: { + ...mapActions([ + 'saveNote', + 'toggleDiscussion', + 'removePlaceholderNotes', + 'toggleResolveNote', + ]), + componentName(note) { + if (note.isPlaceholderNote) { + if (note.placeholderType === SYSTEM_NOTE) { + return placeholderSystemNote; } + return placeholderNote; } + + return noteableNote; }, - created() { - this.resolveDiscussionsSvg = resolveDiscussionsSvg; - this.nextDiscussionsSvg = nextDiscussionsSvg; + componentData(note) { + return note.isPlaceholderNote ? this.note.notes[0] : note; }, - methods: { - ...mapActions([ - 'saveNote', - 'toggleDiscussion', - 'removePlaceholderNotes', - 'toggleResolveNote', - ]), - componentName(note) { - if (note.isPlaceholderNote) { - if (note.placeholderType === SYSTEM_NOTE) { - return placeholderSystemNote; - } - return placeholderNote; - } + toggleDiscussionHandler() { + this.toggleDiscussion({ discussionId: this.note.id }); + }, + showReplyForm() { + this.isReplying = true; + }, + cancelReplyForm(shouldConfirm) { + if (shouldConfirm && this.$refs.noteForm.isDirty) { + const msg = 'Are you sure you want to cancel creating this comment?'; - return noteableNote; - }, - componentData(note) { - return note.isPlaceholderNote ? this.note.notes[0] : note; - }, - toggleDiscussionHandler() { - this.toggleDiscussion({ discussionId: this.note.id }); - }, - showReplyForm() { - this.isReplying = true; - }, - cancelReplyForm(shouldConfirm) { - if (shouldConfirm && this.$refs.noteForm.isDirty) { - // eslint-disable-next-line no-alert - if (!confirm('Are you sure you want to cancel creating this comment?')) { - return; - } + // eslint-disable-next-line no-alert + if (!confirm(msg)) { + return; } + } - this.resetAutoSave(); - this.isReplying = false; - }, - saveReply(noteText, form, callback) { - const replyData = { - endpoint: this.newNotePath, - flashContainer: this.$el, - data: { - in_reply_to_discussion_id: this.note.reply_id, - target_type: this.noteableType, - target_id: this.discussion.noteable_id, - note: { note: noteText }, - }, - }; - this.isReplying = false; + this.resetAutoSave(); + this.isReplying = false; + }, + saveReply(noteText, form, callback) { + const replyData = { + endpoint: this.newNotePath, + flashContainer: this.$el, + data: { + in_reply_to_discussion_id: this.note.reply_id, + target_type: this.noteableType, + target_id: this.discussion.noteable_id, + note: { note: noteText }, + }, + }; + this.isReplying = false; - this.saveNote(replyData) - .then(() => { - this.resetAutoSave(); - callback(); - }) - .catch((err) => { - this.removePlaceholderNotes(); - this.isReplying = true; - this.$nextTick(() => { - const msg = `Your comment could not be submitted! + this.saveNote(replyData) + .then(() => { + this.resetAutoSave(); + callback(); + }) + .catch(err => { + this.removePlaceholderNotes(); + this.isReplying = true; + this.$nextTick(() => { + const msg = `Your comment could not be submitted! Please check your network connection and try again.`; - Flash(msg, 'alert', this.$el); - this.$refs.noteForm.note = noteText; - callback(err); - }); + Flash(msg, 'alert', this.$el); + this.$refs.noteForm.note = noteText; + callback(err); }); - }, - jumpToDiscussion() { - const unresolvedIds = this.unresolvedDiscussions.map(d => d.id); - const index = unresolvedIds.indexOf(this.note.id); + }); + }, + jumpToDiscussion() { + const unresolvedIds = this.unresolvedDiscussions.map(d => d.id); + const index = unresolvedIds.indexOf(this.note.id); - if (index >= 0 && index !== unresolvedIds.length) { - const nextId = unresolvedIds[index + 1]; - const el = document.querySelector(`[data-discussion-id="${nextId}"]`); + if (index >= 0 && index !== unresolvedIds.length) { + const nextId = unresolvedIds[index + 1]; + const el = document.querySelector(`[data-discussion-id="${nextId}"]`); - if (el) { - scrollToElement(el); - } + if (el) { + scrollToElement(el); } - }, + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 6d5501d7d98..3554027d2b4 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -1,152 +1,152 @@ <script> - import $ from 'jquery'; - import { mapGetters, mapActions } from 'vuex'; - import { escape } from 'underscore'; - import Flash from '../../flash'; - import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; - import noteHeader from './note_header.vue'; - import noteActions from './note_actions.vue'; - import noteBody from './note_body.vue'; - import eventHub from '../event_hub'; - import noteable from '../mixins/noteable'; - import resolvable from '../mixins/resolvable'; +import $ from 'jquery'; +import { mapGetters, mapActions } from 'vuex'; +import { escape } from 'underscore'; +import Flash from '../../flash'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import noteHeader from './note_header.vue'; +import noteActions from './note_actions.vue'; +import noteBody from './note_body.vue'; +import eventHub from '../event_hub'; +import noteable from '../mixins/noteable'; +import resolvable from '../mixins/resolvable'; - export default { - components: { - userAvatarLink, - noteHeader, - noteActions, - noteBody, +export default { + components: { + userAvatarLink, + noteHeader, + noteActions, + noteBody, + }, + mixins: [noteable, resolvable], + props: { + note: { + type: Object, + required: true, }, - mixins: [ - noteable, - resolvable, - ], - props: { - note: { - type: Object, - required: true, - }, + }, + data() { + return { + isEditing: false, + isDeleting: false, + isRequesting: false, + isResolving: false, + }; + }, + computed: { + ...mapGetters(['targetNoteHash', 'getUserData']), + author() { + return this.note.author; }, - data() { + classNameBindings() { return { - isEditing: false, - isDeleting: false, - isRequesting: false, - isResolving: false, + 'is-editing': this.isEditing && !this.isRequesting, + 'is-requesting being-posted': this.isRequesting, + 'disabled-content': this.isDeleting, + target: this.targetNoteHash === this.noteAnchorId, }; }, - computed: { - ...mapGetters([ - 'targetNoteHash', - 'getUserData', - ]), - author() { - return this.note.author; - }, - classNameBindings() { - return { - 'is-editing': this.isEditing && !this.isRequesting, - 'is-requesting being-posted': this.isRequesting, - 'disabled-content': this.isDeleting, - target: this.targetNoteHash === this.noteAnchorId, - }; - }, - canReportAsAbuse() { - return this.note.report_abuse_path && this.author.id !== this.getUserData.id; - }, - noteAnchorId() { - return `note_${this.note.id}`; - }, + canReportAsAbuse() { + return ( + this.note.report_abuse_path && this.author.id !== this.getUserData.id + ); }, - - created() { - eventHub.$on('enterEditMode', ({ noteId }) => { - if (noteId === this.note.id) { - this.isEditing = true; - this.scrollToNoteIfNeeded($(this.$el)); - } - }); + noteAnchorId() { + return `note_${this.note.id}`; }, + }, - methods: { - ...mapActions([ - 'deleteNote', - 'updateNote', - 'toggleResolveNote', - 'scrollToNoteIfNeeded', - ]), - editHandler() { + created() { + eventHub.$on('enterEditMode', ({ noteId }) => { + if (noteId === this.note.id) { this.isEditing = true; - }, - deleteHandler() { - // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to delete this comment?')) { - this.isDeleting = true; + this.scrollToNoteIfNeeded($(this.$el)); + } + }); + }, - this.deleteNote(this.note) - .then(() => { - this.isDeleting = false; - }) - .catch(() => { - Flash('Something went wrong while deleting your note. Please try again.'); - this.isDeleting = false; - }); - } - }, - formUpdateHandler(noteText, parentElement, callback) { - const data = { - endpoint: this.note.path, - note: { - target_type: this.noteableType, - target_id: this.note.noteable_id, - note: { note: noteText }, - }, - }; - this.isRequesting = true; - this.oldContent = this.note.note_html; - this.note.note_html = escape(noteText); + methods: { + ...mapActions([ + 'deleteNote', + 'updateNote', + 'toggleResolveNote', + 'scrollToNoteIfNeeded', + ]), + editHandler() { + this.isEditing = true; + }, + deleteHandler() { + // eslint-disable-next-line no-alert + if (confirm('Are you sure you want to delete this comment?')) { + this.isDeleting = true; - this.updateNote(data) + this.deleteNote(this.note) .then(() => { - this.isEditing = false; - this.isRequesting = false; - this.oldContent = null; - $(this.$refs.noteBody.$el).renderGFM(); - this.$refs.noteBody.resetAutoSave(); - callback(); + this.isDeleting = false; }) .catch(() => { - this.isRequesting = false; - this.isEditing = true; - this.$nextTick(() => { - const msg = 'Something went wrong while editing your comment. Please try again.'; - Flash(msg, 'alert', this.$el); - this.recoverNoteContent(noteText); - callback(); - }); + Flash( + 'Something went wrong while deleting your note. Please try again.', + ); + this.isDeleting = false; }); - }, - formCancelHandler(shouldConfirm, isDirty) { - if (shouldConfirm && isDirty) { - // eslint-disable-next-line no-alert - if (!confirm('Are you sure you want to cancel editing this comment?')) return; - } - this.$refs.noteBody.resetAutoSave(); - if (this.oldContent) { - this.note.note_html = this.oldContent; + } + }, + formUpdateHandler(noteText, parentElement, callback) { + const data = { + endpoint: this.note.path, + note: { + target_type: this.noteableType, + target_id: this.note.noteable_id, + note: { note: noteText }, + }, + }; + this.isRequesting = true; + this.oldContent = this.note.note_html; + this.note.note_html = escape(noteText); + + this.updateNote(data) + .then(() => { + this.isEditing = false; + this.isRequesting = false; this.oldContent = null; - } - this.isEditing = false; - }, - recoverNoteContent(noteText) { - // we need to do this to prevent noteForm inconsistent content warning - // this is something we intentionally do so we need to recover the content - this.note.note = noteText; - this.$refs.noteBody.$refs.noteForm.note.note = noteText; - }, + $(this.$refs.noteBody.$el).renderGFM(); + this.$refs.noteBody.resetAutoSave(); + callback(); + }) + .catch(() => { + this.isRequesting = false; + this.isEditing = true; + this.$nextTick(() => { + const msg = + 'Something went wrong while editing your comment. Please try again.'; + Flash(msg, 'alert', this.$el); + this.recoverNoteContent(noteText); + callback(); + }); + }); + }, + formCancelHandler(shouldConfirm, isDirty) { + if (shouldConfirm && isDirty) { + // eslint-disable-next-line no-alert + if (!confirm('Are you sure you want to cancel editing this comment?')) + return; + } + this.$refs.noteBody.resetAutoSave(); + if (this.oldContent) { + this.note.note_html = this.oldContent; + this.oldContent = null; + } + this.isEditing = false; + }, + recoverNoteContent(noteText) { + // we need to do this to prevent noteForm inconsistent content warning + // this is something we intentionally do so we need to recover the content + this.note.note = noteText; + this.$refs.noteBody.$refs.noteForm.note.note = noteText; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index c97472c879c..a90c6d6381d 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,160 +1,162 @@ <script> - import $ from 'jquery'; - import { mapGetters, mapActions } from 'vuex'; - import { getLocationHash } from '../../lib/utils/url_utility'; - import Flash from '../../flash'; - import store from '../stores/'; - import * as constants from '../constants'; - import noteableNote from './noteable_note.vue'; - import noteableDiscussion from './noteable_discussion.vue'; - import systemNote from '../../vue_shared/components/notes/system_note.vue'; - import commentForm from './comment_form.vue'; - import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; - import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; +import $ from 'jquery'; +import { mapGetters, mapActions } from 'vuex'; +import { getLocationHash } from '../../lib/utils/url_utility'; +import Flash from '../../flash'; +import store from '../stores/'; +import * as constants from '../constants'; +import noteableNote from './noteable_note.vue'; +import noteableDiscussion from './noteable_discussion.vue'; +import systemNote from '../../vue_shared/components/notes/system_note.vue'; +import commentForm from './comment_form.vue'; +import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; +import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; - export default { - name: 'NotesApp', - components: { - noteableNote, - noteableDiscussion, - systemNote, - commentForm, - loadingIcon, - placeholderNote, - placeholderSystemNote, +export default { + name: 'NotesApp', + components: { + noteableNote, + noteableDiscussion, + systemNote, + commentForm, + loadingIcon, + placeholderNote, + placeholderSystemNote, + }, + props: { + noteableData: { + type: Object, + required: true, }, - props: { - noteableData: { - type: Object, - required: true, - }, - notesData: { - type: Object, - required: true, - }, - userData: { - type: Object, - required: false, - default: () => ({}), - }, + notesData: { + type: Object, + required: true, }, - store, - data() { - return { - isLoading: true, - }; + userData: { + type: Object, + required: false, + default: () => ({}), }, - computed: { - ...mapGetters([ - 'notes', - 'getNotesDataByProp', - 'discussionCount', - ]), - noteableType() { - // FIXME -- @fatihacet Get this from JSON data. - const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; + }, + store, + data() { + return { + isLoading: true, + }; + }, + computed: { + ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']), + noteableType() { + // FIXME -- @fatihacet Get this from JSON data. + const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; - return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE; - }, - allNotes() { - if (this.isLoading) { - const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0; - - return new Array(totalNotes).fill({ - isSkeletonNote: true, - }); - } - return this.notes; - }, - }, - created() { - this.setNotesData(this.notesData); - this.setNoteableData(this.noteableData); - this.setUserData(this.userData); + return this.noteableData.merge_params + ? MERGE_REQUEST_NOTEABLE_TYPE + : ISSUE_NOTEABLE_TYPE; }, - mounted() { - this.fetchNotes(); + allNotes() { + if (this.isLoading) { + const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0; - const parentElement = this.$el.parentElement; - - if (parentElement && - parentElement.classList.contains('js-vue-notes-event')) { - parentElement.addEventListener('toggleAward', (event) => { - const { awardName, noteId } = event.detail; - this.actionToggleAward({ awardName, noteId }); + return new Array(totalNotes).fill({ + isSkeletonNote: true, }); } - document.addEventListener('refreshVueNotes', this.fetchNotes); - }, - beforeDestroy() { - document.removeEventListener('refreshVueNotes', this.fetchNotes); + return this.notes; }, - methods: { - ...mapActions({ - actionFetchNotes: 'fetchNotes', - poll: 'poll', - actionToggleAward: 'toggleAward', - scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', - setNotesData: 'setNotesData', - setNoteableData: 'setNoteableData', - setUserData: 'setUserData', - setLastFetchedAt: 'setLastFetchedAt', - setTargetNoteHash: 'setTargetNoteHash', - }), - getComponentName(note) { - if (note.isSkeletonNote) { - return skeletonLoadingContainer; - } - if (note.isPlaceholderNote) { - if (note.placeholderType === constants.SYSTEM_NOTE) { - return placeholderSystemNote; - } - return placeholderNote; - } else if (note.individual_note) { - return note.notes[0].system ? systemNote : noteableNote; - } + }, + created() { + this.setNotesData(this.notesData); + this.setNoteableData(this.noteableData); + this.setUserData(this.userData); + }, + mounted() { + this.fetchNotes(); + + const parentElement = this.$el.parentElement; - return noteableDiscussion; - }, - getComponentData(note) { - return note.individual_note ? note.notes[0] : note; - }, - fetchNotes() { - return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) - .then(() => this.initPolling()) - .then(() => { - this.isLoading = false; - }) - .then(() => this.$nextTick()) - .then(() => this.checkLocationHash()) - .catch(() => { - this.isLoading = false; - Flash('Something went wrong while fetching comments. Please try again.'); - }); - }, - initPolling() { - if (this.isPollingInitialized) { - return; + if ( + parentElement && + parentElement.classList.contains('js-vue-notes-event') + ) { + parentElement.addEventListener('toggleAward', event => { + const { awardName, noteId } = event.detail; + this.actionToggleAward({ awardName, noteId }); + }); + } + document.addEventListener('refreshVueNotes', this.fetchNotes); + }, + beforeDestroy() { + document.removeEventListener('refreshVueNotes', this.fetchNotes); + }, + methods: { + ...mapActions({ + actionFetchNotes: 'fetchNotes', + poll: 'poll', + actionToggleAward: 'toggleAward', + scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', + setNotesData: 'setNotesData', + setNoteableData: 'setNoteableData', + setUserData: 'setUserData', + setLastFetchedAt: 'setLastFetchedAt', + setTargetNoteHash: 'setTargetNoteHash', + }), + getComponentName(note) { + if (note.isSkeletonNote) { + return skeletonLoadingContainer; + } + if (note.isPlaceholderNote) { + if (note.placeholderType === constants.SYSTEM_NOTE) { + return placeholderSystemNote; } + return placeholderNote; + } else if (note.individual_note) { + return note.notes[0].system ? systemNote : noteableNote; + } - this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); + return noteableDiscussion; + }, + getComponentData(note) { + return note.individual_note ? note.notes[0] : note; + }, + fetchNotes() { + return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) + .then(() => this.initPolling()) + .then(() => { + this.isLoading = false; + }) + .then(() => this.$nextTick()) + .then(() => this.checkLocationHash()) + .catch(() => { + this.isLoading = false; + Flash( + 'Something went wrong while fetching comments. Please try again.', + ); + }); + }, + initPolling() { + if (this.isPollingInitialized) { + return; + } - this.poll(); - this.isPollingInitialized = true; - }, - checkLocationHash() { - const hash = getLocationHash(); - const element = document.getElementById(hash); + this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); - if (hash && element) { - this.setTargetNoteHash(hash); - this.scrollToNoteIfNeeded($(element)); - } - }, + this.poll(); + this.isPollingInitialized = true; + }, + checkLocationHash() { + const hash = getLocationHash(); + const element = document.getElementById(hash); + + if (hash && element) { + this.setTargetNoteHash(hash); + this.scrollToNoteIfNeeded($(element)); + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 545bf2c99a7..f90775d0157 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,35 +1,43 @@ import Vue from 'vue'; import notesApp from './components/notes_app.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#js-vue-notes', - components: { - notesApp, - }, - data() { - const notesDataset = document.getElementById('js-vue-notes').dataset; - const parsedUserData = JSON.parse(notesDataset.currentUserData); - const currentUserData = parsedUserData ? { - id: parsedUserData.id, - name: parsedUserData.name, - username: parsedUserData.username, - avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, - path: parsedUserData.path, - } : {}; +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#js-vue-notes', + components: { + notesApp, + }, + data() { + const notesDataset = document.getElementById('js-vue-notes').dataset; + const parsedUserData = JSON.parse(notesDataset.currentUserData); + let currentUserData = {}; + + if (parsedUserData) { + currentUserData = { + id: parsedUserData.id, + name: parsedUserData.name, + username: parsedUserData.username, + avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, + path: parsedUserData.path, + }; + } - return { - noteableData: JSON.parse(notesDataset.noteableData), - currentUserData, - notesData: JSON.parse(notesDataset.notesData), - }; - }, - render(createElement) { - return createElement('notes-app', { - props: { - noteableData: this.noteableData, - notesData: this.notesData, - userData: this.currentUserData, + return { + noteableData: JSON.parse(notesDataset.noteableData), + currentUserData, + notesData: JSON.parse(notesDataset.notesData), + }; + }, + render(createElement) { + return createElement('notes-app', { + props: { + noteableData: this.noteableData, + notesData: this.notesData, + userData: this.currentUserData, + }, + }); }, - }); - }, -})); + }), +); diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js index 837a4029346..3dff715905f 100644 --- a/app/assets/javascripts/notes/mixins/autosave.js +++ b/app/assets/javascripts/notes/mixins/autosave.js @@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; export default { methods: { 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() { this.autosave.reset(); diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js index ab1ae115e52..f79049b85f6 100644 --- a/app/assets/javascripts/notes/mixins/resolvable.js +++ b/app/assets/javascripts/notes/mixins/resolvable.js @@ -12,7 +12,8 @@ export default { discussionResolved() { 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); } @@ -26,7 +27,9 @@ export default { return __('Comment and resolve discussion'); } - return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); + return this.discussionResolved + ? __('Unresolve discussion') + : __('Resolve discussion'); }, }, methods: { @@ -42,7 +45,9 @@ export default { }) .catch(() => { 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); }); }, diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index b4c19a9ec22..7c623aac6ed 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -22,7 +22,9 @@ export default { }, toggleResolveNote(endpoint, isResolved) { 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); }, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index ebbacb576d6..244a6980b5a 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -12,97 +12,115 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; let eTagPoll; -export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); -export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); -export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); -export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); -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) - .then(res => res.json()) - .then((res) => { - commit(types.SET_INITIAL_NOTES, res); - }); +export const setNotesData = ({ commit }, data) => + commit(types.SET_NOTES_DATA, data); +export const setNoteableData = ({ commit }, data) => + commit(types.SET_NOTEABLE_DATA, data); +export const setUserData = ({ commit }, data) => + commit(types.SET_USER_DATA, data); +export const setLastFetchedAt = ({ commit }, data) => + commit(types.SET_LAST_FETCHED_AT, data); +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) + .then(res => res.json()) + .then(res => { + commit(types.SET_INITIAL_NOTES, res); + }); -export const deleteNote = ({ commit }, note) => service - .deleteNote(note.path) - .then(() => { +export const deleteNote = ({ commit }, note) => + service.deleteNote(note.path).then(() => { commit(types.DELETE_NOTE, note); }); -export const updateNote = ({ commit }, { endpoint, note }) => service - .updateNote(endpoint, note) - .then(res => res.json()) - .then((res) => { - commit(types.UPDATE_NOTE, res); - }); +export const updateNote = ({ commit }, { endpoint, note }) => + service + .updateNote(endpoint, note) + .then(res => res.json()) + .then(res => { + commit(types.UPDATE_NOTE, res); + }); -export const replyToDiscussion = ({ commit }, { endpoint, data }) => service - .replyToDiscussion(endpoint, data) - .then(res => res.json()) - .then((res) => { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); +export const replyToDiscussion = ({ commit }, { endpoint, data }) => + service + .replyToDiscussion(endpoint, data) + .then(res => res.json()) + .then(res => { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); - return res; - }); + return res; + }); -export const createNewNote = ({ commit }, { endpoint, data }) => service - .createNewNote(endpoint, data) - .then(res => res.json()) - .then((res) => { - if (!res.errors) { - commit(types.ADD_NEW_NOTE, res); - } - return res; - }); +export const createNewNote = ({ commit }, { endpoint, data }) => + service + .createNewNote(endpoint, data) + .then(res => res.json()) + .then(res => { + if (!res.errors) { + commit(types.ADD_NEW_NOTE, res); + } + return res; + }); export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); -export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service - .toggleResolveNote(endpoint, isResolved) - .then(res => res.json()) - .then((res) => { - const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; +export const toggleResolveNote = ( + { commit }, + { endpoint, isResolved, discussion }, +) => + service + .toggleResolveNote(endpoint, isResolved) + .then(res => res.json()) + .then(res => { + const mutationType = discussion + ? types.UPDATE_DISCUSSION + : types.UPDATE_NOTE; - commit(mutationType, res); - }); + commit(mutationType, res); + }); export const closeIssue = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); return service - .toggleIssueState(state.notesData.closePath) - .then(res => res.json()) - .then((data) => { - commit(types.CLOSE_ISSUE); - dispatch('emitStateChangedEvent', data); - dispatch('toggleStateButtonLoading', false); - }); + .toggleIssueState(state.notesData.closePath) + .then(res => res.json()) + .then(data => { + commit(types.CLOSE_ISSUE); + dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); + }); }; export const reopenIssue = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); return service - .toggleIssueState(state.notesData.reopenPath) - .then(res => res.json()) - .then((data) => { - commit(types.REOPEN_ISSUE); - dispatch('emitStateChangedEvent', data); - dispatch('toggleStateButtonLoading', false); - }); + .toggleIssueState(state.notesData.reopenPath) + .then(res => res.json()) + .then(data => { + commit(types.REOPEN_ISSUE); + dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); + }); }; export const toggleStateButtonLoading = ({ commit }, value) => commit(types.TOGGLE_STATE_BUTTON_LOADING, value); export const emitStateChangedEvent = ({ commit, getters }, data) => { - const event = new CustomEvent('issuable_vue_app:change', { detail: { - data, - isClosed: getters.openState === constants.CLOSED, - } }); + const event = new CustomEvent('issuable_vue_app:change', { + detail: { + data, + isClosed: getters.openState === constants.CLOSED, + }, + }); document.dispatchEvent(event); }; @@ -144,59 +162,70 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }); } - return dispatch(methodToDispatch, noteData) - .then((res) => { - const { errors } = res; - const commandsChanges = res.commands_changes; + return dispatch(methodToDispatch, noteData).then(res => { + const { errors } = res; + const commandsChanges = res.commands_changes; - if (hasQuickActions && errors && Object.keys(errors).length) { - eTagPoll.makeRequest(); - - $('.js-gfm-input').trigger('clear-commands-cache.atwho'); - Flash('Commands applied', 'notice', noteData.flashContainer); - } + if (hasQuickActions && errors && Object.keys(errors).length) { + eTagPoll.makeRequest(); - if (commandsChanges) { - if (commandsChanges.emoji_award) { - const votesBlock = $('.js-awards-block').eq(0); - - loadAwardsHandler() - .then((awardsHandler) => { - awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); - awardsHandler.scrollToAwards(); - }) - .catch(() => { - Flash( - 'Something went wrong while adding your award. Please try again.', - 'alert', - noteData.flashContainer, - ); - }); - } + $('.js-gfm-input').trigger('clear-commands-cache.atwho'); + Flash('Commands applied', 'notice', noteData.flashContainer); + } - if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { - sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); - } + if (commandsChanges) { + if (commandsChanges.emoji_award) { + const votesBlock = $('.js-awards-block').eq(0); + + loadAwardsHandler() + .then(awardsHandler => { + awardsHandler.addAwardToEmojiBar( + votesBlock, + commandsChanges.emoji_award, + ); + awardsHandler.scrollToAwards(); + }) + .catch(() => { + Flash( + 'Something went wrong while adding your award. Please try again.', + 'alert', + noteData.flashContainer, + ); + }); } - if (errors && errors.commands_only) { - Flash(errors.commands_only, 'notice', noteData.flashContainer); + if ( + commandsChanges.spend_time != null || + commandsChanges.time_estimate != null + ) { + sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); } - commit(types.REMOVE_PLACEHOLDER_NOTES); + } - return res; - }); + if (errors && errors.commands_only) { + Flash(errors.commands_only, 'notice', noteData.flashContainer); + } + commit(types.REMOVE_PLACEHOLDER_NOTES); + + return res; + }); }; const pollSuccessCallBack = (resp, commit, state, getters) => { if (resp.notes && resp.notes.length) { const { notesById } = getters; - resp.notes.forEach((note) => { + resp.notes.forEach(note => { if (notesById[note.id]) { commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { - const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); + } else if ( + note.type === constants.DISCUSSION_NOTE || + note.type === constants.DIFF_NOTE + ) { + const discussion = utils.findNoteObjectById( + state.notes, + note.discussion_id, + ); if (discussion) { commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); @@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => { resource: service, method: 'poll', data: state, - successCallback: resp => resp.json() - .then(data => pollSuccessCallBack(data, commit, state, getters)), - errorCallback: () => Flash('Something went wrong while fetching latest comments.'), + successCallback: resp => + resp + .json() + .then(data => pollSuccessCallBack(data, commit, state, getters)), + errorCallback: () => + Flash('Something went wrong while fetching latest comments.'), }); if (!Visibility.hidden()) { @@ -248,15 +280,22 @@ export const restartPolling = () => { }; 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(data => pollSuccessCallBack(data, commit, state, getters)) .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] }); }; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index e6180101c58..f89591a54d6 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop]; export const openState = state => state.noteableData.state; 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) => { - note.notes.every(n => Object.assign(acc, { [n.id]: n })); - return acc; -}, {}); +export const notesById = state => + state.notes.reduce((acc, note) => { + note.notes.every(n => Object.assign(acc, { [n.id]: n })); + return acc; + }, {}); const reverseNotes = array => array.slice(0).reverse(); -const isLastNote = (note, state) => !note.system && - state.userData && note.author && +const isLastNote = (note, state) => + !note.system && + state.userData && + note.author && note.author.id === state.userData.id; -export const getCurrentUserLastNote = state => _.flatten( - reverseNotes(state.notes) - .map(note => reverseNotes(note.notes)), +export const getCurrentUserLastNote = state => + _.flatten( + reverseNotes(state.notes).map(note => reverseNotes(note.notes)), ).find(el => isLastNote(el, state)); -export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) - .find(el => isLastNote(el, state)); +export const getDiscussionLastNote = state => discussion => + 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); return discussions.length; @@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => { return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]); }; -export const resolvedDiscussionsById = (state) => { +export const resolvedDiscussionsById = state => { const map = {}; - state.notes.forEach((n) => { + state.notes.forEach(n => { if (n.notes) { const resolved = n.notes.every(note => note.resolved && !note.system); diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 9308daa36f1..c8edc06349f 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -7,7 +7,7 @@ export default { [types.ADD_NEW_NOTE](state, note) { const { discussion_id, type } = note; 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) { const noteData = { @@ -63,13 +63,15 @@ export default { const note = notes[i]; 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) { if (children[j].isPlaceholderNote) { 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); } } @@ -89,10 +91,10 @@ export default { [types.SET_INITIAL_NOTES](state, notesData) { const notes = []; - notesData.forEach((note) => { + notesData.forEach(note => { // To support legacy notes, should be very rare case. if (note.individual_note && note.notes.length > 1) { - note.notes.forEach((n) => { + note.notes.forEach(n => { notes.push({ ...note, notes: [n], // override notes array to only have one item to mimick individual_note @@ -103,7 +105,7 @@ export default { notes.push({ ...note, - expanded: (oldNote ? oldNote.expanded : note.expanded), + expanded: oldNote ? oldNote.expanded : note.expanded, }); } }); @@ -128,7 +130,9 @@ export default { notesArr.push({ individual_note: true, isPlaceholderNote: true, - placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE, + placeholderType: data.isSystemNote + ? constants.SYSTEM_NOTE + : constants.NOTE, notes: [ { body: data.noteBody, @@ -141,12 +145,16 @@ export default { const { awardName, note } = data; const { id, name, username } = state.userData; - const hasEmojiAwardedByCurrentUser = note.award_emoji - .filter(emoji => emoji.name === data.awardName && emoji.user.id === id); + const hasEmojiAwardedByCurrentUser = note.award_emoji.filter( + emoji => emoji.name === data.awardName && emoji.user.id === id, + ); if (hasEmojiAwardedByCurrentUser.length) { // 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 { note.award_emoji.push({ name: awardName, diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 275263a2aaa..a0e096ebfaf 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache'; 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'; - 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}`); return commandRegex.test(note); }); @@ -27,4 +29,5 @@ export const getQuickActionText = (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(); |