96e6fc70b4
Removes set favicon related methods from global scope Improves test related with favicon Removes convertPermissionToBoolean from global scope. Adds tests for convertPermissionToBoolean - were non existant Removes setParamInURL from gl.utils Removes parseIntPagination from gl.utils namespace Remove normalizeCRLFHeaders from gl.utils namespace Removes normalizeHeaders from gl.utils namespace Use gl.utils for filtered search Fix bad import Fix broken test by cleaning window.history namespace Adds changelog
1533 lines
52 KiB
JavaScript
1533 lines
52 KiB
JavaScript
/* eslint-disable no-restricted-properties, func-names, space-before-function-paren,
|
|
no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase,
|
|
no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line,
|
|
default-case, prefer-template, consistent-return, no-alert, no-return-assign,
|
|
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
|
|
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
|
|
newline-per-chained-call, no-useless-escape, class-methods-use-this */
|
|
/* global Flash */
|
|
/* global Autosave */
|
|
/* global ResolveService */
|
|
/* global mrRefreshWidgetUrl */
|
|
|
|
import $ from 'jquery';
|
|
import _ from 'underscore';
|
|
import Cookies from 'js-cookie';
|
|
import autosize from 'vendor/autosize';
|
|
import Dropzone from 'dropzone';
|
|
import 'vendor/jquery.caret'; // required by jquery.atwho
|
|
import 'vendor/jquery.atwho';
|
|
import AjaxCache from '~/lib/utils/ajax_cache';
|
|
import CommentTypeToggle from './comment_type_toggle';
|
|
import loadAwardsHandler from './awards_handler';
|
|
import './autosave';
|
|
import './dropzone_input';
|
|
import TaskList from './task_list';
|
|
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
|
|
|
|
window.autosize = autosize;
|
|
window.Dropzone = Dropzone;
|
|
|
|
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 {
|
|
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
|
|
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.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.cleanBinding();
|
|
this.addBinding();
|
|
this.setPollingInterval();
|
|
this.setupMainTargetNoteForm();
|
|
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');
|
|
}
|
|
}
|
|
|
|
setViewType(view) {
|
|
this.view = Cookies.get('diff_view') || view;
|
|
}
|
|
|
|
addBinding() {
|
|
// Edit note link
|
|
$(document).on('click', '.js-note-edit', this.showEditForm.bind(this));
|
|
$(document).on('click', '.note-edit-cancel', this.cancelEdit);
|
|
// Reopen and close actions for Issue/MR combined with note form submit
|
|
$(document).on('click', '.js-comment-submit-button', this.postComment);
|
|
$(document).on('click', '.js-comment-save-button', this.updateComment);
|
|
$(document).on('keyup input', '.js-note-text', this.updateTargetButtons);
|
|
// resolve a discussion
|
|
$(document).on('click', '.js-comment-resolve-button', this.postComment);
|
|
// remove a note (in general)
|
|
$(document).on('click', '.js-note-delete', this.removeNote);
|
|
// delete note attachment
|
|
$(document).on('click', '.js-note-attachment-delete', this.removeAttachment);
|
|
// reset main target form when clicking discard
|
|
$(document).on('click', '.js-note-discard', this.resetMainTargetForm);
|
|
// update the file name when an attachment is selected
|
|
$(document).on('change', '.js-note-attachment-input', this.updateFormAttachment);
|
|
// reply to diff/discussion notes
|
|
$(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
|
|
// add diff note
|
|
$(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
|
|
// hide diff note form
|
|
$(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
|
|
// toggle commit list
|
|
$(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
|
|
// fetch notes when tab becomes visible
|
|
$(document).on('visibilitychange', this.visibilityChange);
|
|
// when issue status changes, we need to refresh data
|
|
$(document).on('issuable:change', this.refresh);
|
|
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
|
|
$(document).on('ajax:success', '.js-main-target-form', this.addNote);
|
|
$(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
|
|
$(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
|
|
$(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
|
|
// when a key is clicked on the notes
|
|
$(document).on('keydown', '.js-note-text', this.keydownNoteText);
|
|
// When the URL fragment/hash has changed, `#note_xxx`
|
|
return $(window).on('hashchange', this.onHashChange);
|
|
}
|
|
|
|
cleanBinding() {
|
|
$(document).off('click', '.js-note-edit');
|
|
$(document).off('click', '.note-edit-cancel');
|
|
$(document).off('click', '.js-note-delete');
|
|
$(document).off('click', '.js-note-attachment-delete');
|
|
$(document).off('click', '.js-discussion-reply-button');
|
|
$(document).off('click', '.js-add-diff-note-button');
|
|
$(document).off('visibilitychange');
|
|
$(document).off('keyup input', '.js-note-text');
|
|
$(document).off('click', '.js-note-target-reopen');
|
|
$(document).off('click', '.js-note-target-close');
|
|
$(document).off('click', '.js-note-discard');
|
|
$(document).off('keydown', '.js-note-text');
|
|
$(document).off('click', '.js-comment-resolve-button');
|
|
$(document).off('click', '.system-note-commit-list-toggler');
|
|
$(document).off('ajax:success', '.js-main-target-form');
|
|
$(document).off('ajax:success', '.js-discussion-note-form');
|
|
$(document).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) {
|
|
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
|
|
if (isMetaKey(e)) {
|
|
return;
|
|
}
|
|
|
|
$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 (!confirm('Are you sure you want to cancel creating this comment?')) {
|
|
return;
|
|
}
|
|
}
|
|
this.removeDiscussionNoteForm(discussionNoteForm);
|
|
return;
|
|
}
|
|
editNote = $textarea.closest('.note');
|
|
if (editNote.length) {
|
|
originalText = $textarea.closest('form').data('original-note');
|
|
newText = $textarea.val();
|
|
if (originalText !== newText) {
|
|
if (!confirm('Are you sure you want to cancel editing this comment?')) {
|
|
return;
|
|
}
|
|
}
|
|
return this.removeNoteEditForm(editNote);
|
|
}
|
|
}
|
|
}
|
|
|
|
initRefresh() {
|
|
if (Notes.interval) {
|
|
clearInterval(Notes.interval);
|
|
}
|
|
return Notes.interval = setInterval((function(_this) {
|
|
return function() {
|
|
return _this.refresh();
|
|
};
|
|
})(this), this.pollingInterval);
|
|
}
|
|
|
|
refresh() {
|
|
if (!document.hidden) {
|
|
return this.getContent();
|
|
}
|
|
}
|
|
|
|
getContent() {
|
|
if (this.refreshing) {
|
|
return;
|
|
}
|
|
this.refreshing = true;
|
|
return $.ajax({
|
|
url: this.notes_url,
|
|
headers: { 'X-Last-Fetched-At': this.last_fetched_at },
|
|
dataType: 'json',
|
|
success: (function(_this) {
|
|
return function(data) {
|
|
var notes;
|
|
notes = data.notes;
|
|
_this.last_fetched_at = data.last_fetched_at;
|
|
_this.setPollingInterval(data.notes.length);
|
|
return $.each(notes, function(i, note) {
|
|
_this.renderNote(note);
|
|
});
|
|
};
|
|
})(this)
|
|
}).always((function(_this) {
|
|
return function() {
|
|
return _this.refreshing = false;
|
|
};
|
|
})(this));
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
var nthInterval;
|
|
if (shouldReset == null) {
|
|
shouldReset = true;
|
|
}
|
|
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) {
|
|
var 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
|
|
gl.utils.localTimeAgo($note.find('.js-timeago'), 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 = gl.utils.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.commands_only) {
|
|
if (noteEntity.commands_changes &&
|
|
Object.keys(noteEntity.commands_changes).length > 0) {
|
|
$notesList.find('.system-note.being-posted').remove();
|
|
}
|
|
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
|
|
this.refresh();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const $note = $notesList.find(`#note_${noteEntity.id}`);
|
|
if (Notes.isNewNote(noteEntity, this.note_ids)) {
|
|
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);
|
|
}
|
|
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
|
|
else if (Notes.isUpdatedNote(noteEntity, $note)) {
|
|
const isEditing = $note.hasClass('is-editing');
|
|
const 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.
|
|
*
|
|
* Note: for rendering inline notes use renderDiscussionNote
|
|
*/
|
|
renderDiscussionNote(noteEntity, $form) {
|
|
var discussionContainer, form, row, lineType, diffAvatarContainer;
|
|
if (!Notes.isNewNote(noteEntity, this.note_ids)) {
|
|
return;
|
|
}
|
|
this.note_ids.push(noteEntity.id);
|
|
form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
|
|
row = form.closest('tr');
|
|
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
|
|
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
|
|
// is this the first note of discussion?
|
|
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
|
|
if (!discussionContainer.length) {
|
|
discussionContainer = form.closest('.discussion').find('.notes');
|
|
}
|
|
if (discussionContainer.length === 0) {
|
|
if (noteEntity.diff_discussion_html) {
|
|
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
|
|
|
|
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
|
|
// insert the note and the reply button after the temp row
|
|
row.after($discussion);
|
|
} else {
|
|
// Merge new discussion HTML in
|
|
var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
|
|
var contentContainerClass = '.' + $notes.closest('.notes_content')
|
|
.attr('class')
|
|
.split(' ')
|
|
.join('.');
|
|
|
|
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
|
|
}
|
|
}
|
|
// Init discussion on 'Discussion' page if it is merge request page
|
|
const page = $('body').attr('data-page');
|
|
if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
|
|
Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
|
|
}
|
|
} else {
|
|
// append new note to all matching discussions
|
|
Notes.animateAppendNote(noteEntity.html, discussionContainer);
|
|
}
|
|
|
|
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
|
|
gl.diffNotesCompileComponents();
|
|
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
|
|
}
|
|
|
|
gl.utils.localTimeAgo($('.js-timeago'), false);
|
|
Notes.checkMergeRequestStatus();
|
|
return this.updateNotesCount(1);
|
|
}
|
|
|
|
getLineHolder(changesDiscussionContainer) {
|
|
return $(changesDiscussionContainer).closest('.notes_holder')
|
|
.prevAll('.line_holder')
|
|
.first()
|
|
.get(0);
|
|
}
|
|
|
|
renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
|
|
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
|
|
|
|
if (!avatarHolder.length) {
|
|
avatarHolder = document.createElement('diff-note-avatars');
|
|
avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id);
|
|
|
|
diffAvatarContainer.append(avatarHolder);
|
|
|
|
gl.diffNotesCompileComponents();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called in response the main target form has been successfully submitted.
|
|
*
|
|
* Removes any errors.
|
|
* Resets text and preview.
|
|
* Resets buttons.
|
|
*/
|
|
resetMainTargetForm(e) {
|
|
var form;
|
|
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();
|
|
|
|
var event = document.createEvent('Event');
|
|
event.initEvent('autosize:update', true, false);
|
|
form.find('.js-autosize')[0].dispatchEvent(event);
|
|
|
|
this.updateTargetButtons(e);
|
|
}
|
|
|
|
reenableTargetFormSubmitButton() {
|
|
var form;
|
|
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() {
|
|
var form;
|
|
// find the form
|
|
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);
|
|
// 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();
|
|
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').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
|
|
* setup GFM auto complete
|
|
* show the form
|
|
*/
|
|
setupNoteForm(form) {
|
|
var textarea, key;
|
|
new gl.GLForm(form, this.enableGFM);
|
|
textarea = form.find('.js-note-text');
|
|
key = [
|
|
'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('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
|
|
}
|
|
|
|
updateNoteError($parentTimeline) {
|
|
new Flash('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) {
|
|
var projectPath = $form.data('project-path');
|
|
var discussionId = $form.data('discussion-id');
|
|
var mergeRequestId = $form.data('noteable-iid');
|
|
|
|
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) {
|
|
var $noteEntityEl, $note_li;
|
|
// Convert returned HTML to a jQuery object so we can modify it further
|
|
$noteEntityEl = $(noteEntity.html);
|
|
$noteEntityEl.addClass('fade-in-full');
|
|
this.revertNoteEditForm($targetNote);
|
|
$noteEntityEl.renderGFM();
|
|
// Find the note's `li` element by ID and replace it with the updated HTML
|
|
$note_li = $('.note-row-' + noteEntity.id);
|
|
|
|
$note_li.replaceWith($noteEntityEl);
|
|
this.setupNewNote($noteEntityEl);
|
|
|
|
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
|
gl.diffNotesCompileComponents();
|
|
}
|
|
}
|
|
|
|
checkContentToAllowEditing($el) {
|
|
var initialContent = $el.find('.original-note-content').text().trim();
|
|
var currentContent = $el.find('.js-note-text').val();
|
|
var isAllowed = true;
|
|
|
|
if (currentContent === initialContent) {
|
|
this.removeNoteEditForm($el);
|
|
}
|
|
else {
|
|
var $buttons = $el.find('.note-form-actions');
|
|
var 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, scrollTo, myLastNote) {
|
|
e.preventDefault();
|
|
|
|
var $target = $(e.target);
|
|
var $editForm = $(this.getEditFormSelector($target));
|
|
var $note = $target.closest('.note');
|
|
var $currentlyEditing = $('.note.is-editing:visible');
|
|
|
|
if ($currentlyEditing.length) {
|
|
var 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');
|
|
var selector = this.getEditFormSelector($target);
|
|
var $editForm = $(selector);
|
|
|
|
$editForm.insertBefore('.notes-form');
|
|
$editForm.find('.js-comment-save-button').enable();
|
|
$editForm.find('.js-finish-edit-warning').hide();
|
|
}
|
|
|
|
getEditFormSelector($el) {
|
|
var 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) {
|
|
var form = $note.find('.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('original-note'));
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
var noteElId, noteId, dataNoteId, $note, lineHolder;
|
|
$note = $(e.currentTarget).closest('.note');
|
|
noteElId = $note.attr('id');
|
|
noteId = $note.attr('data-note-id');
|
|
lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
|
|
.closest('.notes_holder')
|
|
.prev('.line_holder');
|
|
$(`.note[id="${noteElId}"]`).each((function(_this) {
|
|
// A same note appears in the "Discussion" and in the "Changes" tab, we have
|
|
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
|
|
// where $('#noteId') would return only one.
|
|
return function(i, el) {
|
|
var $note, $notes;
|
|
$note = $(el);
|
|
$notes = $note.closest('.discussion-notes');
|
|
const discussionId = $('.notes', $notes).data('discussion-id');
|
|
|
|
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
|
if (gl.diffNoteApps[noteElId]) {
|
|
gl.diffNoteApps[noteElId].$destroy();
|
|
}
|
|
}
|
|
|
|
$note.remove();
|
|
|
|
// check if this is the last note for this line
|
|
if ($notes.find('.note').length === 0) {
|
|
var notesTr = $notes.closest('tr');
|
|
|
|
// "Discussions" tab
|
|
$notes.closest('.timeline-entry').remove();
|
|
|
|
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
|
|
|
|
// The notes tr can contain multiple lists of notes, like on the parallel diff
|
|
if (notesTr.find('.discussion-notes').length > 1) {
|
|
$notes.remove();
|
|
} else {
|
|
notesTr.remove();
|
|
}
|
|
}
|
|
};
|
|
})(this));
|
|
|
|
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('.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) {
|
|
var form, replyLink;
|
|
form = this.cleanForm(this.formClone.clone());
|
|
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) {
|
|
// setup note target
|
|
const diffFileData = dataHolder.closest('.text-file');
|
|
|
|
var 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.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
|
|
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');
|
|
|
|
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
|
var $commentBtn = form.find('comment-and-resolve-btn');
|
|
$commentBtn.attr(':discussion-id', `'${discussionID}'`);
|
|
|
|
gl.diffNotesCompileComponents();
|
|
}
|
|
|
|
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
|
|
});
|
|
}
|
|
|
|
toggleDiffNote({
|
|
target,
|
|
lineType,
|
|
forceShow,
|
|
showReplyInput = false,
|
|
}) {
|
|
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
|
|
$link = $(target);
|
|
row = $link.closest('tr');
|
|
const nextRow = row.next();
|
|
let targetRow = row;
|
|
if (nextRow.is('.notes_holder')) {
|
|
targetRow = nextRow;
|
|
}
|
|
|
|
hasNotes = nextRow.is('.notes_holder');
|
|
addForm = false;
|
|
let lineTypeSelector = '';
|
|
rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
|
|
// 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.toggle(showNow);
|
|
notesContent.toggle(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) {
|
|
var glForm, row;
|
|
row = form.closest('tr');
|
|
glForm = form.data('gl-form');
|
|
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();
|
|
} else {
|
|
// only remove the form
|
|
return form.remove();
|
|
}
|
|
}
|
|
|
|
cancelDiscussionForm(e) {
|
|
var form;
|
|
e.preventDefault();
|
|
form = $(e.target).closest('.js-discussion-note-form');
|
|
return this.removeDiscussionNoteForm(form);
|
|
}
|
|
|
|
/**
|
|
* Called after an attachment file has been selected.
|
|
*
|
|
* Updates the file name for the selected attachment.
|
|
*/
|
|
updateFormAttachment() {
|
|
var filename, form;
|
|
form = $(this).closest('form');
|
|
// get only the basename
|
|
filename = $(this).val().replace(/^.*[\\\/]/, '');
|
|
return form.find('.js-attachment-filename').text(filename);
|
|
}
|
|
|
|
/**
|
|
* Called when the tab visibility changes
|
|
*/
|
|
visibilityChange() {
|
|
return this.refresh();
|
|
}
|
|
|
|
updateTargetButtons(e) {
|
|
var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea;
|
|
textarea = $(e.target);
|
|
form = textarea.parents('form');
|
|
reopenbtn = form.find('.js-note-target-reopen');
|
|
closebtn = form.find('.js-note-target-close');
|
|
discardbtn = form.find('.js-note-discard');
|
|
|
|
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');
|
|
}
|
|
if (discardbtn.is(':hidden')) {
|
|
return discardbtn.show();
|
|
}
|
|
} else {
|
|
reopentext = reopenbtn.data('original-text');
|
|
closetext = closebtn.data('original-text');
|
|
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');
|
|
}
|
|
if (discardbtn.is(':visible')) {
|
|
return discardbtn.hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
putEditFormInPlace($el) {
|
|
var $editForm = $(this.getEditFormSelector($el));
|
|
var $note = $el.closest('.note');
|
|
|
|
$editForm.insertAfter($note.find('.note-text'));
|
|
|
|
var $originalContentEl = $note.find('.original-note-content');
|
|
var originalContent = $originalContentEl.text().trim();
|
|
var postUrl = $originalContentEl.data('post-url');
|
|
var targetId = $originalContentEl.data('target-id');
|
|
var targetType = $originalContentEl.data('target-type');
|
|
|
|
new gl.GLForm($editForm.find('form'), this.enableGFM);
|
|
|
|
$editForm.find('form')
|
|
.attr('action', postUrl)
|
|
.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 $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
|
|
This comment has changed since you started editing, please review the
|
|
<a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
|
|
updated comment
|
|
</a>
|
|
to ensure information is not lost
|
|
</div>`);
|
|
$alert.insertAfter($note.find('.note-text'));
|
|
}
|
|
}
|
|
|
|
updateNotesCount(updateCount) {
|
|
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
|
|
}
|
|
|
|
toggleCommitList(e) {
|
|
const $element = $(e.currentTarget);
|
|
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
|
|
|
|
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
|
|
$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, function(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.flashInstance = new Flash(...flashParams);
|
|
}
|
|
|
|
clearFlash() {
|
|
if (this.flashInstance && this.flashInstance.flashContainer) {
|
|
this.flashInstance.flashContainer.hide();
|
|
this.flashInstance = 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 exists 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.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) {
|
|
return {
|
|
formData: $form.serialize(),
|
|
formContent: _.escape($form.find('.js-note-text').val()),
|
|
formAction: $form.attr('action'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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, index) => {
|
|
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 = `Applying command to ${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="hidden-xs">${_.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('.hidden-xs').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 `ajaxPost`
|
|
* 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);
|
|
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 } = 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) {
|
|
$submitBtn.disable();
|
|
$form.find('.js-comment-submit-button').disable();
|
|
}
|
|
|
|
tempFormContent = formContent;
|
|
if (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);
|
|
}
|
|
}
|
|
|
|
/* eslint-disable promise/catch-or-return */
|
|
// Make request to submit comment on server
|
|
ajaxPost(formAction, formData)
|
|
.then((note) => {
|
|
// Submission successful! remove placeholder
|
|
$notesContainer.find(`#${noteUniqueId}`).remove();
|
|
|
|
// 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('discussion-id'))
|
|
.attr('data-resolve-all', 'true')
|
|
.attr('data-project-path', $submitBtn.data('project-path'));
|
|
}
|
|
|
|
// Show final note element on UI
|
|
this.addDiscussionNote($form, note, $notesContainer.length === 0);
|
|
|
|
// 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]);
|
|
}).fail(() => {
|
|
// Submission failed, remove placeholder note and show Flash error message
|
|
$notesContainer.find(`#${noteUniqueId}`).remove();
|
|
|
|
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(formContent);
|
|
this.reenableTargetFormSubmitButton(e);
|
|
this.addNoteError($form);
|
|
});
|
|
|
|
return $closeBtn.text($closeBtn.data('original-text'));
|
|
}
|
|
|
|
/**
|
|
* 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 `ajaxPost`
|
|
* 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('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>');
|
|
|
|
/* eslint-disable promise/catch-or-return */
|
|
// Make request to update comment on server
|
|
ajaxPost(formAction, formData)
|
|
.then((note) => {
|
|
// Submission successful! render final note element
|
|
this.updateNote(note, $editingNote);
|
|
})
|
|
.fail(() => {
|
|
// Submission failed, revert back to original note
|
|
$noteBodyText.html(_.escape(cachedNoteBodyText));
|
|
$editingNote.removeClass('being-posted fade-in');
|
|
$editingNote.find('.fa.fa-spinner').remove();
|
|
|
|
// Show Flash message about failure
|
|
this.updateNoteError();
|
|
});
|
|
|
|
return $closeBtn.text($closeBtn.data('original-text'));
|
|
}
|
|
}
|
|
|
|
window.Notes = Notes;
|