debian-mirror-gitlab/app/assets/javascripts/deprecated_notes.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1833 lines
61 KiB
JavaScript
Raw Normal View History

2022-08-13 15:12:31 +05:30
/* eslint-disable camelcase,
2020-01-01 13:55:28 +05:30
no-unused-expressions, default-case,
2022-05-07 20:08:51 +05:30
consistent-return, no-param-reassign,
2020-01-01 13:55:28 +05:30
no-shadow, no-useless-escape,
2019-12-26 22:10:19 +05:30
class-methods-use-this */
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
/* global ResolveService */
2019-09-04 21:01:54 +05:30
/*
2021-11-11 11:23:49 +05:30
deprecated_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
2019-09-04 21:01:54 +05:30
*/
2022-08-13 15:12:31 +05:30
import { GlSkeletonLoader } from '@gitlab/ui';
2021-03-11 19:13:27 +05:30
import Autosize from 'autosize';
2017-08-17 22:00:37 +05:30
import $ from 'jquery';
2021-03-11 19:13:27 +05:30
import { escape, uniqueId } from 'lodash';
2018-05-09 12:01:36 +05:30
import Vue from 'vue';
2023-03-17 16:20:25 +05:30
import { renderGFM } from '~/behaviors/markdown/render_gfm';
2023-03-04 22:38:38 +05:30
import { createAlert, VARIANT_INFO } from '~/flash';
2023-04-23 21:23:45 +05:30
import { sanitize } from '~/lib/dompurify';
2021-03-11 19:13:27 +05:30
import '~/lib/utils/jquery_at_who';
2020-01-01 13:55:28 +05:30
import AjaxCache from '~/lib/utils/ajax_cache';
2022-05-07 20:08:51 +05:30
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
2020-01-01 13:55:28 +05:30
import syntaxHighlight from '~/syntax_highlight';
2021-11-18 22:05:49 +05:30
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
import * as constants from '~/notes/constants';
2022-05-07 20:08:51 +05:30
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
2021-03-11 19:13:27 +05:30
import Autosave from './autosave';
import loadAwardsHandler from './awards_handler';
2018-11-08 19:23:39 +05:30
import { defaultAutocompleteConfig } from './gfm_auto_complete';
2018-03-17 18:26:18 +05:30
import GLForm from './gl_form';
2021-03-11 19:13:27 +05:30
import axios from './lib/utils/axios_utils';
2018-05-09 12:01:36 +05:30
import {
2022-04-04 11:22:00 +05:30
getCookie,
2018-05-09 12:01:36 +05:30
isInViewport,
getPagePath,
scrollToElement,
isMetaKey,
2018-11-08 19:23:39 +05:30
isInMRPage,
2018-05-09 12:01:36 +05:30
} from './lib/utils/common_utils';
2018-03-17 18:26:18 +05:30
import { localTimeAgo } from './lib/utils/datetime_utility';
2021-03-11 19:13:27 +05:30
import { getLocationHash } from './lib/utils/url_utility';
2019-09-04 21:01:54 +05:30
import { sprintf, s__, __ } from './locale';
2021-03-11 19:13:27 +05:30
import TaskList from './task_list';
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
window.autosize = Autosize;
2017-09-10 17:25:29 +05:30
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 {
2018-11-08 19:23:39 +05:30
static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM) {
2018-03-17 18:26:18 +05:30
if (!this.instance) {
2018-11-08 19:23:39 +05:30
this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
2018-03-17 18:26:18 +05:30
}
}
2018-03-27 19:54:05 +05:30
static getInstance() {
return this.instance;
}
2018-11-08 19:23:39 +05:30
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = defaultAutocompleteConfig) {
2017-09-10 17:25:29 +05:30
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);
2018-03-17 18:26:18 +05:30
this.onAddImageDiffNote = this.onAddImageDiffNote.bind(this);
2017-09-10 17:25:29 +05:30
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);
2023-03-04 22:38:38 +05:30
this.clearAlertWrapper = this.clearAlert.bind(this);
2017-09-10 17:25:29 +05:30
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;
2018-05-09 12:01:36 +05:30
this.notesCountBadge ||
(this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
2017-09-10 17:25:29 +05:30
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
2018-11-08 19:23:39 +05:30
this.$wrapperEl = isInMRPage() ? $(document).find('.diffs') : $(document);
2017-09-10 17:25:29 +05:30
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
2018-11-08 19:23:39 +05:30
this.setupMainTargetNoteForm(enableGFM);
2017-09-10 17:25:29 +05:30
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
2018-05-09 12:01:36 +05:30
selector: '.notes',
2017-09-10 17:25:29 +05:30
});
this.collapseLongCommitList();
this.setViewType(view);
2021-04-29 21:17:54 +05:30
// We are in the merge requests page so we need another edit form for Changes tab
2018-03-17 18:26:18 +05:30
if (getPagePath(1) === 'merge_requests') {
2021-03-08 18:12:59 +05:30
$('.note-edit-form').clone().addClass('mr-note-edit-form').insertAfter('.note-edit-form');
2018-05-09 12:01:36 +05:30
}
const hash = getLocationHash();
const $anchor = hash && document.getElementById(hash);
if ($anchor) {
this.loadLazyDiff({ currentTarget: $anchor });
2017-09-10 17:25:29 +05:30
}
}
setViewType(view) {
2022-04-04 11:22:00 +05:30
this.view = getCookie('diff_view') || view;
2017-09-10 17:25:29 +05:30
}
addBinding() {
// Edit note link
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
2017-09-10 17:25:29 +05:30
// Reopen and close actions for Issue/MR combined with note form submit
2021-11-18 22:05:49 +05:30
this.$wrapperEl.on(
'click',
// this oddly written selector needs to match the old style (input with class) as
// well as the new DOM styling from the Vue-based note form
'input.js-comment-submit-button, .js-comment-submit-button > button:first-child',
this.postComment,
);
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
2017-09-10 17:25:29 +05:30
// resolve a discussion
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
2017-09-10 17:25:29 +05:30
// remove a note (in general)
2022-06-21 17:19:12 +05:30
this.$wrapperEl.on('ajax:success', '.js-note-delete', this.removeNote);
2017-09-10 17:25:29 +05:30
// delete note attachment
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
2017-09-10 17:25:29 +05:30
// update the file name when an attachment is selected
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
2017-09-10 17:25:29 +05:30
// reply to diff/discussion notes
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
2017-09-10 17:25:29 +05:30
// add diff note
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
2018-03-17 18:26:18 +05:30
// add diff note for images
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
2017-09-10 17:25:29 +05:30
// hide diff note form
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
2017-09-10 17:25:29 +05:30
// toggle commit list
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
2018-05-09 12:01:36 +05:30
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
2018-11-20 20:47:30 +05:30
this.$wrapperEl.on(
'click',
'.js-toggle-lazy-diff-retry-button',
this.onClickRetryLazyLoad.bind(this),
);
2018-05-09 12:01:36 +05:30
2017-09-10 17:25:29 +05:30
// fetch notes when tab becomes visible
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
2017-09-10 17:25:29 +05:30
// when issue status changes, we need to refresh data
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('issuable:change', this.refresh);
2017-09-10 17:25:29 +05:30
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
2018-11-08 19:23:39 +05:30
this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
2018-05-09 12:01:36 +05:30
this.$wrapperEl.on(
'ajax:complete',
'.js-main-target-form',
this.reenableTargetFormSubmitButton,
);
2017-09-10 17:25:29 +05:30
// when a key is clicked on the notes
2018-03-27 19:54:05 +05:30
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
2017-09-10 17:25:29 +05:30
// When the URL fragment/hash has changed, `#note_xxx`
2018-03-27 19:54:05 +05:30
$(window).on('hashchange', this.onHashChange);
2017-09-10 17:25:29 +05:30
}
cleanBinding() {
2018-03-27 19:54:05 +05:30
this.$wrapperEl.off('click', '.js-note-edit');
this.$wrapperEl.off('click', '.note-edit-cancel');
2022-06-21 17:19:12 +05:30
this.$wrapperEl.off('ajax:success', '.js-note-delete');
2018-03-27 19:54:05 +05:30
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');
2021-02-22 17:27:13 +05:30
// eslint-disable-next-line @gitlab/no-global-event-off
2018-03-27 19:54:05 +05:30
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');
2018-05-09 12:01:36 +05:30
this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
2018-03-27 19:54:05 +05:30
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');
2017-09-10 17:25:29 +05:30
$(window).off('hashchange', this.onHashChange);
}
static initCommentTypeToggle(form) {
2021-11-18 22:05:49 +05:30
const el = form.querySelector('.js-comment-type-dropdown');
const { noteableName } = el.dataset;
2017-09-10 17:25:29 +05:30
const noteTypeInput = form.querySelector('#note_type');
2021-11-18 22:05:49 +05:30
const formHasContent = form.querySelector('.js-note-text').value.trim().length > 0;
2017-09-10 17:25:29 +05:30
2021-11-18 22:05:49 +05:30
form.commentTypeComponent = new Vue({
el,
data() {
return {
noteType: constants.COMMENT,
disabled: !formHasContent,
};
},
render(createElement) {
return createElement(CommentTypeDropdown, {
props: {
noteType: this.noteType,
noteableDisplayName: noteableName,
disabled: this.disabled,
},
on: {
change: (arg) => {
this.noteType = arg;
if (this.noteType === constants.DISCUSSION) {
noteTypeInput.value = constants.DISCUSSION_NOTE;
} else {
noteTypeInput.value = '';
}
},
},
});
},
});
2017-09-10 17:25:29 +05:30
}
2022-05-07 20:08:51 +05:30
async keydownNoteText(e) {
2020-01-01 13:55:28 +05:30
let discussionNoteForm;
let editNote;
let myLastNote;
let myLastNoteEditBtn;
let newText;
let originalText;
2018-03-17 18:26:18 +05:30
if (isMetaKey(e)) {
2017-09-10 17:25:29 +05:30
return;
2016-09-13 17:45:13 +05:30
}
2020-01-01 13:55:28 +05:30
const $textarea = $(e.target);
2017-09-10 17:25:29 +05:30
// Edit previous note when UP arrow is hit
switch (e.which) {
case 38:
if ($textarea.val() !== '') {
return;
}
2018-05-09 12:01:36 +05:30
myLastNote = $(
2018-11-08 19:23:39 +05:30
`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`,
2018-05-09 12:01:36 +05:30
$textarea.closest('.note, .notes_holder, #notes'),
);
2017-09-10 17:25:29 +05:30
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) {
2016-09-13 17:45:13 +05:30
if ($textarea.val() !== '') {
2022-05-07 20:08:51 +05:30
const confirmed = await confirmAction(__('Your comment will be discarded.'), {
primaryBtnVariant: 'danger',
primaryBtnText: __('Discard'),
});
if (!confirmed) return;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
this.removeDiscussionNoteForm(discussionNoteForm);
return;
}
editNote = $textarea.closest('.note');
if (editNote.length) {
2018-03-27 19:54:05 +05:30
originalText = $textarea.closest('form').data('originalNote');
2017-09-10 17:25:29 +05:30
newText = $textarea.val();
if (originalText !== newText) {
2022-05-07 20:08:51 +05:30
const confirmed = await confirmAction(
__('Are you sure you want to discard this comment?'),
{
primaryBtnVariant: 'danger',
primaryBtnText: __('Discard'),
},
);
if (!confirmed) return;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
return this.removeNoteEditForm(editNote);
}
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
initRefresh() {
if (Notes.interval) {
2016-09-13 17:45:13 +05:30
clearInterval(Notes.interval);
2017-09-10 17:25:29 +05:30
}
2019-12-26 22:10:19 +05:30
Notes.interval = setInterval(() => this.refresh(), this.pollingInterval);
2017-09-10 17:25:29 +05:30
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
refresh() {
if (!document.hidden) {
return this.getContent();
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
getContent() {
if (this.refreshing) {
return;
}
2018-03-27 19:54:05 +05:30
2017-09-10 17:25:29 +05:30
this.refreshing = true;
2018-03-27 19:54:05 +05:30
2018-05-09 12:01:36 +05:30
axios
.get(`${this.notes_url}?html=true`, {
headers: {
'X-Last-Fetched-At': this.last_fetched_at,
},
})
.then(({ data }) => {
2018-11-08 19:23:39 +05:30
const { notes } = data;
2018-05-09 12:01:36 +05:30
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;
});
2017-09-10 17:25:29 +05:30
}
/**
* 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;
}
2022-08-13 15:12:31 +05:30
const nthInterval = this.basePollingInterval * 2 ** (this.maxPollingSteps - 1);
2017-09-10 17:25:29 +05:30
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
this.pollingInterval *= 2;
}
return this.initRefresh();
}
handleQuickActions(noteEntity) {
2020-01-01 13:55:28 +05:30
let votesBlock;
2017-09-10 17:25:29 +05:30
if (noteEntity.commands_changes) {
if ('merge' in noteEntity.commands_changes) {
Notes.checkMergeRequestStatus();
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0);
2017-08-17 22:00:37 +05:30
2018-05-09 12:01:36 +05:30
loadAwardsHandler()
2021-03-08 18:12:59 +05:30
.then((awardsHandler) => {
2018-11-08 19:23:39 +05:30
awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
2018-05-09 12:01:36 +05:30
awardsHandler.scrollToAwards();
})
.catch(() => {
// ignore
});
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
setupNewNote($note) {
// Update datetime format on the recent note
2021-09-30 23:02:18 +05:30
localTimeAgo($note.find('.js-timeago').get(), false);
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
this.collapseLongCommitList();
this.taskList.init();
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// 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;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
onHashChange() {
if (this.$noteToCleanHighlight) {
Notes.updateNoteTargetSelector(this.$noteToCleanHighlight);
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
this.$noteToCleanHighlight = null;
}
static updateNoteTargetSelector($note) {
2018-03-17 18:26:18 +05:30
const hash = getLocationHash();
2017-09-10 17:25:29 +05:30
// 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);
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
if (!noteEntity.valid) {
2018-03-27 19:54:05 +05:30
if (noteEntity.errors && noteEntity.errors.commands_only) {
2018-11-08 19:23:39 +05:30
if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
2017-09-10 17:25:29 +05:30
$notesList.find('.system-note.being-posted').remove();
2017-08-17 22:00:37 +05:30
}
2023-03-04 22:38:38 +05:30
this.addAlert({
2021-04-29 21:17:54 +05:30
message: noteEntity.errors.commands_only,
2023-03-04 22:38:38 +05:30
variant: VARIANT_INFO,
2021-04-29 21:17:54 +05:30
parent: this.parentTimeline.get(0),
});
2017-09-10 17:25:29 +05:30
this.refresh();
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
return;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (Notes.isNewNote(noteEntity, this.note_ids)) {
2018-11-08 19:23:39 +05:30
if (isInMRPage()) {
2018-03-27 19:54:05 +05:30
return;
}
2017-08-17 22:00:37 +05:30
this.note_ids.push(noteEntity.id);
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
if ($notesList.length) {
$notesList.find('.system-note.being-posted').remove();
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
this.setupNewNote($newNote);
this.refresh();
2016-09-13 17:45:13 +05:30
return this.updateNotesCount(1);
2018-05-09 12:01:36 +05:30
} 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.
2017-09-10 17:25:29 +05:30
const isEditing = $note.hasClass('is-editing');
2021-03-08 18:12:59 +05:30
const initialContent = normalizeNewlines($note.find('.original-note-content').text().trim());
2017-09-10 17:25:29 +05:30
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);
2018-05-09 12:01:36 +05:30
const isTextareaUntouched =
2018-11-08 19:23:39 +05:30
currentContent === initialContent || currentContent === sanitizedNoteNote;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
2018-05-09 12:01:36 +05:30
} else if (isEditing && !isTextareaUntouched) {
2017-09-10 17:25:29 +05:30
this.putConflictEditWarningInPlace(noteEntity, $note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
2018-05-09 12:01:36 +05:30
} else {
2017-09-10 17:25:29 +05:30
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
this.setupNewNote($updatedNote);
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
}
isParallelView() {
2022-04-04 11:22:00 +05:30
return getCookie('diff_view') === 'parallel';
2017-09-10 17:25:29 +05:30
}
/**
2018-03-27 19:54:05 +05:30
* Render note in discussion area. To render inline notes use renderDiscussionNote.
2017-09-10 17:25:29 +05:30
*/
renderDiscussionNote(noteEntity, $form) {
2020-01-01 13:55:28 +05:30
let discussionContainer;
let row;
2018-03-27 19:54:05 +05:30
2017-09-10 17:25:29 +05:30
if (!Notes.isNewNote(noteEntity, this.note_ids)) {
return;
}
this.note_ids.push(noteEntity.id);
2018-03-17 18:26:18 +05:30
2020-01-01 13:55:28 +05:30
const form =
$form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
2018-05-09 12:01:36 +05:30
row =
form.length || !noteEntity.discussion_line_code
? form.closest('tr')
: $(`#${noteEntity.discussion_line_code}`);
2018-03-17 18:26:18 +05:30
if (noteEntity.on_image) {
row = form;
}
2017-09-10 17:25:29 +05:30
// is this the first note of discussion?
2018-11-08 19:23:39 +05:30
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
2017-09-10 17:25:29 +05:30
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
if (discussionContainer.length === 0) {
if (noteEntity.diff_discussion_html) {
2023-03-17 16:20:25 +05:30
const discussionElement = document.createElement('table');
2023-04-23 21:23:45 +05:30
let internalNote;
let discussionDOM;
if (!noteEntity.on_image) {
/*
DOMPurify will strip table-less <tr>/<td>, so to get it to stop deleting
nodes (since our note HTML starts with a table-less <tr>), we need to wrap
the noteEntity discussion HTML in a <table> to perform the other
sanitization.
*/
internalNote = sanitize(`<table>${noteEntity.diff_discussion_html}</table>`, {
RETURN_DOM: true,
});
/*
Since we wrapped the <tr> in a <table>, we need to extract the <tr> back out.
DOMPurify returns a Body Element, so we have to start there, then get the
wrapping table, and then get the content we actually want.
Curiously, DOMPurify **ADDS** a totally novel <tbody>, so we're actually
inserting a completely as-yet-unseen <tbody> element here.
*/
discussionDOM = internalNote.querySelector('table').firstChild;
} else {
// Image comments don't need <table> manipulation, they're already <div>s
internalNote = sanitize(noteEntity.diff_discussion_html, {
RETURN_DOM: true,
});
discussionDOM = internalNote.firstChild;
}
discussionElement.insertAdjacentElement('afterbegin', discussionDOM);
2023-03-17 16:20:25 +05:30
renderGFM(discussionElement);
const $discussion = $(discussionElement).unwrap();
2016-09-13 17:45:13 +05:30
2018-11-08 19:23:39 +05:30
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
2017-09-10 17:25:29 +05:30
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
// Merge new discussion HTML in
2020-01-01 13:55:28 +05:30
const $notes = $discussion.find(
`.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
);
const contentContainerClass = $notes
2019-12-21 20:55:43 +05:30
.closest('.notes-content')
.attr('class')
.split(' ')
.join('.');
2018-05-09 12:01:36 +05:30
row
2019-12-21 20:55:43 +05:30
.find(`.${contentContainerClass} .content`)
2018-05-09 12:01:36 +05:30
.append($notes.closest('.content').children());
2016-09-13 17:45:13 +05:30
}
2018-11-08 19:23:39 +05:30
} else {
Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
} else {
// append new note to all matching discussions
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
2016-09-13 17:45:13 +05:30
2021-09-30 23:02:18 +05:30
localTimeAgo(document.querySelectorAll('.js-timeago'), false);
2017-09-10 17:25:29 +05:30
Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
getLineHolder(changesDiscussionContainer) {
2018-05-09 12:01:36 +05:30
return $(changesDiscussionContainer)
.closest('.notes_holder')
2017-09-10 17:25:29 +05:30
.prevAll('.line_holder')
.first()
.get(0);
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
/**
* Called in response the main target form has been successfully submitted.
*
* Removes any errors.
* Resets text and preview.
* Resets buttons.
*/
resetMainTargetForm(e) {
2020-01-01 13:55:28 +05:30
const form = $('.js-main-target-form');
2017-09-10 17:25:29 +05:30
// remove validation errors
form.find('.js-errors').remove();
// reset text and preview
form.find('.js-md-write-button').click();
2021-03-08 18:12:59 +05:30
form.find('.js-note-text').val('').trigger('input');
2023-03-17 16:20:25 +05:30
form.find('.js-note-text').each(function reset() {
this.$autosave.reset();
});
2017-09-10 17:25:29 +05:30
2020-01-01 13:55:28 +05:30
const event = document.createEvent('Event');
2017-09-10 17:25:29 +05:30
event.initEvent('autosize:update', true, false);
form.find('.js-autosize')[0].dispatchEvent(event);
this.updateTargetButtons(e);
}
reenableTargetFormSubmitButton() {
2020-01-01 13:55:28 +05:30
const form = $('.js-main-target-form');
2017-09-10 17:25:29 +05:30
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.
*/
2018-11-08 19:23:39 +05:30
setupMainTargetNoteForm(enableGFM) {
2017-09-10 17:25:29 +05:30
// find the form
2020-01-01 13:55:28 +05:30
const form = $('.js-new-note-form');
2017-09-10 17:25:29 +05:30
// Set a global clone of the form for later cloning
this.formClone = form.clone();
// show the form
2018-11-08 19:23:39 +05:30
this.setupNoteForm(form, enableGFM);
2017-09-10 17:25:29 +05:30
// 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
2018-12-05 23:21:45 +05:30
* set up GFM auto complete
2017-09-10 17:25:29 +05:30
* show the form
*/
2018-11-08 19:23:39 +05:30
setupNoteForm(form, enableGFM = defaultAutocompleteConfig) {
this.glForm = new GLForm(form, enableGFM);
2020-01-01 13:55:28 +05:30
const textarea = form.find('.js-note-text');
const key = [
2019-09-04 21:01:54 +05:30
s__('NoteForm|Note'),
2017-09-10 17:25:29 +05:30
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(),
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// LegacyDiffNote
form.find('#note_line_code').val(),
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
// DiffNote
2018-03-17 18:26:18 +05:30
form.find('#note_position').val(),
2017-09-10 17:25:29 +05:30
];
2023-03-17 16:20:25 +05:30
const textareaEl = textarea.get(0);
// eslint-disable-next-line no-new
if (textareaEl) new Autosave(textareaEl, key);
2017-09-10 17:25:29 +05:30
}
/**
* 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');
}
2023-03-04 22:38:38 +05:30
return this.addAlert({
2021-04-29 21:17:54 +05:30
message: __(
2019-09-04 21:01:54 +05:30
'Your comment could not be submitted! Please check your network connection and try again.',
),
2021-04-29 21:17:54 +05:30
parent: formParentTimeline.get(0),
});
2017-09-10 17:25:29 +05:30
}
2019-12-04 20:38:33 +05:30
updateNoteError() {
2023-03-04 22:38:38 +05:30
createAlert({
2021-04-29 21:17:54 +05:30
message: __(
'Your comment could not be updated! Please check your network connection and try again.',
),
});
2017-09-10 17:25:29 +05:30
}
/**
* 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) {
2020-01-01 13:55:28 +05:30
const discussionId = $form.data('discussionId');
const mergeRequestId = $form.data('noteableIid');
2017-09-10 17:25:29 +05:30
if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
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
2020-01-01 13:55:28 +05:30
const $noteEntityEl = $(noteEntity.html);
2021-11-11 11:23:49 +05:30
const $noteAvatar = $noteEntityEl.find('.image-diff-avatar-link');
2022-04-04 11:22:00 +05:30
const $targetNoteBadge = $targetNote.find('.design-note-pin');
2021-11-11 11:23:49 +05:30
$noteAvatar.append($targetNoteBadge);
2017-09-10 17:25:29 +05:30
this.revertNoteEditForm($targetNote);
2023-03-17 16:20:25 +05:30
renderGFM($noteEntityEl.get(0));
2017-09-10 17:25:29 +05:30
// Find the note's `li` element by ID and replace it with the updated HTML
2020-01-01 13:55:28 +05:30
const $note_li = $(`.note-row-${noteEntity.id}`);
2017-09-10 17:25:29 +05:30
$note_li.replaceWith($noteEntityEl);
this.setupNewNote($noteEntityEl);
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
checkContentToAllowEditing($el) {
2021-03-08 18:12:59 +05:30
const initialContent = $el.find('.original-note-content').text().trim();
2020-01-01 13:55:28 +05:30
const currentContent = $el.find('.js-note-text').val();
let isAllowed = true;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (currentContent === initialContent) {
this.removeNoteEditForm($el);
2018-05-09 12:01:36 +05:30
} else {
2020-01-01 13:55:28 +05:30
const isWidgetVisible = isInViewport($el.get(0));
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (!isWidgetVisible) {
2018-03-17 18:26:18 +05:30
scrollToElement($el);
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
$el.find('.js-finish-edit-warning').show();
isAllowed = false;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
return isAllowed;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
/**
* 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
*/
2019-12-04 20:38:33 +05:30
showEditForm(e) {
2017-09-10 17:25:29 +05:30
e.preventDefault();
2016-09-13 17:45:13 +05:30
2020-01-01 13:55:28 +05:30
const $target = $(e.target);
const $editForm = $(this.getEditFormSelector($target));
const $note = $target.closest('.note');
const $currentlyEditing = $('.note.is-editing:visible');
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ($currentlyEditing.length) {
2020-01-01 13:55:28 +05:30
const isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (!isEditAllowed) {
return;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
$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];
2018-05-09 12:01:36 +05:30
} else {
2017-09-10 17:25:29 +05:30
$note.find('.js-finish-edit-warning').hide();
this.removeNoteEditForm($note);
}
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
revertNoteEditForm($target) {
$target = $target || $('.note.is-editing:visible');
2020-01-01 13:55:28 +05:30
const selector = this.getEditFormSelector($target);
const $editForm = $(selector);
2017-08-17 22:00:37 +05:30
2018-03-27 19:54:05 +05:30
$editForm.insertBefore('.diffs');
2017-09-10 17:25:29 +05:30
$editForm.find('.js-comment-save-button').enable();
$editForm.find('.js-finish-edit-warning').hide();
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
getEditFormSelector($el) {
2020-01-01 13:55:28 +05:30
let selector = '.note-edit-form:not(.mr-note-edit-form)';
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
if ($el.parents('#diffs').length) {
selector = '.note-edit-form.mr-note-edit-form';
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
return selector;
}
removeNoteEditForm($note) {
2020-01-01 13:55:28 +05:30
const form = $note.find('.diffs .current-note-edit-form');
2018-03-27 19:54:05 +05:30
2017-09-10 17:25:29 +05:30
$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.
2018-11-08 19:23:39 +05:30
return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote'));
2017-09-10 17:25:29 +05:30
}
/**
* 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) {
2020-01-01 13:55:28 +05:30
const $note = $(e.currentTarget).closest('.note');
2018-05-09 12:01:36 +05:30
2022-06-21 17:19:12 +05:30
$note.one('ajax:complete', () => {
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();
2016-09-13 17:45:13 +05:30
}
2019-12-26 22:10:19 +05:30
}
2022-06-21 17:19:12 +05:30
});
2017-09-10 17:25:29 +05:30
2022-06-21 17:19:12 +05:30
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
});
2017-09-10 17:25:29 +05:30
}
/**
* 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();
2018-11-08 19:23:39 +05:30
return $note.find('.diffs .current-note-edit-form').remove();
2017-09-10 17:25:29 +05:30
}
/**
* 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) {
2020-01-01 13:55:28 +05:30
const form = this.cleanForm(this.formClone.clone());
const replyLink = $(target).closest('.js-discussion-reply-button');
2017-09-10 17:25:29 +05:30
// insert the form after the button
2021-03-08 18:12:59 +05:30
replyLink.closest('.discussion-reply-holder').hide().after(form);
2017-09-10 17:25:29 +05:30
// 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) {
2018-12-05 23:21:45 +05:30
// set up note target
2018-03-17 18:26:18 +05:30
let diffFileData = dataHolder.closest('.text-file');
if (diffFileData.length === 0) {
diffFileData = dataHolder.closest('.image');
}
2017-09-10 17:25:29 +05:30
2020-01-01 13:55:28 +05:30
const discussionID = dataHolder.data('discussionId');
2017-09-10 17:25:29 +05:30
if (discussionID) {
form.attr('data-discussion-id', discussionID);
form.find('#in_reply_to_discussion_id').val(discussionID);
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form.find('#note_project_id').val(dataHolder.data('discussionProjectId'));
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType'));
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
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'));
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form.find('#note_type').val(dataHolder.data('noteType'));
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
// LegacyDiffNote
form.find('#note_line_code').val(dataHolder.data('lineCode'));
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
// DiffNote
form.find('#note_position').val(dataHolder.attr('data-position'));
2017-08-17 22:00:37 +05:30
2021-03-11 19:13:27 +05:30
form.append('</div>').find('.js-close-discussion-note-form').show().removeClass('hide');
2017-09-10 17:25:29 +05:30
form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
2016-09-13 17:45:13 +05:30
2018-11-08 19:23:39 +05:30
form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form.find('.js-note-text').focus();
2018-11-08 19:23:39 +05:30
form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
2017-09-10 17:25:29 +05:30
}
/**
* 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,
2018-05-09 12:01:36 +05:30
showReplyInput,
2019-09-04 21:01:54 +05:30
currentUsername: gon.current_username,
currentUserAvatar: gon.current_user_avatar_url,
currentUserFullname: gon.current_user_fullname,
2017-09-10 17:25:29 +05:30
});
}
2018-03-17 18:26:18 +05:30
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);
2018-12-05 23:21:45 +05:30
// Set up comment form
2018-03-17 18:26:18 +05:30
let newForm;
2018-11-08 19:23:39 +05:30
const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
2018-03-17 18:26:18 +05:30
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);
}
2019-12-04 20:38:33 +05:30
toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
2020-01-01 13:55:28 +05:30
let addForm;
let newForm;
let noteForm;
let replyButton;
let rowCssToAdd;
const $link = $(target);
const row = $link.closest('tr');
2017-09-10 17:25:29 +05:30
const nextRow = row.next();
let targetRow = row;
if (nextRow.is('.notes_holder')) {
targetRow = nextRow;
}
2017-08-17 22:00:37 +05:30
2020-01-01 13:55:28 +05:30
const hasNotes = nextRow.is('.notes_holder');
2017-09-10 17:25:29 +05:30
addForm = false;
let lineTypeSelector = '';
2018-05-09 12:01:36 +05:30
rowCssToAdd =
2019-07-31 22:56:46 +05:30
'<tr class="notes_holder js-temp-notes-holder"><td class="notes-content" colspan="3"><div class="content"></div></td></tr>';
2017-09-10 17:25:29 +05:30
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
2018-05-09 12:01:36 +05:30
rowCssToAdd =
2019-07-31 22:56:46 +05:30
'<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>';
2017-09-10 17:25:29 +05:30
}
2019-07-31 22:56:46 +05:30
const notesContentSelector = `.notes-content${lineTypeSelector} .content`;
2017-09-10 17:25:29 +05:30
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;
}
}
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
} else if (showReplyInput) {
// add a notes row and insert the form
row.after(rowCssToAdd);
targetRow = row.next();
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
2018-11-08 19:23:39 +05:30
const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
2017-09-10 17:25:29 +05:30
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
2018-11-08 19:23:39 +05:30
targetRow.toggleClass('hide', !showNow);
notesContent.toggleClass('hide', !showNow);
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
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) {
2020-01-01 13:55:28 +05:30
const row = form.closest('tr');
const glForm = form.data('glForm');
2017-09-10 17:25:29 +05:30
glForm.destroy();
2023-03-17 16:20:25 +05:30
form.find('.js-note-text').each(function reset() {
this.$autosave.reset();
});
2018-05-09 12:01:36 +05:30
// show the reply button (will only work for replies)
form.prev('.discussion-reply-holder').show();
2017-09-10 17:25:29 +05:30
if (row.is('.js-temp-notes-holder')) {
// remove temporary row for diff lines
return row.remove();
}
2020-05-24 23:13:21 +05:30
// only remove the form
return form.remove();
2017-09-10 17:25:29 +05:30
}
cancelDiscussionForm(e) {
e.preventDefault();
2018-03-17 18:26:18 +05:30
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);
2017-09-10 17:25:29 +05:30
}
/**
* Called after an attachment file has been selected.
*
* Updates the file name for the selected attachment.
*/
updateFormAttachment() {
2020-01-01 13:55:28 +05:30
const form = $(this).closest('form');
2017-09-10 17:25:29 +05:30
// get only the basename
2020-01-01 13:55:28 +05:30
const filename = $(this)
2018-05-09 12:01:36 +05:30
.val()
.replace(/^.*[\\\/]/, '');
2017-09-10 17:25:29 +05:30
return form.find('.js-attachment-filename').text(filename);
}
/**
* Called when the tab visibility changes
*/
visibilityChange() {
return this.refresh();
}
updateTargetButtons(e) {
2020-01-01 13:55:28 +05:30
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');
2021-11-18 22:05:49 +05:30
const commentTypeComponent = form.get(0)?.commentTypeComponent;
2017-09-10 17:25:29 +05:30
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);
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if (closebtn.text() !== closetext) {
closebtn.text(closetext);
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
if (reopenbtn.is(':not(.btn-comment-and-reopen)')) {
reopenbtn.addClass('btn-comment-and-reopen');
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if (closebtn.is(':not(.btn-comment-and-close)')) {
closebtn.addClass('btn-comment-and-close');
2016-09-13 17:45:13 +05:30
}
2021-11-18 22:05:49 +05:30
if (commentTypeComponent) {
commentTypeComponent.disabled = false;
}
2017-09-10 17:25:29 +05:30
} else {
2018-03-27 19:54:05 +05:30
reopentext = reopenbtn.data('originalText');
closetext = closebtn.data('originalText');
2017-09-10 17:25:29 +05:30
if (reopenbtn.text() !== reopentext) {
reopenbtn.text(reopentext);
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if (closebtn.text() !== closetext) {
closebtn.text(closetext);
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if (reopenbtn.is('.btn-comment-and-reopen')) {
reopenbtn.removeClass('btn-comment-and-reopen');
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
if (closebtn.is('.btn-comment-and-close')) {
closebtn.removeClass('btn-comment-and-close');
}
2021-11-18 22:05:49 +05:30
if (commentTypeComponent) {
commentTypeComponent.disabled = true;
}
2017-09-10 17:25:29 +05:30
}
}
putEditFormInPlace($el) {
2020-01-01 13:55:28 +05:30
const $editForm = $(this.getEditFormSelector($el));
const $note = $el.closest('.note');
2017-09-10 17:25:29 +05:30
$editForm.insertAfter($note.find('.note-text'));
2020-01-01 13:55:28 +05:30
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');
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
2017-09-10 17:25:29 +05:30
2021-03-08 18:12:59 +05:30
$editForm.find('form').attr('action', `${postUrl}?html=true`).attr('data-remote', 'true');
2017-09-10 17:25:29 +05:30
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
2021-03-08 18:12:59 +05:30
$editForm.find('.js-note-text').focus().val(originalContent);
2017-09-10 17:25:29 +05:30
$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) {
2019-09-30 21:07:59 +05:30
const open_link = `<a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">`;
2017-09-10 17:25:29 +05:30
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
2019-09-04 21:01:54 +05:30
${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>',
},
)}
2017-09-10 17:25:29 +05:30
</div>`);
$alert.insertAfter($note.find('.note-text'));
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
updateNotesCount(updateCount) {
2018-11-08 19:23:39 +05:30
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
2018-05-09 12:01:36 +05:30
}
static renderPlaceholderComponent($container) {
const el = $container.find('.js-code-placeholder').get(0);
2018-11-08 19:23:39 +05:30
// eslint-disable-next-line no-new
2018-05-09 12:01:36 +05:30
new Vue({
el,
components: {
2022-08-13 15:12:31 +05:30
GlSkeletonLoader,
2018-05-09 12:01:36 +05:30
},
render(createElement) {
2022-08-13 15:12:31 +05:30
return createElement('gl-skeleton-loader');
2018-05-09 12:01:36 +05:30
},
});
}
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);
2018-11-20 20:47:30 +05:30
return this.loadLazyDiff(e).then(() => {
2018-05-09 12:01:36 +05:30
$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
2018-11-20 20:47:30 +05:30
.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');
});
2018-05-09 12:01:36 +05:30
}
return Promise.resolve();
2017-09-10 17:25:29 +05:30
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
toggleCommitList(e) {
const $element = $(e.currentTarget);
2018-11-08 19:23:39 +05:30
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
2020-10-24 23:57:45 +05:30
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');
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
$closestSystemCommitList.toggleClass('hide-shade');
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
/**
* Scans system notes with `ul` elements in system note body
* then collapse long commit list pushed by user to make it less
* intrusive.
*/
collapseLongCommitList() {
2021-03-08 18:12:59 +05:30
const systemNotes = $('#notes-list').find('li.system-note').has('ul');
2017-08-17 22:00:37 +05:30
2019-12-21 20:55:43 +05:30
$.each(systemNotes, (index, systemNote) => {
2017-09-10 17:25:29 +05:30
const $systemNote = $(systemNote);
2018-05-09 12:01:36 +05:30
const headerMessage = $systemNote
.find('.note-text')
2020-03-13 15:44:24 +05:30
.find('p')
.first()
2018-05-09 12:01:36 +05:30
.text()
.replace(':', '');
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
$systemNote.find('.note-header .system-note-message').html(headerMessage);
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
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 {
2018-11-08 19:23:39 +05:30
$systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
});
}
2017-08-17 22:00:37 +05:30
2023-03-04 22:38:38 +05:30
addAlert(...alertParams) {
this.alert = createAlert(...alertParams);
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2023-03-04 22:38:38 +05:30
clearAlert() {
this.alert?.dismiss();
2017-09-10 17:25:29 +05:30
}
cleanForm($form) {
// Remove dropdown
2018-05-09 12:01:36 +05:30
$form.find('.dropdown-menu').remove();
2017-09-10 17:25:29 +05:30
return $form;
}
/**
2020-05-24 23:13:21 +05:30
* Check if note does not exist on page
2017-09-10 17:25:29 +05:30
*/
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(
2021-03-08 18:12:59 +05:30
$note.find('.original-note-content').first().text().trim(),
2017-09-10 17:25:29 +05:30
);
return sanitizedNoteEntityText !== currentNoteText;
}
static checkMergeRequestStatus() {
2018-03-17 18:26:18 +05:30
if (getPagePath(1) === 'merge_requests' && gl.mrWidget) {
2017-09-10 17:25:29 +05:30
gl.mrWidget.checkStatus();
}
}
static animateAppendNote(noteHtml, $notesList) {
const $note = $(noteHtml);
2023-03-17 16:20:25 +05:30
$note.addClass('fade-in-full');
renderGFM($note.get(0));
2017-09-10 17:25:29 +05:30
$notesList.append($note);
return $note;
}
static animateUpdateNote(noteHtml, $note) {
const $updatedNote = $(noteHtml);
2023-03-17 16:20:25 +05:30
$updatedNote.addClass('fade-in');
renderGFM($updatedNote.get(0));
2017-09-10 17:25:29 +05:30
$note.replaceWith($updatedNote);
return $updatedNote;
}
/**
* Get data from Form attributes to use for saving/submitting comment.
*/
getFormData($form) {
2018-03-17 18:26:18 +05:30
const content = $form.find('.js-note-text').val();
2017-09-10 17:25:29 +05:30
return {
2019-12-21 20:55:43 +05:30
// eslint-disable-next-line no-jquery/no-serialize
2017-09-10 17:25:29 +05:30
formData: $form.serialize(),
2020-03-13 15:44:24 +05:30
formContent: escape(content),
2017-09-10 17:25:29 +05:30
formAction: $form.attr('action'),
2018-03-17 18:26:18 +05:30
formContentOriginal: content,
2017-08-17 22:00:37 +05:30
};
2017-09-10 17:25:29 +05:30
}
/**
* 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`
2021-03-08 18:12:59 +05:30
const executedCommands = availableQuickActions.filter((command) => {
2017-09-10 17:25:29 +05:30
const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(formContent);
});
if (executedCommands && executedCommands.length) {
if (executedCommands.length > 1) {
2019-09-04 21:01:54 +05:30
tempFormContent = __('Applying multiple commands');
2017-09-10 17:25:29 +05:30
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
2019-09-04 21:01:54 +05:30
tempFormContent = sprintf(__('Applying command to %{commandDescription}'), {
commandDescription,
});
2017-09-10 17:25:29 +05:30
}
} else {
2019-09-04 21:01:54 +05:30
tempFormContent = __('Applying command');
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
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.
*/
2018-05-09 12:01:36 +05:30
createPlaceholderNote({
formContent,
uniqueId,
isDiscussionNote,
currentUsername,
currentUserFullname,
currentUserAvatar,
}) {
2017-09-10 17:25:29 +05:30
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">
2020-03-13 15:44:24 +05:30
<a href="/${escape(currentUsername)}">
2017-09-10 17:25:29 +05:30
<img class="avatar s40" src="${currentUserAvatar}" />
</a>
</div>
<div class="timeline-content ${discussionClass}">
<div class="note-header">
<div class="note-header-info">
2020-03-13 15:44:24 +05:30
<a href="/${escape(currentUsername)}">
<span class="d-none d-sm-inline-block bold">${escape(currentUsername)}</span>
<span class="note-headline-light">${escape(currentUsername)}</span>
2017-09-10 17:25:29 +05:30
</a>
</div>
</div>
<div class="note-body">
<div class="note-text">
<p>${formContent}</p>
2017-08-17 22:00:37 +05:30
</div>
2017-09-10 17:25:29 +05:30
</div>
</div>
</div>
2018-05-09 12:01:36 +05:30
</li>`,
2017-09-10 17:25:29 +05:30
);
2020-03-13 15:44:24 +05:30
$tempNote.find('.d-none.d-sm-inline-block').text(escape(currentUserFullname));
$tempNote.find('.note-headline-light').text(`@${escape(currentUsername)}`);
2017-09-10 17:25:29 +05:30
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>
2017-08-17 22:00:37 +05:30
</div>
2017-09-10 17:25:29 +05:30
</div>
2018-05-09 12:01:36 +05:30
</li>`,
2017-09-10 17:25:29 +05:30
);
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
2018-03-17 18:26:18 +05:30
* 5) Perform network request to submit the note using `axios.post`
2017-09-10 17:25:29 +05:30
* 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
2023-03-04 22:38:38 +05:30
* 2. Show error alert message about failure
2017-09-10 17:25:29 +05:30
*/
postComment(e) {
e.preventDefault();
// Get Form metadata
const $submitBtn = $(e.target);
2018-05-09 12:01:36 +05:30
$submitBtn.prop('disabled', true);
2017-09-10 17:25:29 +05:30
let $form = $submitBtn.parents('form');
2021-11-18 22:05:49 +05:30
const commentTypeComponent = $form.get(0)?.commentTypeComponent;
if (commentTypeComponent) commentTypeComponent.disabled = true;
2017-09-10 17:25:29 +05:30
const $closeBtn = $form.find('.js-note-target-close');
2018-05-09 12:01:36 +05:30
const isDiscussionNote =
2021-03-08 18:12:59 +05:30
$submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion';
2017-09-10 17:25:29 +05:30
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
2018-11-08 19:23:39 +05:30
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
2017-09-10 17:25:29 +05:30
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');
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// If comment is to resolve discussion, disable submit buttons while
// comment posting is finished.
if (isDiscussionResolve) {
$form.find('.js-comment-submit-button').disable();
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
tempFormContent = formContent;
2020-07-28 23:09:34 +05:30
if (this.glForm.supportsQuickActions && this.hasQuickActions(formContent)) {
2017-09-10 17:25:29 +05:30
tempFormContent = this.stripQuickActions(formContent);
hasQuickActions = true;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Show placeholder note
if (tempFormContent) {
2020-03-13 15:44:24 +05:30
noteUniqueId = uniqueId('tempNote_');
2018-05-09 12:01:36 +05:30
$notesContainer.append(
this.createPlaceholderNote({
formContent: tempFormContent,
uniqueId: noteUniqueId,
isDiscussionNote,
currentUsername: gon.current_username,
currentUserFullname: gon.current_user_fullname,
currentUserAvatar: gon.current_user_avatar_url,
}),
);
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Show placeholder system note
if (hasQuickActions) {
2020-03-13 15:44:24 +05:30
systemNoteUniqueId = uniqueId('tempSystemNote_');
2018-05-09 12:01:36 +05:30
$notesContainer.append(
this.createPlaceholderSystemNote({
formContent: this.getQuickActionDescription(
formContent,
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands),
),
uniqueId: systemNoteUniqueId,
}),
);
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Clear the form textarea
if ($notesContainer.length) {
if (isMainForm) {
this.resetMainTargetForm(e);
} else if (isDiscussionForm) {
this.removeDiscussionNoteForm($form);
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2018-05-09 12:01:36 +05:30
$closeBtn.text($closeBtn.data('originalText'));
2017-09-10 17:25:29 +05:30
// Make request to submit comment on server
2018-05-09 12:01:36 +05:30
return axios
.post(`${formAction}?html=true`, formData)
2021-03-08 18:12:59 +05:30
.then((res) => {
2018-03-17 18:26:18 +05:30
const note = res.data;
2018-05-09 12:01:36 +05:30
$submitBtn.prop('disabled', false);
2021-11-18 22:05:49 +05:30
if (commentTypeComponent) commentTypeComponent.disabled = false;
2017-09-10 17:25:29 +05:30
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
2018-03-17 18:26:18 +05:30
const $diffFile = $form.closest('.diff-file');
if ($diffFile.length > 0) {
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
$diffFile[0].dispatchEvent(blurEvent);
}
2017-09-10 17:25:29 +05:30
// Reset cached commands list when command is applied
if (hasQuickActions) {
2018-11-08 19:23:39 +05:30
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
// Clear previous form errors
2023-03-04 22:38:38 +05:30
this.clearAlertWrapper();
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Check if this was discussion comment
if (isDiscussionForm) {
// Remove flash-container
$notesContainer.find('.flash-container').remove();
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// If comment intends to resolve discussion, do the same.
if (isDiscussionResolve) {
$form
2018-03-27 19:54:05 +05:30
.attr('data-discussion-id', $submitBtn.data('discussionId'))
2017-09-10 17:25:29 +05:30
.attr('data-resolve-all', 'true')
2018-03-27 19:54:05 +05:30
.attr('data-project-path', $submitBtn.data('projectPath'));
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
// Show final note element on UI
2018-03-17 18:26:18 +05:30
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);
}
}
2017-09-10 17:25:29 +05:30
// append flash-container to the Notes list
if ($notesContainer.length) {
2018-11-08 19:23:39 +05:30
$notesContainer.append('<div class="flash-container" style="display: none;"></div>');
2017-08-17 22:00:37 +05:30
}
2018-05-09 12:01:36 +05:30
} else if (isMainForm) {
// Check if this was main thread comment
2017-09-10 17:25:29 +05:30
// Show final note element on UI and perform form and action buttons cleanup
this.addNote($form, note);
this.reenableTargetFormSubmitButton(e);
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if (note.commands_changes) {
this.handleQuickActions(note);
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
$form.trigger('ajax:success', [note]);
2018-05-09 12:01:36 +05:30
})
.catch(() => {
2017-09-10 17:25:29 +05:30
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
2018-05-09 12:01:36 +05:30
$submitBtn.prop('disabled', false);
2021-11-18 22:05:49 +05:30
if (commentTypeComponent) commentTypeComponent.disabled = false;
2018-03-17 18:26:18 +05:30
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
const closestDiffFile = $form.closest('.diff-file');
if (closestDiffFile.length) {
closestDiffFile[0].dispatchEvent(blurEvent);
}
2017-09-10 17:25:29 +05:30
if (hasQuickActions) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove();
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
2018-11-08 19:23:39 +05:30
const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
2017-09-10 17:25:29 +05:30
this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
$form.find('.js-note-text').val(formContentOriginal);
2017-09-10 17:25:29 +05:30
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
2018-03-17 18:26:18 +05:30
* 3) Perform network request to submit the updated note using `axios.post`
2017-09-10 17:25:29 +05:30
* 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);
2018-11-08 19:23:39 +05:30
$editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
2022-05-07 20:08:51 +05:30
const $timeAgo = $editingNote.find('.note-headline-meta a');
$timeAgo.empty();
$timeAgo.append(loadingIconForLegacyJS({ inline: true, size: 'sm' }));
2017-09-10 17:25:29 +05:30
// Make request to update comment on server
2018-05-09 12:01:36 +05:30
axios
.post(`${formAction}?html=true`, formData)
2018-03-17 18:26:18 +05:30
.then(({ data }) => {
2017-09-10 17:25:29 +05:30
// Submission successful! render final note element
2018-03-17 18:26:18 +05:30
this.updateNote(data, $editingNote);
2017-09-10 17:25:29 +05:30
})
2018-03-17 18:26:18 +05:30
.catch(() => {
2017-09-10 17:25:29 +05:30
// Submission failed, revert back to original note
2020-03-13 15:44:24 +05:30
$noteBodyText.html(escape(cachedNoteBodyText));
2017-09-10 17:25:29 +05:30
$editingNote.removeClass('being-posted fade-in');
2021-06-08 01:23:25 +05:30
$editingNote.find('.gl-spinner').remove();
2017-09-10 17:25:29 +05:30
// Show Flash message about failure
this.updateNoteError();
});
2018-03-27 19:54:05 +05:30
return $closeBtn.text($closeBtn.data('originalText'));
2017-09-10 17:25:29 +05:30
}
}