summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/notes.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/notes.js')
-rw-r--r--app/assets/javascripts/notes.js1744
1 files changed, 0 insertions, 1744 deletions
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
deleted file mode 100644
index ef51587734d..00000000000
--- a/app/assets/javascripts/notes.js
+++ /dev/null
@@ -1,1744 +0,0 @@
-/* eslint-disable no-restricted-properties, babel/camelcase,
-no-unused-expressions, default-case,
-consistent-return, no-alert, no-param-reassign,
-no-shadow, no-useless-escape,
-class-methods-use-this */
-
-/* global ResolveService */
-
-/*
-old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
- */
-
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import Autosize from 'autosize';
-import $ from 'jquery';
-import Cookies from 'js-cookie';
-import { escape, uniqueId } from 'lodash';
-import Vue from 'vue';
-import '~/lib/utils/jquery_at_who';
-import AjaxCache from '~/lib/utils/ajax_cache';
-import syntaxHighlight from '~/syntax_highlight';
-import Autosave from './autosave';
-import loadAwardsHandler from './awards_handler';
-import CommentTypeToggle from './comment_type_toggle';
-import createFlash from './flash';
-import { defaultAutocompleteConfig } from './gfm_auto_complete';
-import GLForm from './gl_form';
-import axios from './lib/utils/axios_utils';
-import {
- isInViewport,
- getPagePath,
- scrollToElement,
- isMetaKey,
- isInMRPage,
-} from './lib/utils/common_utils';
-import { localTimeAgo } from './lib/utils/datetime_utility';
-import { getLocationHash } from './lib/utils/url_utility';
-import { sprintf, s__, __ } from './locale';
-import TaskList from './task_list';
-
-window.autosize = Autosize;
-
-function normalizeNewlines(str) {
- return str.replace(/\r\n/g, '\n');
-}
-
-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) {
- if (!this.instance) {
- this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
- }
- }
-
- static getInstance() {
- return this.instance;
- }
-
- constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) {
- this.updateTargetButtons = this.updateTargetButtons.bind(this);
- this.updateComment = this.updateComment.bind(this);
- this.visibilityChange = this.visibilityChange.bind(this);
- this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
- this.onAddDiffNote = this.onAddDiffNote.bind(this);
- this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this);
- this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
- this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
- this.removeNote = this.removeNote.bind(this);
- this.cancelEdit = this.cancelEdit.bind(this);
- this.updateNote = this.updateNote.bind(this);
- this.addDiscussionNote = this.addDiscussionNote.bind(this);
- this.addNoteError = this.addNoteError.bind(this);
- this.addNote = this.addNote.bind(this);
- this.resetMainTargetForm = this.resetMainTargetForm.bind(this);
- this.refresh = this.refresh.bind(this);
- this.keydownNoteText = this.keydownNoteText.bind(this);
- this.toggleCommitList = this.toggleCommitList.bind(this);
- this.postComment = this.postComment.bind(this);
- this.clearFlashWrapper = this.clearFlash.bind(this);
- this.onHashChange = this.onHashChange.bind(this);
-
- this.notes_url = notes_url;
- this.note_ids = note_ids;
- this.enableGFM = enableGFM;
- // Used to keep track of updated notes while people are editing things
- 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.basePollingInterval = 15000;
- this.maxPollingSteps = 4;
-
- this.$wrapperEl = isInMRPage() ? $(document).find('.diffs') : $(document);
- this.cleanBinding();
- this.addBinding();
- this.setPollingInterval();
- this.setupMainTargetNoteForm(enableGFM);
- this.taskList = new TaskList({
- dataType: 'note',
- fieldName: 'note',
- 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');
- }
-
- const hash = getLocationHash();
- const $anchor = hash && document.getElementById(hash);
-
- if ($anchor) {
- this.loadLazyDiff({ currentTarget: $anchor });
- }
- }
-
- setViewType(view) {
- this.view = Cookies.get('diff_view') || view;
- }
-
- addBinding() {
- // Edit note link
- this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
- this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
- // 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);
- // 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);
- // update the file name when an attachment is selected
- 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);
- // 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);
- // hide diff note form
- 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', '.js-toggle-lazy-diff', this.loadLazyDiff);
- this.$wrapperEl.on(
- 'click',
- '.js-toggle-lazy-diff-retry-button',
- this.onClickRetryLazyLoad.bind(this),
- );
-
- // fetch notes when tab becomes visible
- this.$wrapperEl.on('visibilitychange', this.visibilityChange);
- // when issue status changes, we need to refresh data
- 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,
- );
- // 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`
- $(window).on('hashchange', this.onHashChange);
- }
-
- cleanBinding() {
- this.$wrapperEl.off('click', '.js-note-edit');
- this.$wrapperEl.off('click', '.note-edit-cancel');
- this.$wrapperEl.off('click', '.js-note-delete');
- this.$wrapperEl.off('click', '.js-note-attachment-delete');
- this.$wrapperEl.off('click', '.js-discussion-reply-button');
- this.$wrapperEl.off('click', '.js-add-diff-note-button');
- this.$wrapperEl.off('click', '.js-add-image-diff-note-button');
- // eslint-disable-next-line @gitlab/no-global-event-off
- this.$wrapperEl.off('visibilitychange');
- this.$wrapperEl.off('keyup input', '.js-note-text');
- this.$wrapperEl.off('click', '.js-note-target-reopen');
- this.$wrapperEl.off('click', '.js-note-target-close');
- this.$wrapperEl.off('keydown', '.js-note-text');
- this.$wrapperEl.off('click', '.js-comment-resolve-button');
- this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
- this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
- this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
- this.$wrapperEl.off('ajax:success', '.js-main-target-form');
- this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
- this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
- $(window).off('hashchange', this.onHashChange);
- }
-
- static initCommentTypeToggle(form) {
- 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 closeButton = form.querySelector('.js-note-target-close');
- const reopenButton = form.querySelector('.js-note-target-reopen');
-
- const commentTypeToggle = new CommentTypeToggle({
- dropdownTrigger,
- dropdownList,
- noteTypeInput,
- submitButton,
- closeButton,
- reopenButton,
- });
-
- commentTypeToggle.initDroplab();
- }
-
- keydownNoteText(e) {
- let discussionNoteForm;
- let editNote;
- let myLastNote;
- let myLastNoteEditBtn;
- let newText;
- let originalText;
-
- if (isMetaKey(e)) {
- return;
- }
-
- const $textarea = $(e.target);
- // Edit previous note when UP arrow is hit
- switch (e.which) {
- case 38:
- if ($textarea.val() !== '') {
- return;
- }
- 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]);
- }
- break;
- // Cancel creating diff note or editing any note when ESCAPE is hit
- case 27:
- discussionNoteForm = $textarea.closest('.js-discussion-note-form');
- if (discussionNoteForm.length) {
- if ($textarea.val() !== '') {
- if (!window.confirm(__('Your comment will be discarded.'))) {
- return;
- }
- }
- this.removeDiscussionNoteForm(discussionNoteForm);
- return;
- }
- editNote = $textarea.closest('.note');
- if (editNote.length) {
- originalText = $textarea.closest('form').data('originalNote');
- newText = $textarea.val();
- if (originalText !== newText) {
- if (!window.confirm(__('Are you sure you want to discard this comment?'))) {
- return;
- }
- }
- return this.removeNoteEditForm(editNote);
- }
- }
- }
-
- initRefresh() {
- if (Notes.interval) {
- clearInterval(Notes.interval);
- }
- Notes.interval = setInterval(() => this.refresh(), this.pollingInterval);
- }
-
- refresh() {
- if (!document.hidden) {
- return this.getContent();
- }
- }
-
- getContent() {
- if (this.refreshing) {
- return;
- }
-
- this.refreshing = true;
-
- axios
- .get(`${this.notes_url}?html=true`, {
- headers: {
- 'X-Last-Fetched-At': this.last_fetched_at,
- },
- })
- .then(({ data }) => {
- const { notes } = data;
- 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;
- });
- }
-
- /**
- * Increase @pollingInterval up to 120 seconds on every function call,
- * if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
- * will reset to @basePollingInterval.
- *
- * Note: this function is used to gradually increase the polling interval
- * if there aren't new notes coming from the server
- */
- setPollingInterval(shouldReset) {
- if (shouldReset == null) {
- shouldReset = true;
- }
- const nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
- if (shouldReset) {
- this.pollingInterval = this.basePollingInterval;
- } else if (this.pollingInterval < nthInterval) {
- this.pollingInterval *= 2;
- }
- return this.initRefresh();
- }
-
- handleQuickActions(noteEntity) {
- let votesBlock;
- if (noteEntity.commands_changes) {
- if ('merge' in noteEntity.commands_changes) {
- Notes.checkMergeRequestStatus();
- }
-
- 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
- });
- }
- }
- }
-
- setupNewNote($note) {
- // Update datetime format on the recent note
- localTimeAgo($note.find('.js-timeago').get(), false);
-
- this.collapseLongCommitList();
- this.taskList.init();
-
- // This stops the note highlight, #note_xxx`, from being removed after real time update
- // The `:target` selector does not re-evaluate after we replace element in the DOM
- Notes.updateNoteTargetSelector($note);
- this.$noteToCleanHighlight = $note;
- }
-
- onHashChange() {
- if (this.$noteToCleanHighlight) {
- Notes.updateNoteTargetSelector(this.$noteToCleanHighlight);
- }
-
- this.$noteToCleanHighlight = null;
- }
-
- static updateNoteTargetSelector($note) {
- const hash = getLocationHash();
- // Needs to be an explicit true/false for the jQuery `toggleClass(force)`
- const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0);
- $note.toggleClass('target', addTargetClass);
- }
-
- /**
- * Render note in main comments area.
- *
- * Note: for rendering inline notes use renderDiscussionNote
- */
- renderNote(noteEntity, $form, $notesList = $('.main-notes-list')) {
- if (noteEntity.discussion_html) {
- return this.renderDiscussionNote(noteEntity, $form);
- }
-
- if (!noteEntity.valid) {
- if (noteEntity.errors && noteEntity.errors.commands_only) {
- if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
- $notesList.find('.system-note.being-posted').remove();
- }
- this.addFlash({
- message: noteEntity.errors.commands_only,
- type: 'notice',
- parent: this.parentTimeline.get(0),
- });
- this.refresh();
- }
- return;
- }
-
- const $note = $notesList.find(`#note_${noteEntity.id}`);
- if (Notes.isNewNote(noteEntity, this.note_ids)) {
- if (isInMRPage()) {
- return;
- }
-
- this.note_ids.push(noteEntity.id);
-
- if ($notesList.length) {
- $notesList.find('.system-note.being-posted').remove();
- }
- const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
-
- this.setupNewNote($newNote);
- this.refresh();
- return this.updateNotesCount(1);
- } else if (Notes.isUpdatedNote(noteEntity, $note)) {
- // The server can send the same update multiple times so we need to make sure to only update once per actual update.
- const isEditing = $note.hasClass('is-editing');
- const initialContent = normalizeNewlines($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;
-
- if (isEditing && isTextareaUntouched) {
- $textarea.val(noteEntity.note);
- this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
- } else if (isEditing && !isTextareaUntouched) {
- this.putConflictEditWarningInPlace(noteEntity, $note);
- this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
- } else {
- const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
- this.setupNewNote($updatedNote);
- }
- }
- }
-
- isParallelView() {
- return Cookies.get('diff_view') === 'parallel';
- }
-
- /**
- * Render note in discussion area. To render inline notes use renderDiscussionNote.
- */
- renderDiscussionNote(noteEntity, $form) {
- let discussionContainer;
- let row;
-
- if (!Notes.isNewNote(noteEntity, this.note_ids)) {
- return;
- }
- this.note_ids.push(noteEntity.id);
-
- const 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;
- }
-
- // is this the first note of discussion?
- discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
- if (!discussionContainer.length) {
- discussionContainer = form.closest('.discussion').find('.notes');
- }
- if (discussionContainer.length === 0) {
- if (noteEntity.diff_discussion_html) {
- const $discussion = $(noteEntity.diff_discussion_html).renderGFM();
-
- 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
- const $notes = $discussion.find(
- `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
- );
- const contentContainerClass = $notes
- .closest('.notes-content')
- .attr('class')
- .split(' ')
- .join('.');
-
- row
- .find(`.${contentContainerClass} .content`)
- .append($notes.closest('.content').children());
- }
- } else {
- Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
- }
- } else {
- // append new note to all matching discussions
- Notes.animateAppendNote(noteEntity.html, discussionContainer);
- }
-
- localTimeAgo(document.querySelectorAll('.js-timeago'), false);
- Notes.checkMergeRequestStatus();
- return this.updateNotesCount(1);
- }
-
- getLineHolder(changesDiscussionContainer) {
- return $(changesDiscussionContainer)
- .closest('.notes_holder')
- .prevAll('.line_holder')
- .first()
- .get(0);
- }
-
- /**
- * Called in response the main target form has been successfully submitted.
- *
- * Removes any errors.
- * Resets text and preview.
- * Resets buttons.
- */
- resetMainTargetForm(e) {
- const form = $('.js-main-target-form');
- // remove validation errors
- 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();
-
- const event = document.createEvent('Event');
- event.initEvent('autosize:update', true, false);
- form.find('.js-autosize')[0].dispatchEvent(event);
-
- this.updateTargetButtons(e);
- }
-
- reenableTargetFormSubmitButton() {
- const form = $('.js-main-target-form');
- return form.find('.js-note-text').trigger('input');
- }
-
- /**
- * Shows the main form and does some setup on it.
- *
- * Sets some hidden fields in the form.
- */
- setupMainTargetNoteForm(enableGFM) {
- // find the form
- const form = $('.js-new-note-form');
- // Set a global clone of the form for later cloning
- this.formClone = form.clone();
- // show the form
- this.setupNoteForm(form, enableGFM);
- // fix classes
- form.removeClass('js-new-note-form');
- form.addClass('js-main-target-form');
- form.find('#note_line_code').remove();
- form.find('#note_position').remove();
- form.find('#note_type').val('');
- form.find('#note_project_id').remove();
- form.find('#in_reply_to_discussion_id').remove();
- this.parentTimeline = form.parents('.timeline');
-
- if (form.length) {
- Notes.initCommentTypeToggle(form.get(0));
- }
- }
-
- /**
- * General note form setup.
- *
- * deactivates the submit button when text is empty
- * hides the preview button when text is empty
- * set up GFM auto complete
- * show the form
- */
- setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
- this.glForm = new GLForm(form, enableGFM);
- const textarea = form.find('.js-note-text');
- const key = [
- s__('NoteForm|Note'),
- form.find('#note_noteable_type').val(),
- form.find('#note_noteable_id').val(),
- form.find('#note_commit_id').val(),
- form.find('#note_type').val(),
- form.find('#note_project_id').val(),
- form.find('#in_reply_to_discussion_id').val(),
-
- // LegacyDiffNote
- form.find('#note_line_code').val(),
-
- // DiffNote
- form.find('#note_position').val(),
- ];
- return new Autosave(textarea, key);
- }
-
- /**
- * Called in response to the new note form being submitted
- *
- * Adds new note to list.
- */
- addNote($form, note) {
- return this.renderNote(note);
- }
-
- addNoteError($form) {
- let formParentTimeline;
- if ($form.hasClass('js-main-target-form')) {
- formParentTimeline = $form.parents('.timeline');
- } else if ($form.hasClass('js-discussion-note-form')) {
- formParentTimeline = $form.closest('.discussion-notes').find('.notes');
- }
- return this.addFlash({
- message: __(
- 'Your comment could not be submitted! Please check your network connection and try again.',
- ),
- parent: formParentTimeline.get(0),
- });
- }
-
- updateNoteError() {
- createFlash({
- message: __(
- 'Your comment could not be updated! Please check your network connection and try again.',
- ),
- });
- }
-
- /**
- * Called in response to the new note form being submitted
- *
- * Adds new note to list.
- */
- addDiscussionNote($form, note, isNewDiffComment) {
- if ($form.attr('data-resolve-all') != null) {
- const discussionId = $form.data('discussionId');
- const mergeRequestId = $form.data('noteableIid');
-
- if (ResolveService != null) {
- ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
- }
- }
-
- this.renderNote(note, $form);
- // cleanup after successfully creating a diff/discussion note
- if (isNewDiffComment) {
- this.removeDiscussionNoteForm($form);
- }
- }
-
- /**
- * Called in response to the edit note form being submitted
- *
- * Updates the current note field.
- */
- updateNote(noteEntity, $targetNote) {
- // Convert returned HTML to a jQuery object so we can modify it further
- const $noteEntityEl = $(noteEntity.html);
- this.revertNoteEditForm($targetNote);
- $noteEntityEl.renderGFM();
- // Find the note's `li` element by ID and replace it with the updated HTML
- const $note_li = $(`.note-row-${noteEntity.id}`);
-
- $note_li.replaceWith($noteEntityEl);
- this.setupNewNote($noteEntityEl);
- }
-
- checkContentToAllowEditing($el) {
- const initialContent = $el.find('.original-note-content').text().trim();
- const currentContent = $el.find('.js-note-text').val();
- let isAllowed = true;
-
- if (currentContent === initialContent) {
- this.removeNoteEditForm($el);
- } else {
- const isWidgetVisible = isInViewport($el.get(0));
-
- if (!isWidgetVisible) {
- scrollToElement($el);
- }
-
- $el.find('.js-finish-edit-warning').show();
- isAllowed = false;
- }
-
- return isAllowed;
- }
-
- /**
- * Called in response to clicking the edit note link
- *
- * Replaces the note text with the note edit form
- * Adds a data attribute to the form with the original content of the note for cancellations
- */
- showEditForm(e) {
- e.preventDefault();
-
- const $target = $(e.target);
- const $editForm = $(this.getEditFormSelector($target));
- const $note = $target.closest('.note');
- const $currentlyEditing = $('.note.is-editing:visible');
-
- if ($currentlyEditing.length) {
- const isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
-
- if (!isEditAllowed) {
- return;
- }
- }
-
- $note.find('.js-note-attachment-delete').show();
- $editForm.addClass('current-note-edit-form');
- $note.addClass('is-editing');
- this.putEditFormInPlace($target);
- }
-
- /**
- * Called in response to clicking the edit note link
- *
- * Hides edit form and restores the original note text to the editor textarea.
- */
- cancelEdit(e) {
- e.preventDefault();
- const $target = $(e.target);
- const $note = $target.closest('.note');
- const noteId = $note.attr('data-note-id');
-
- this.revertNoteEditForm($target);
-
- if (this.updatedNotesTrackingMap[noteId]) {
- const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
- $note.replaceWith($newNote);
- this.setupNewNote($newNote);
- // Now that we have taken care of the update, clear it out
- delete this.updatedNotesTrackingMap[noteId];
- } else {
- $note.find('.js-finish-edit-warning').hide();
- this.removeNoteEditForm($note);
- }
- }
-
- revertNoteEditForm($target) {
- $target = $target || $('.note.is-editing:visible');
- const selector = this.getEditFormSelector($target);
- const $editForm = $(selector);
-
- $editForm.insertBefore('.diffs');
- $editForm.find('.js-comment-save-button').enable();
- $editForm.find('.js-finish-edit-warning').hide();
- }
-
- getEditFormSelector($el) {
- let selector = '.note-edit-form:not(.mr-note-edit-form)';
-
- if ($el.parents('#diffs').length) {
- selector = '.note-edit-form.mr-note-edit-form';
- }
-
- return selector;
- }
-
- removeNoteEditForm($note) {
- const form = $note.find('.diffs .current-note-edit-form');
-
- $note.removeClass('is-editing');
- 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'));
- }
-
- /**
- * Called in response to deleting a note of any kind.
- *
- * Removes the actual note from view.
- * Removes the whole discussion if the last note is being removed.
- */
- removeNote(e) {
- const $note = $(e.currentTarget).closest('.note');
- const noteElId = $note.attr('id');
- $(`.note[id="${noteElId}"]`).each((i, el) => {
- // 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.
- const $note = $(el);
- const $notes = $note.closest('.discussion-notes');
- const discussionId = $('.notes', $notes).data('discussionId');
-
- $note.remove();
-
- // check if this is the last note for this line
- if ($notes.find('.note').length === 0) {
- const 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.checkMergeRequestStatus();
- return this.updateNotesCount(-1);
- }
-
- /**
- * Called in response to clicking the delete attachment link
- *
- * Removes the attachment wrapper view, including image tag if it exists
- * Resets the note editing form
- */
- removeAttachment() {
- const $note = $(this).closest('.note');
- $note.find('.note-attachment').remove();
- $note.find('.note-body > .note-text').show();
- $note.find('.note-header').show();
- return $note.find('.diffs .current-note-edit-form').remove();
- }
-
- /**
- * Called when clicking on the "reply" button for a diff line.
- *
- * Shows the note form below the notes.
- */
- onReplyToDiscussionNote(e) {
- this.replyToDiscussionNote(e.target);
- }
-
- replyToDiscussionNote(target) {
- const form = this.cleanForm(this.formClone.clone());
- const replyLink = $(target).closest('.js-discussion-reply-button');
- // insert the form after the button
- replyLink.closest('.discussion-reply-holder').hide().after(form);
- // show the form
- return this.setupDiscussionNoteForm(replyLink, form);
- }
-
- /**
- * Shows the diff or discussion form and does some setup on it.
- *
- * Sets some hidden fields in the form.
- *
- * Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
- */
- setupDiscussionNoteForm(dataHolder, form) {
- // set up note target
- let diffFileData = dataHolder.closest('.text-file');
-
- if (diffFileData.length === 0) {
- diffFileData = dataHolder.closest('.image');
- }
-
- const discussionID = dataHolder.data('discussionId');
-
- if (discussionID) {
- form.attr('data-discussion-id', discussionID);
- form.find('#in_reply_to_discussion_id').val(discussionID);
- }
-
- form.find('#note_project_id').val(dataHolder.data('discussionProjectId'));
-
- form.attr('data-line-code', dataHolder.data('lineCode'));
- form.find('#line_type').val(dataHolder.data('lineType'));
-
- form.find('#note_noteable_type').val(diffFileData.data('noteableType'));
- form.find('#note_noteable_id').val(diffFileData.data('noteableId'));
- form.find('#note_commit_id').val(diffFileData.data('commitId'));
-
- form.find('#note_type').val(dataHolder.data('noteType'));
-
- // LegacyDiffNote
- form.find('#note_line_code').val(dataHolder.data('lineCode'));
-
- // DiffNote
- form.find('#note_position').val(dataHolder.attr('data-position'));
-
- form.append('</div>').find('.js-close-discussion-note-form').show().removeClass('hide');
- form.find('.js-note-target-close').remove();
- form.find('.js-note-new-discussion').remove();
- this.setupNoteForm(form);
-
- form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
-
- form.find('.js-note-text').focus();
- form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
- }
-
- /**
- * Called when clicking on the "add a comment" button on the side of a diff line.
- *
- * Inserts a temporary row for the form below the line.
- * Sets up the form and shows it.
- */
- onAddDiffNote(e) {
- e.preventDefault();
- const link = e.currentTarget || e.target;
- const $link = $(link);
- const showReplyInput = !$link.hasClass('js-diff-comment-avatar');
- this.toggleDiffNote({
- target: $link,
- lineType: link.dataset.lineType,
- showReplyInput,
- currentUsername: gon.current_username,
- currentUserAvatar: gon.current_user_avatar_url,
- currentUserFullname: gon.current_user_fullname,
- });
- }
-
- onAddImageDiffNote(e) {
- const $link = $(e.currentTarget || e.target);
- const $diffFile = $link.closest('.diff-file');
-
- const clickEvent = new CustomEvent('click.imageDiff', {
- detail: e,
- });
-
- $diffFile[0].dispatchEvent(clickEvent);
-
- // Set up comment form
- let newForm;
- const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
- const $form = $noteContainer.find('> .discussion-form');
-
- if ($form.length === 0) {
- newForm = this.cleanForm(this.formClone.clone());
- newForm.appendTo($noteContainer);
- } else {
- newForm = $form;
- }
-
- this.setupDiscussionNoteForm($link, newForm);
- }
-
- toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
- let addForm;
- let newForm;
- let noteForm;
- let replyButton;
- let rowCssToAdd;
- const $link = $(target);
- const row = $link.closest('tr');
- const nextRow = row.next();
- let targetRow = row;
- if (nextRow.is('.notes_holder')) {
- targetRow = nextRow;
- }
-
- const hasNotes = nextRow.is('.notes_holder');
- addForm = false;
- let lineTypeSelector = '';
- rowCssToAdd =
- '<tr class="notes_holder js-temp-notes-holder"><td class="notes-content" colspan="3"><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>';
- }
- const notesContentSelector = `.notes-content${lineTypeSelector} .content`;
- let notesContent = targetRow.find(notesContentSelector);
-
- if (hasNotes && showReplyInput) {
- targetRow.show();
- notesContent = targetRow.find(notesContentSelector);
- if (notesContent.length) {
- notesContent.show();
- replyButton = notesContent.find('.js-discussion-reply-button:visible');
- if (replyButton.length) {
- this.replyToDiscussionNote(replyButton[0]);
- } else {
- // In parallel view, the form may not be present in one of the panes
- noteForm = notesContent.find('.js-discussion-note-form');
- if (noteForm.length === 0) {
- addForm = true;
- }
- }
- }
- } else if (showReplyInput) {
- // add a notes row and insert the form
- row.after(rowCssToAdd);
- targetRow = row.next();
- notesContent = targetRow.find(notesContentSelector);
- addForm = true;
- } else {
- const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
- const isForced = forceShow === true || forceShow === false;
- const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
-
- targetRow.toggleClass('hide', !showNow);
- notesContent.toggleClass('hide', !showNow);
- }
-
- if (addForm) {
- newForm = this.cleanForm(this.formClone.clone());
- newForm.appendTo(notesContent);
- // show the form
- return this.setupDiscussionNoteForm($link, newForm);
- }
- }
-
- /**
- * Called in response to "cancel" on a diff note form.
- *
- * Shows the reply button again.
- * Removes the form and if necessary it's temporary row.
- */
- removeDiscussionNoteForm(form) {
- const row = form.closest('tr');
- const 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();
- if (row.is('.js-temp-notes-holder')) {
- // remove temporary row for diff lines
- return row.remove();
- }
- // only remove the form
- return form.remove();
- }
-
- cancelDiscussionForm(e) {
- e.preventDefault();
- const $form = $(e.target).closest('.js-discussion-note-form');
- const $discussionNote = $(e.target).closest('.discussion-notes');
-
- if ($discussionNote.length === 0) {
- // Only send blur event when the discussion form
- // is not part of a discussion note
- const $diffFile = $form.closest('.diff-file');
-
- if ($diffFile.length > 0) {
- const blurEvent = new CustomEvent('blur.imageDiff', {
- detail: e,
- });
-
- $diffFile[0].dispatchEvent(blurEvent);
- }
- }
-
- return this.removeDiscussionNoteForm($form);
- }
-
- /**
- * Called after an attachment file has been selected.
- *
- * Updates the file name for the selected attachment.
- */
- updateFormAttachment() {
- const form = $(this).closest('form');
- // get only the basename
- const filename = $(this)
- .val()
- .replace(/^.*[\\\/]/, '');
- return form.find('.js-attachment-filename').text(filename);
- }
-
- /**
- * Called when the tab visibility changes
- */
- visibilityChange() {
- return this.refresh();
- }
-
- updateTargetButtons(e) {
- let closetext;
- let reopentext;
- const textarea = $(e.target);
- const form = textarea.parents('form');
- const reopenbtn = form.find('.js-note-target-reopen');
- const closebtn = form.find('.js-note-target-close');
-
- if (textarea.val().trim().length > 0) {
- reopentext = reopenbtn.attr('data-alternative-text');
- closetext = closebtn.attr('data-alternative-text');
- if (reopenbtn.text() !== reopentext) {
- reopenbtn.text(reopentext);
- }
- if (closebtn.text() !== closetext) {
- closebtn.text(closetext);
- }
- if (reopenbtn.is(':not(.btn-comment-and-reopen)')) {
- reopenbtn.addClass('btn-comment-and-reopen');
- }
- if (closebtn.is(':not(.btn-comment-and-close)')) {
- closebtn.addClass('btn-comment-and-close');
- }
- } else {
- reopentext = reopenbtn.data('originalText');
- closetext = closebtn.data('originalText');
- if (reopenbtn.text() !== reopentext) {
- reopenbtn.text(reopentext);
- }
- if (closebtn.text() !== closetext) {
- closebtn.text(closetext);
- }
- if (reopenbtn.is('.btn-comment-and-reopen')) {
- reopenbtn.removeClass('btn-comment-and-reopen');
- }
- if (closebtn.is('.btn-comment-and-close')) {
- closebtn.removeClass('btn-comment-and-close');
- }
- }
- }
-
- putEditFormInPlace($el) {
- const $editForm = $(this.getEditFormSelector($el));
- const $note = $el.closest('.note');
-
- $editForm.insertAfter($note.find('.note-text'));
-
- const $originalContentEl = $note.find('.original-note-content');
- const originalContent = $originalContentEl.text().trim();
- const postUrl = $originalContentEl.data('postUrl');
- const targetId = $originalContentEl.data('targetId');
- const targetType = $originalContentEl.data('targetType');
-
- this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
-
- $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-md-write-button').trigger('click');
- $editForm.find('.referenced-users').hide();
- }
-
- putConflictEditWarningInPlace(noteEntity, $note) {
- if ($note.find('.js-conflict-edit-warning').length === 0) {
- const open_link = `<a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">`;
- const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
- ${sprintf(
- s__(
- 'Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost',
- ),
- {
- open_link,
- close_link: '</a>',
- },
- )}
- </div>`);
- $alert.insertAfter($note.find('.note-text'));
- }
- }
-
- updateNotesCount(updateCount) {
- return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
- }
-
- static renderPlaceholderComponent($container) {
- const el = $container.find('.js-code-placeholder').get(0);
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- GlSkeletonLoading,
- },
- render(createElement) {
- return createElement('gl-skeleton-loading');
- },
- });
- }
-
- static renderDiffContent($container, data) {
- const { discussion_html } = data;
- const lines = $(discussion_html).find('.line_holder');
- lines.addClass('fade-in');
- $container.find('.diff-content > table > tbody').prepend(lines);
- const fileHolder = $container.find('.file-holder');
- $container.find('.line-holder-placeholder').remove();
- syntaxHighlight(fileHolder);
- }
-
- onClickRetryLazyLoad(e) {
- const $retryButton = $(e.currentTarget);
-
- $retryButton.prop('disabled', true);
-
- return this.loadLazyDiff(e).then(() => {
- $retryButton.prop('disabled', false);
- });
- }
-
- loadLazyDiff(e) {
- const $container = $(e.currentTarget).closest('.js-toggle-container');
- Notes.renderPlaceholderComponent($container);
-
- $container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
-
- const $tableEl = $container.find('tbody');
- if ($tableEl.length === 0) return;
-
- const fileHolder = $container.find('.file-holder');
- const url = fileHolder.data('linesPath');
-
- const $errorContainer = $container.find('.js-error-lazy-load-diff');
- const $successContainer = $container.find('.js-success-lazy-load');
-
- /**
- * We only fetch resolved discussions.
- * Unresolved discussions don't have an endpoint being provided.
- */
- if (url) {
- return axios
- .get(url)
- .then(({ data }) => {
- // Reset state in case last request returned error
- $successContainer.removeClass('hidden');
- $errorContainer.addClass('hidden');
-
- Notes.renderDiffContent($container, data);
- })
- .catch(() => {
- $successContainer.addClass('hidden');
- $errorContainer.removeClass('hidden');
- });
- }
- return Promise.resolve();
- }
-
- toggleCommitList(e) {
- const $element = $(e.currentTarget);
- const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
- const $svgChevronUpElement = $element.find('svg.js-chevron-up');
- const $svgChevronDownElement = $element.find('svg.js-chevron-down');
-
- $svgChevronUpElement.toggleClass('gl-display-none');
- $svgChevronDownElement.toggleClass('gl-display-none');
-
- $closestSystemCommitList.toggleClass('hide-shade');
- }
-
- /**
- * Scans system notes with `ul` elements in system note body
- * then collapse long commit list pushed by user to make it less
- * intrusive.
- */
- collapseLongCommitList() {
- const systemNotes = $('#notes-list').find('li.system-note').has('ul');
-
- $.each(systemNotes, (index, systemNote) => {
- const $systemNote = $(systemNote);
- const headerMessage = $systemNote
- .find('.note-text')
- .find('p')
- .first()
- .text()
- .replace(':', '');
-
- $systemNote.find('.note-header .system-note-message').html(headerMessage);
-
- if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) {
- $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');
- }
- });
- }
-
- addFlash(...flashParams) {
- this.flashContainer = createFlash(...flashParams);
- }
-
- clearFlash() {
- if (this.flashContainer) {
- this.flashContainer.style.display = 'none';
- this.flashContainer = null;
- }
- }
-
- cleanForm($form) {
- // Remove JS classes that are not needed here
- $form.find('.js-comment-type-dropdown').removeClass('btn-group');
-
- // Remove dropdown
- $form.find('.dropdown-menu').remove();
-
- return $form;
- }
-
- /**
- * Check if note does not exist on page
- */
- static isNewNote(noteEntity, noteIds) {
- return $.inArray(noteEntity.id, noteIds) === -1;
- }
-
- /**
- * Check if $note already contains the `noteEntity` content
- */
- static isUpdatedNote(noteEntity, $note) {
- // 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(),
- );
- return sanitizedNoteEntityText !== currentNoteText;
- }
-
- static checkMergeRequestStatus() {
- if (getPagePath(1) === 'merge_requests' && gl.mrWidget) {
- gl.mrWidget.checkStatus();
- }
- }
-
- static animateAppendNote(noteHtml, $notesList) {
- const $note = $(noteHtml);
-
- $note.addClass('fade-in-full').renderGFM();
- $notesList.append($note);
- return $note;
- }
-
- static animateUpdateNote(noteHtml, $note) {
- const $updatedNote = $(noteHtml);
-
- $updatedNote.addClass('fade-in').renderGFM();
- $note.replaceWith($updatedNote);
- return $updatedNote;
- }
-
- /**
- * Get data from Form attributes to use for saving/submitting comment.
- */
- getFormData($form) {
- const content = $form.find('.js-note-text').val();
- return {
- // eslint-disable-next-line no-jquery/no-serialize
- formData: $form.serialize(),
- formContent: escape(content),
- formAction: $form.attr('action'),
- formContentOriginal: content,
- };
- }
-
- /**
- * Identify if comment has any quick actions
- */
- hasQuickActions(formContent) {
- return REGEX_QUICK_ACTIONS.test(formContent);
- }
-
- /**
- * Remove quick actions and leave comment with pure message
- */
- stripQuickActions(formContent) {
- return formContent.replace(REGEX_QUICK_ACTIONS, '').trim();
- }
-
- /**
- * Gets appropriate description from quick actions found in provided `formContent`
- */
- getQuickActionDescription(formContent, availableQuickActions = []) {
- let tempFormContent;
-
- // Identify executed quick actions from `formContent`
- const executedCommands = availableQuickActions.filter((command) => {
- const commandRegex = new RegExp(`/${command.name}`);
- return commandRegex.test(formContent);
- });
-
- if (executedCommands && executedCommands.length) {
- if (executedCommands.length > 1) {
- tempFormContent = __('Applying multiple commands');
- } else {
- const commandDescription = executedCommands[0].description.toLowerCase();
- tempFormContent = sprintf(__('Applying command to %{commandDescription}'), {
- commandDescription,
- });
- }
- } else {
- tempFormContent = __('Applying command');
- }
-
- return tempFormContent;
- }
-
- /**
- * Create placeholder note DOM element populated with comment body
- * that we will show while comment is being posted.
- * 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,
- }) {
- const discussionClass = isDiscussionNote ? 'discussion' : '';
- const $tempNote = $(
- `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
- <div class="timeline-entry-inner">
- <div class="timeline-icon">
- <a href="/${escape(currentUsername)}">
- <img class="avatar s40" src="${currentUserAvatar}" />
- </a>
- </div>
- <div class="timeline-content ${discussionClass}">
- <div class="note-header">
- <div class="note-header-info">
- <a href="/${escape(currentUsername)}">
- <span class="d-none d-sm-inline-block bold">${escape(currentUsername)}</span>
- <span class="note-headline-light">${escape(currentUsername)}</span>
- </a>
- </div>
- </div>
- <div class="note-body">
- <div class="note-text">
- <p>${formContent}</p>
- </div>
- </div>
- </div>
- </div>
- </li>`,
- );
-
- $tempNote.find('.d-none.d-sm-inline-block').text(escape(currentUserFullname));
- $tempNote.find('.note-headline-light').text(`@${escape(currentUsername)}`);
-
- return $tempNote;
- }
-
- /**
- * Create Placeholder System Note DOM element populated with quick action description
- */
- createPlaceholderSystemNote({ formContent, uniqueId }) {
- const $tempNote = $(
- `<li id="${uniqueId}" class="note system-note timeline-entry being-posted fade-in-half">
- <div class="timeline-entry-inner">
- <div class="timeline-content">
- <i>${formContent}</i>
- </div>
- </div>
- </li>`,
- );
-
- return $tempNote;
- }
-
- /**
- * This method does following tasks step-by-step whenever a new comment
- * is submitted by user (both main thread comments as well as discussion comments).
- *
- * 1) Get Form metadata
- * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve
- * 3) Build temporary placeholder element (using `createPlaceholderNote`)
- * 4) Show placeholder note on UI
- * 5) Perform network request to submit the note using `axios.post`
- * a) If request is successfully completed
- * 1. Remove placeholder element
- * 2. Show submitted Note element
- * 3. Perform post-submit errands
- * a. Mark discussion as resolved if comment submission was for resolve.
- * b. Reset comment form to original state.
- * b) If request failed
- * 1. Remove placeholder element
- * 2. Show error Flash message about failure
- */
- postComment(e) {
- e.preventDefault();
-
- // Get Form metadata
- const $submitBtn = $(e.target);
- $submitBtn.prop('disabled', true);
- 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 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);
- let noteUniqueId;
- let systemNoteUniqueId;
- let hasQuickActions = false;
- let $notesContainer;
- let tempFormContent;
-
- // Get reference to notes container based on type of comment
- if (isDiscussionForm) {
- $notesContainer = $form.parent('.discussion-notes').find('.notes');
- } else if (isMainForm) {
- $notesContainer = $('ul.main-notes-list');
- }
-
- // If comment is to resolve discussion, disable submit buttons while
- // comment posting is finished.
- if (isDiscussionResolve) {
- $form.find('.js-comment-submit-button').disable();
- }
-
- tempFormContent = formContent;
- if (this.glForm.supportsQuickActions && this.hasQuickActions(formContent)) {
- tempFormContent = this.stripQuickActions(formContent);
- hasQuickActions = true;
- }
-
- // 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,
- }),
- );
- }
-
- // 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,
- }),
- );
- }
-
- // Clear the form textarea
- if ($notesContainer.length) {
- if (isMainForm) {
- this.resetMainTargetForm(e);
- } else if (isDiscussionForm) {
- this.removeDiscussionNoteForm($form);
- }
- }
-
- $closeBtn.text($closeBtn.data('originalText'));
-
- // Make request to submit comment on server
- return axios
- .post(`${formAction}?html=true`, formData)
- .then((res) => {
- const note = res.data;
-
- $submitBtn.prop('disabled', false);
- // Submission successful! remove placeholder
- $notesContainer.find(`#${noteUniqueId}`).remove();
-
- const $diffFile = $form.closest('.diff-file');
- if ($diffFile.length > 0) {
- const blurEvent = new CustomEvent('blur.imageDiff', {
- detail: e,
- });
-
- $diffFile[0].dispatchEvent(blurEvent);
- }
-
- // Reset cached commands list when command is applied
- if (hasQuickActions) {
- $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
- }
-
- // Clear previous form errors
- this.clearFlashWrapper();
-
- // Check if this was discussion comment
- if (isDiscussionForm) {
- // Remove flash-container
- $notesContainer.find('.flash-container').remove();
-
- // If comment intends to resolve discussion, do the same.
- if (isDiscussionResolve) {
- $form
- .attr('data-discussion-id', $submitBtn.data('discussionId'))
- .attr('data-resolve-all', 'true')
- .attr('data-project-path', $submitBtn.data('projectPath'));
- }
-
- // Show final note element on UI
- const isNewDiffComment = $notesContainer.length === 0;
- this.addDiscussionNote($form, note, isNewDiffComment);
-
- if (isNewDiffComment) {
- // Add image badge, avatar badge and toggle discussion badge for new image diffs
- const notePosition = $form.find('#note_position').val();
- if ($diffFile.length > 0 && notePosition.length > 0) {
- const { x, y, width, height } = JSON.parse(notePosition);
- const addBadgeEvent = new CustomEvent('addBadge.imageDiff', {
- detail: {
- x,
- y,
- width,
- height,
- noteId: `note_${note.id}`,
- discussionId: note.discussion_id,
- },
- });
-
- $diffFile[0].dispatchEvent(addBadgeEvent);
- }
- }
-
- // append flash-container to the Notes list
- if ($notesContainer.length) {
- $notesContainer.append('<div class="flash-container" style="display: none;"></div>');
- }
- } 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);
- }
-
- if (note.commands_changes) {
- this.handleQuickActions(note);
- }
-
- $form.trigger('ajax:success', [note]);
- })
- .catch(() => {
- // Submission failed, remove placeholder note and show Flash error message
- $notesContainer.find(`#${noteUniqueId}`).remove();
- $submitBtn.prop('disabled', false);
- const blurEvent = new CustomEvent('blur.imageDiff', {
- detail: e,
- });
-
- const closestDiffFile = $form.closest('.diff-file');
-
- if (closestDiffFile.length) {
- closestDiffFile[0].dispatchEvent(blurEvent);
- }
-
- if (hasQuickActions) {
- $notesContainer.find(`#${systemNoteUniqueId}`).remove();
- }
-
- // Show form again on UI on failure
- if (isDiscussionForm && $notesContainer.length) {
- const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
- this.replyToDiscussionNote(replyButton[0]);
- $form = $notesContainer.parent().find('form');
- }
-
- $form.find('.js-note-text').val(formContentOriginal);
- this.reenableTargetFormSubmitButton(e);
- this.addNoteError($form);
- });
- }
-
- /**
- * This method does following tasks step-by-step whenever an existing comment
- * is updated by user (both main thread comments as well as discussion comments).
- *
- * 1) Get Form metadata
- * 2) Update note element with new content
- * 3) Perform network request to submit the updated note using `axios.post`
- * a) If request is successfully completed
- * 1. Show submitted Note element
- * b) If request failed
- * 1. Revert Note element to original content
- * 2. Show error Flash message about failure
- */
- updateComment(e) {
- e.preventDefault();
-
- // Get Form metadata
- const $submitBtn = $(e.target);
- const $form = $submitBtn.parents('form');
- const $closeBtn = $form.find('.js-note-target-close');
- const $editingNote = $form.parents('.note.is-editing');
- const $noteBody = $editingNote.find('.js-task-list-container');
- const $noteBodyText = $noteBody.find('.note-text');
- const { formData, formContent, formAction } = this.getFormData($form);
-
- // Cache original comment content
- const cachedNoteBodyText = $noteBodyText.html();
-
- // 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('<span class="spinner align-text-bottom"></span>');
-
- // Make request to update comment on server
- axios
- .post(`${formAction}?html=true`, formData)
- .then(({ data }) => {
- // Submission successful! render final note element
- this.updateNote(data, $editingNote);
- })
- .catch(() => {
- // Submission failed, revert back to original note
- $noteBodyText.html(escape(cachedNoteBodyText));
- $editingNote.removeClass('being-posted fade-in');
- $editingNote.find('.gl-spinner').remove();
-
- // Show Flash message about failure
- this.updateNoteError();
- });
-
- return $closeBtn.text($closeBtn.data('originalText'));
- }
-}
-
-window.Notes = Notes;