notes.js 54.4 KB
Newer Older
E
Eric Eastwood 已提交
1 2 3 4 5 6 7
/* 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 */
8 9 10
/* global Flash */
/* global Autosave */
/* global ResolveService */
11
/* global mrRefreshWidgetUrl */
F
Fatih Acet 已提交
12

13
import $ from 'jquery';
14
import Cookies from 'js-cookie';
15 16 17 18
import autosize from 'vendor/autosize';
import Dropzone from 'dropzone';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
19
import AjaxCache from '~/lib/utils/ajax_cache';
20
import CommentTypeToggle from './comment_type_toggle';
21 22 23
import './autosave';
import './dropzone_input';
import './task_list';
24

25 26
window.autosize = autosize;
window.Dropzone = Dropzone;
F
Fatih Acet 已提交
27

28 29 30 31
const normalizeNewlines = function(str) {
  return str.replace(/\r\n/g, '\n');
};

F
Fatih Acet 已提交
32 33
(function() {
  this.Notes = (function() {
34
    const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
35
    const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
F
Fatih Acet 已提交
36 37 38

    Notes.interval = null;

39
    function Notes(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
40 41 42 43
      this.updateTargetButtons = this.updateTargetButtons.bind(this);
      this.updateComment = this.updateComment.bind(this);
      this.visibilityChange = this.visibilityChange.bind(this);
      this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this);
44
      this.onAddDiffNote = this.onAddDiffNote.bind(this);
45
      this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this);
46
      this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this);
47 48 49 50 51 52 53 54 55 56 57
      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);
58
      this.clearFlashWrapper = this.clearFlash.bind(this);
59

F
Fatih Acet 已提交
60 61
      this.notes_url = notes_url;
      this.note_ids = note_ids;
62
      this.enableGFM = enableGFM;
63 64
      // Used to keep track of updated notes while people are editing things
      this.updatedNotesTrackingMap = {};
F
Fatih Acet 已提交
65 66
      this.last_fetched_at = last_fetched_at;
      this.noteable_url = document.URL;
E
Eric Eastwood 已提交
67
      this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
F
Fatih Acet 已提交
68 69
      this.basePollingInterval = 15000;
      this.maxPollingSteps = 4;
70

F
Fatih Acet 已提交
71 72 73 74
      this.cleanBinding();
      this.addBinding();
      this.setPollingInterval();
      this.setupMainTargetNoteForm();
75 76
      this.taskList = new gl.TaskList({
        dataType: 'note',
77
        fieldName: 'note',
78
        selector: '.notes'
79
      });
80
      this.collapseLongCommitList();
81
      this.setViewType(view);
82 83 84 85

      // We are in the Merge Requests page so we need another edit form for Changes tab
      if (gl.utils.getPagePath(1) === 'merge_requests') {
        $('.note-edit-form').clone()
F
Fatih Acet 已提交
86
          .addClass('mr-note-edit-form').insertAfter('.note-edit-form');
87
      }
F
Fatih Acet 已提交
88 89
    }

90 91 92 93
    Notes.prototype.setViewType = function(view) {
      this.view = Cookies.get('diff_view') || view;
    };

F
Fatih Acet 已提交
94
    Notes.prototype.addBinding = function() {
95
      // Edit note link
E
Eric Eastwood 已提交
96 97
      $(document).on('click', '.js-note-edit', this.showEditForm.bind(this));
      $(document).on('click', '.note-edit-cancel', this.cancelEdit);
98
      // Reopen and close actions for Issue/MR combined with note form submit
E
Eric Eastwood 已提交
99 100 101
      $(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);
102
      // resolve a discussion
K
Kushal Pandya 已提交
103
      $(document).on('click', '.js-comment-resolve-button', this.postComment);
104
      // remove a note (in general)
E
Eric Eastwood 已提交
105
      $(document).on('click', '.js-note-delete', this.removeNote);
106
      // delete note attachment
E
Eric Eastwood 已提交
107
      $(document).on('click', '.js-note-attachment-delete', this.removeAttachment);
108
      // reset main target form when clicking discard
E
Eric Eastwood 已提交
109
      $(document).on('click', '.js-note-discard', this.resetMainTargetForm);
110
      // update the file name when an attachment is selected
E
Eric Eastwood 已提交
111
      $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment);
112
      // reply to diff/discussion notes
E
Eric Eastwood 已提交
113
      $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
114
      // add diff note
E
Eric Eastwood 已提交
115
      $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
116
      // hide diff note form
E
Eric Eastwood 已提交
117
      $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
118
      // toggle commit list
E
Eric Eastwood 已提交
119
      $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
120
      // fetch notes when tab becomes visible
E
Eric Eastwood 已提交
121
      $(document).on('visibilitychange', this.visibilityChange);
122
      // when issue status changes, we need to refresh data
E
Eric Eastwood 已提交
123
      $(document).on('issuable:change', this.refresh);
K
Kushal Pandya 已提交
124
      // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
E
Eric Eastwood 已提交
125 126 127 128
      $(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);
129
      // when a key is clicked on the notes
E
Eric Eastwood 已提交
130
      return $(document).on('keydown', '.js-note-text', this.keydownNoteText);
F
Fatih Acet 已提交
131 132 133
    };

    Notes.prototype.cleanBinding = function() {
E
Eric Eastwood 已提交
134 135 136 137 138 139 140 141 142 143 144 145
      $(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');
146
      $(document).off('click', '.js-comment-resolve-button');
E
Eric Eastwood 已提交
147 148 149 150
      $(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');
F
Fatih Acet 已提交
151 152
    };

153 154 155 156 157 158
    Notes.initCommentTypeToggle = function (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');
L
Luke "Jared" Bennett 已提交
159
      const reopenButton = form.querySelector('.js-note-target-reopen');
160 161 162 163 164 165 166 167 168

      const commentTypeToggle = new CommentTypeToggle({
        dropdownTrigger,
        dropdownList,
        noteTypeInput,
        submitButton,
        closeButton,
        reopenButton,
      });
169

170
      commentTypeToggle.initDroplab();
171 172
    };

F
Fatih Acet 已提交
173 174
    Notes.prototype.keydownNoteText = function(e) {
      var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
175
      if (gl.utils.isMetaKey(e)) {
F
Fatih Acet 已提交
176 177
        return;
      }
178

F
Fatih Acet 已提交
179
      $textarea = $(e.target);
180
      // Edit previous note when UP arrow is hit
F
Fatih Acet 已提交
181 182 183 184 185
      switch (e.which) {
        case 38:
          if ($textarea.val() !== '') {
            return;
          }
186
          myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, #notes'));
F
Fatih Acet 已提交
187 188 189 190 191
          if (myLastNote.length) {
            myLastNoteEditBtn = myLastNote.find('.js-note-edit');
            return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
          }
          break;
192
        // Cancel creating diff note or editing any note when ESCAPE is hit
F
Fatih Acet 已提交
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
        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);
          }
      }
    };

    Notes.prototype.initRefresh = function() {
      clearInterval(Notes.interval);
      return Notes.interval = setInterval((function(_this) {
        return function() {
          return _this.refresh();
        };
      })(this), this.pollingInterval);
    };

    Notes.prototype.refresh = function() {
D
Douwe Maan 已提交
228
      if (!document.hidden) {
F
Fatih Acet 已提交
229 230 231 232 233 234 235 236 237 238 239
        return this.getContent();
      }
    };

    Notes.prototype.getContent = function() {
      if (this.refreshing) {
        return;
      }
      this.refreshing = true;
      return $.ajax({
        url: this.notes_url,
E
Eric Eastwood 已提交
240 241
        headers: { 'X-Last-Fetched-At': this.last_fetched_at },
        dataType: 'json',
F
Fatih Acet 已提交
242 243 244 245 246 247 248
        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) {
249
              _this.renderNote(note);
F
Fatih Acet 已提交
250 251 252 253 254 255 256 257 258 259 260 261 262 263
            });
          };
        })(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.
264

F
Fatih Acet 已提交
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    Note: this function is used to gradually increase the polling interval
    if there aren't new notes coming from the server
     */

    Notes.prototype.setPollingInterval = function(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();
    };

283
    Notes.prototype.handleQuickActions = function(noteEntity) {
M
mhasbini 已提交
284
      var votesBlock;
285 286
      if (noteEntity.commands_changes) {
        if ('merge' in noteEntity.commands_changes) {
F
Fatih Acet 已提交
287
          Notes.checkMergeRequestStatus();
M
mhasbini 已提交
288 289
        }

290
        if ('emoji_award' in noteEntity.commands_changes) {
M
mhasbini 已提交
291
          votesBlock = $('.js-awards-block').eq(0);
292
          gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
M
mhasbini 已提交
293 294
          return gl.awardsHandler.scrollToAwards();
        }
295 296 297
      }
    };

298 299 300 301 302 303 304
    Notes.prototype.setupNewNote = function($note) {
      // Update datetime format on the recent note
      gl.utils.localTimeAgo($note.find('.js-timeago'), false);
      this.collapseLongCommitList();
      this.taskList.init();
    };

F
Fatih Acet 已提交
305 306
    /*
    Render note in main comments area.
307

F
Fatih Acet 已提交
308 309 310
    Note: for rendering inline notes use renderDiscussionNote
     */

311
    Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) {
E
Eric Eastwood 已提交
312
      if (noteEntity.discussion_html) {
313
        return this.renderDiscussionNote(noteEntity, $form);
314 315
      }

316 317
      if (!noteEntity.valid) {
        if (noteEntity.errors.commands_only) {
318
          this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
M
mhasbini 已提交
319
          this.refresh();
F
Fatih Acet 已提交
320 321 322
        }
        return;
      }
M
mhasbini 已提交
323

324
      const $note = $notesList.find(`#note_${noteEntity.id}`);
325
      if (Notes.isNewNote(noteEntity, this.note_ids)) {
326
        this.note_ids.push(noteEntity.id);
327

328 329 330
        if ($notesList.length) {
          $notesList.find('.system-note.being-posted').remove();
        }
331
        const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList);
332

333
        this.setupNewNote($newNote);
334
        this.refresh();
F
Fatih Acet 已提交
335 336
        return this.updateNotesCount(1);
      }
337
      // The server can send the same update multiple times so we need to make sure to only update once per actual update.
338
      else if (Notes.isUpdatedNote(noteEntity, $note)) {
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
        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);
359
          this.setupNewNote($updatedNote);
360 361
        }
      }
F
Fatih Acet 已提交
362 363 364
    };

    Notes.prototype.isParallelView = function() {
365
      return Cookies.get('diff_view') === 'parallel';
F
Fatih Acet 已提交
366 367 368 369
    };

    /*
    Render note in discussion area.
370

F
Fatih Acet 已提交
371 372 373
    Note: for rendering inline notes use renderDiscussionNote
     */

374
    Notes.prototype.renderDiscussionNote = function(noteEntity, $form) {
375
      var discussionContainer, form, row, lineType, diffAvatarContainer;
376
      if (!Notes.isNewNote(noteEntity, this.note_ids)) {
F
Fatih Acet 已提交
377 378
        return;
      }
379
      this.note_ids.push(noteEntity.id);
E
Eric Eastwood 已提交
380 381
      form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
      row = form.closest('tr');
382 383
      lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
      diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
384
      // is this the first note of discussion?
385
      discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
D
Douwe Maan 已提交
386 387
      if (!discussionContainer.length) {
        discussionContainer = form.closest('.discussion').find('.notes');
F
Fatih Acet 已提交
388 389
      }
      if (discussionContainer.length === 0) {
390 391
        if (noteEntity.diff_discussion_html) {
          var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
392 393 394 395 396 397

          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
E
Eric Eastwood 已提交
398
            var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
399 400 401 402 403 404 405
            var contentContainerClass = '.' + $notes.closest('.notes_content')
              .attr('class')
              .split(' ')
              .join('.');

            row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
          }
406
        }
407
        // Init discussion on 'Discussion' page if it is merge request page
408
        const page = $('body').attr('data-page');
E
Eric Eastwood 已提交
409
        if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
410
          Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
F
Fatih Acet 已提交
411 412
        }
      } else {
413
        // append new note to all matching discussions
414
        Notes.animateAppendNote(noteEntity.html, discussionContainer);
F
Fatih Acet 已提交
415
      }
416

417
      if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
418
        gl.diffNotesCompileComponents();
419
        this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
420 421
      }

422
      gl.utils.localTimeAgo($('.js-timeago'), false);
F
Fatih Acet 已提交
423
      Notes.checkMergeRequestStatus();
F
Fatih Acet 已提交
424 425 426
      return this.updateNotesCount(1);
    };

427 428 429 430 431 432 433
    Notes.prototype.getLineHolder = function(changesDiscussionContainer) {
      return $(changesDiscussionContainer).closest('.notes_holder')
        .prevAll('.line_holder')
        .first()
        .get(0);
    };

434
    Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, noteEntity) {
435 436 437 438 439
      var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
      var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');

      if (!avatarHolder.length) {
        avatarHolder = document.createElement('diff-note-avatars');
440
        avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id);
441 442 443 444 445 446 447 448 449 450 451

        diffAvatarContainer.append(avatarHolder);

        gl.diffNotesCompileComponents();
      }

      if (commentButton.length) {
        commentButton.remove();
      }
    };

F
Fatih Acet 已提交
452 453
    /*
    Called in response the main target form has been successfully submitted.
454

F
Fatih Acet 已提交
455 456 457 458 459 460 461
    Removes any errors.
    Resets text and preview.
    Resets buttons.
     */

    Notes.prototype.resetMainTargetForm = function(e) {
      var form;
E
Eric Eastwood 已提交
462
      form = $('.js-main-target-form');
463
      // remove validation errors
E
Eric Eastwood 已提交
464
      form.find('.js-errors').remove();
465
      // reset text and preview
E
Eric Eastwood 已提交
466 467 468
      form.find('.js-md-write-button').click();
      form.find('.js-note-text').val('').trigger('input');
      form.find('.js-note-text').data('autosave').reset();
469 470 471 472 473 474

      var event = document.createEvent('Event');
      event.initEvent('autosize:update', true, false);
      form.find('.js-autosize')[0].dispatchEvent(event);

      this.updateTargetButtons(e);
F
Fatih Acet 已提交
475 476 477 478
    };

    Notes.prototype.reenableTargetFormSubmitButton = function() {
      var form;
E
Eric Eastwood 已提交
479 480
      form = $('.js-main-target-form');
      return form.find('.js-note-text').trigger('input');
F
Fatih Acet 已提交
481 482 483 484
    };

    /*
    Shows the main form and does some setup on it.
485

F
Fatih Acet 已提交
486 487 488 489 490
    Sets some hidden fields in the form.
     */

    Notes.prototype.setupMainTargetNoteForm = function() {
      var form;
491
      // find the form
E
Eric Eastwood 已提交
492
      form = $('.js-new-note-form');
493
      // Set a global clone of the form for later cloning
F
Fatih Acet 已提交
494
      this.formClone = form.clone();
495
      // show the form
F
Fatih Acet 已提交
496
      this.setupNoteForm(form);
497
      // fix classes
E
Eric Eastwood 已提交
498 499 500 501 502 503
      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('#in_reply_to_discussion_id').remove();
504
      form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
505 506
      this.parentTimeline = form.parents('.timeline');

507
      if (form.length) {
508
        Notes.initCommentTypeToggle(form.get(0));
509
      }
F
Fatih Acet 已提交
510 511 512 513
    };

    /*
    General note form setup.
514

F
Fatih Acet 已提交
515 516 517 518 519 520 521
    deactivates the submit button when text is empty
    hides the preview button when text is empty
    setup GFM auto complete
    show the form
     */

    Notes.prototype.setupNoteForm = function(form) {
522
      var textarea, key;
523
      new gl.GLForm(form, this.enableGFM);
E
Eric Eastwood 已提交
524
      textarea = form.find('.js-note-text');
525
      key = [
E
Eric Eastwood 已提交
526 527 528 529 530 531
        '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('#in_reply_to_discussion_id').val(),
532 533

        // LegacyDiffNote
E
Eric Eastwood 已提交
534
        form.find('#note_line_code').val(),
535 536

        // DiffNote
E
Eric Eastwood 已提交
537
        form.find('#note_position').val()
538 539
      ];
      return new Autosave(textarea, key);
F
Fatih Acet 已提交
540 541 542 543
    };

    /*
    Called in response to the new note form being submitted
544

F
Fatih Acet 已提交
545 546 547
    Adds new note to list.
     */

K
Kushal Pandya 已提交
548
    Notes.prototype.addNote = function($form, note) {
F
Fatih Acet 已提交
549 550 551
      return this.renderNote(note);
    };

552
    Notes.prototype.addNoteError = function($form) {
K
Kushal Pandya 已提交
553 554 555 556 557 558
      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');
      }
559
      return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
F
Fatih Acet 已提交
560 561
    };

K
Kushal Pandya 已提交
562 563
    Notes.prototype.updateNoteError = $parentTimeline => new Flash('Your comment could not be updated! Please check your network connection and try again.');

F
Fatih Acet 已提交
564 565
    /*
    Called in response to the new note form being submitted
566

F
Fatih Acet 已提交
567 568 569
    Adds new note to list.
     */

K
Kushal Pandya 已提交
570
    Notes.prototype.addDiscussionNote = function($form, note, isNewDiffComment) {
571
      if ($form.attr('data-resolve-all') != null) {
572 573 574
        var projectPath = $form.data('project-path');
        var discussionId = $form.data('discussion-id');
        var mergeRequestId = $form.data('noteable-iid');
575 576

        if (ResolveService != null) {
P
Phil Hughes 已提交
577
          ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
578 579
        }
      }
580

D
Douwe Maan 已提交
581
      this.renderNote(note, $form);
582
      // cleanup after successfully creating a diff/discussion note
K
Kushal Pandya 已提交
583 584 585
      if (isNewDiffComment) {
        this.removeDiscussionNoteForm($form);
      }
F
Fatih Acet 已提交
586 587 588 589
    };

    /*
    Called in response to the edit note form being submitted
590

F
Fatih Acet 已提交
591 592 593
    Updates the current note field.
     */

594
    Notes.prototype.updateNote = function(noteEntity, $targetNote) {
K
Kushal Pandya 已提交
595
      var $noteEntityEl, $note_li;
596
      // Convert returned HTML to a jQuery object so we can modify it further
K
Kushal Pandya 已提交
597 598
      $noteEntityEl = $(noteEntity.html);
      $noteEntityEl.addClass('fade-in-full');
599
      this.revertNoteEditForm($targetNote);
K
Kushal Pandya 已提交
600 601 602
      gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl));
      $noteEntityEl.renderGFM();
      $noteEntityEl.find('.js-task-list-container').taskList('enable');
603
      // Find the note's `li` element by ID and replace it with the updated HTML
604
      $note_li = $('.note-row-' + noteEntity.id);
605

K
Kushal Pandya 已提交
606
      $note_li.replaceWith($noteEntityEl);
607

608 609
      if (typeof gl.diffNotesCompileComponents !== 'undefined') {
        gl.diffNotesCompileComponents();
610
      }
F
Fatih Acet 已提交
611 612
    };

613 614
    Notes.prototype.checkContentToAllowEditing = function($el) {
      var initialContent = $el.find('.original-note-content').text().trim();
615
      var currentContent = $el.find('.js-note-text').val();
616 617 618 619 620 621 622
      var isAllowed = true;

      if (currentContent === initialContent) {
        this.removeNoteEditForm($el);
      }
      else {
        var $buttons = $el.find('.note-form-actions');
F
Fatih Acet 已提交
623
        var isWidgetVisible = gl.utils.isInViewport($el.get(0));
624

625
        if (!isWidgetVisible) {
F
Fatih Acet 已提交
626
          gl.utils.scrollToElement($el);
627 628
        }

629
        $el.find('.js-finish-edit-warning').show();
630 631 632 633
        isAllowed = false;
      }

      return isAllowed;
634
    };
635

F
Fatih Acet 已提交
636 637
    /*
    Called in response to clicking the edit note link
638

F
Fatih Acet 已提交
639 640
    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
641
    */
F
Fatih Acet 已提交
642 643
    Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) {
      e.preventDefault();
644

645
      var $target = $(e.target);
646
      var $editForm = $(this.getEditFormSelector($target));
647
      var $note = $target.closest('.note');
648
      var $currentlyEditing = $('.note.is-editing:visible');
649

650 651 652 653 654 655 656 657
      if ($currentlyEditing.length) {
        var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);

        if (!isEditAllowed) {
          return;
        }
      }

658
      $note.find('.js-note-attachment-delete').show();
659
      $editForm.addClass('current-note-edit-form');
660
      $note.addClass('is-editing');
661
      this.putEditFormInPlace($target);
F
Fatih Acet 已提交
662 663 664 665
    };

    /*
    Called in response to clicking the edit note link
666

F
Fatih Acet 已提交
667 668 669 670 671
    Hides edit form and restores the original note text to the editor textarea.
     */

    Notes.prototype.cancelEdit = function(e) {
      e.preventDefault();
672 673 674 675
      const $target = $(e.target);
      const $note = $target.closest('.note');
      const noteId = $note.attr('data-note-id');

676
      this.revertNoteEditForm($target);
677 678 679 680

      if (this.updatedNotesTrackingMap[noteId]) {
        const $newNote = $(this.updatedNotesTrackingMap[noteId].html);
        $note.replaceWith($newNote);
681
        this.setupNewNote($newNote);
E
Eric Eastwood 已提交
682 683
        // Now that we have taken care of the update, clear it out
        delete this.updatedNotesTrackingMap[noteId];
684 685 686 687 688
      }
      else {
        $note.find('.js-finish-edit-warning').hide();
        this.removeNoteEditForm($note);
      }
F
Fatih Acet 已提交
689 690
    };

691
    Notes.prototype.revertNoteEditForm = function($target) {
692
      $target = $target || $('.note.is-editing:visible');
693 694
      var selector = this.getEditFormSelector($target);
      var $editForm = $(selector);
695 696

      $editForm.insertBefore('.notes-form');
K
Kushal Pandya 已提交
697
      $editForm.find('.js-comment-save-button').enable();
698
      $editForm.find('.js-finish-edit-warning').hide();
699
    };
700

701
    Notes.prototype.getEditFormSelector = function($el) {
F
Fatih Acet 已提交
702
      var selector = '.note-edit-form:not(.mr-note-edit-form)';
703 704

      if ($el.parents('#diffs').length) {
F
Fatih Acet 已提交
705
        selector = '.note-edit-form.mr-note-edit-form';
706 707 708 709
      }

      return selector;
    };
710

711 712 713
    Notes.prototype.removeNoteEditForm = function($note) {
      var form = $note.find('.current-note-edit-form');
      $note.removeClass('is-editing');
714
      form.removeClass('current-note-edit-form');
715
      form.find('.js-finish-edit-warning').hide();
716
      // Replace markdown textarea text with original note text.
717
      return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
F
Fatih Acet 已提交
718 719 720 721
    };

    /*
    Called in response to deleting a note of any kind.
722

F
Fatih Acet 已提交
723 724 725 726 727
    Removes the actual note from view.
    Removes the whole discussion if the last note is being removed.
     */

    Notes.prototype.removeNote = function(e) {
728 729 730 731 732 733 734
      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');
E
Eric Eastwood 已提交
735
      $(`.note[id="${noteElId}"]`).each((function(_this) {
736
        // A same note appears in the "Discussion" and in the "Changes" tab, we have
E
Eric Eastwood 已提交
737 738
        // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
        // where $('#noteId') would return only one.
F
Fatih Acet 已提交
739
        return function(i, el) {
740 741
          var $note, $notes;
          $note = $(el);
E
Eric Eastwood 已提交
742
          $notes = $note.closest('.discussion-notes');
743

744
          if (typeof gl.diffNotesCompileComponents !== 'undefined') {
745 746
            if (gl.diffNoteApps[noteElId]) {
              gl.diffNoteApps[noteElId].$destroy();
747 748 749
            }
          }

750
          $note.remove();
751

752
          // check if this is the last note for this line
E
Eric Eastwood 已提交
753 754
          if ($notes.find('.note').length === 0) {
            var notesTr = $notes.closest('tr');
755

756
            // "Discussions" tab
E
Eric Eastwood 已提交
757
            $notes.closest('.timeline-entry').remove();
758

759 760
            // The notes tr can contain multiple lists of notes, like on the parallel diff
            if (notesTr.find('.discussion-notes').length > 1) {
761
              $notes.remove();
762
            } else {
763
              notesTr.remove();
764
            }
F
Fatih Acet 已提交
765 766 767
          }
        };
      })(this));
F
Fatih Acet 已提交
768 769

      Notes.checkMergeRequestStatus();
F
Fatih Acet 已提交
770 771 772 773 774
      return this.updateNotesCount(-1);
    };

    /*
    Called in response to clicking the delete attachment link
775

F
Fatih Acet 已提交
776 777 778 779 780
    Removes the attachment wrapper view, including image tag if it exists
    Resets the note editing form
     */

    Notes.prototype.removeAttachment = function() {
E
Eric Eastwood 已提交
781 782 783 784 785
      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();
F
Fatih Acet 已提交
786 787 788 789
    };

    /*
    Called when clicking on the "reply" button for a diff line.
790

F
Fatih Acet 已提交
791 792 793
    Shows the note form below the notes.
     */

794 795 796 797 798
    Notes.prototype.onReplyToDiscussionNote = function(e) {
      this.replyToDiscussionNote(e.target);
    };

    Notes.prototype.replyToDiscussionNote = function(target) {
F
Fatih Acet 已提交
799
      var form, replyLink;
L
Luke "Jared" Bennett 已提交
800
      form = this.cleanForm(this.formClone.clone());
E
Eric Eastwood 已提交
801
      replyLink = $(target).closest('.js-discussion-reply-button');
802
      // insert the form after the button
803 804 805 806
      replyLink
        .closest('.discussion-reply-holder')
        .hide()
        .after(form);
807
      // show the form
F
Fatih Acet 已提交
808 809 810 811 812
      return this.setupDiscussionNoteForm(replyLink, form);
    };

    /*
    Shows the diff or discussion form and does some setup on it.
813

F
Fatih Acet 已提交
814
    Sets some hidden fields in the form.
815

816
    Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
F
Fatih Acet 已提交
817 818 819
     */

    Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
820
      // setup note target
E
Eric Eastwood 已提交
821
      var discussionID = dataHolder.data('discussionId');
822

D
Douwe Maan 已提交
823
      if (discussionID) {
E
Eric Eastwood 已提交
824 825
        form.attr('data-discussion-id', discussionID);
        form.find('#in_reply_to_discussion_id').val(discussionID);
D
Douwe Maan 已提交
826
      }
827

E
Eric Eastwood 已提交
828 829
      form.attr('data-line-code', dataHolder.data('lineCode'));
      form.find('#line_type').val(dataHolder.data('lineType'));
830

E
Eric Eastwood 已提交
831 832 833 834
      form.find('#note_noteable_type').val(dataHolder.data('noteableType'));
      form.find('#note_noteable_id').val(dataHolder.data('noteableId'));
      form.find('#note_commit_id').val(dataHolder.data('commitId'));
      form.find('#note_type').val(dataHolder.data('noteType'));
835 836

      // LegacyDiffNote
E
Eric Eastwood 已提交
837
      form.find('#note_line_code').val(dataHolder.data('lineCode'));
838 839

      // DiffNote
E
Eric Eastwood 已提交
840
      form.find('#note_position').val(dataHolder.attr('data-position'));
841

F
Fatih Acet 已提交
842
      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'));
843
      form.find('.js-note-target-close').remove();
844
      form.find('.js-note-new-discussion').remove();
F
Fatih Acet 已提交
845
      this.setupNoteForm(form);
846

D
Douwe Maan 已提交
847 848
      form
        .removeClass('js-main-target-form')
E
Eric Eastwood 已提交
849
        .addClass('discussion-form js-discussion-note-form');
D
Douwe Maan 已提交
850

851
      if (typeof gl.diffNotesCompileComponents !== 'undefined') {
852
        var $commentBtn = form.find('comment-and-resolve-btn');
L
Luke "Jared" Bennett 已提交
853
        $commentBtn.attr(':discussion-id', `'${discussionID}'`);
P
Phil Hughes 已提交
854

855
        gl.diffNotesCompileComponents();
856 857
      }

E
Eric Eastwood 已提交
858
      form.find('.js-note-text').focus();
859 860
      form
        .find('.js-comment-resolve-button')
D
Douwe Maan 已提交
861
        .attr('data-discussion-id', discussionID);
F
Fatih Acet 已提交
862 863 864 865
    };

    /*
    Called when clicking on the "add a comment" button on the side of a diff line.
866

F
Fatih Acet 已提交
867 868 869 870
    Inserts a temporary row for the form below the line.
    Sets up the form and shows it.
     */

871
    Notes.prototype.onAddDiffNote = function(e) {
F
Fatih Acet 已提交
872
      e.preventDefault();
873 874
      const link = e.currentTarget || e.target;
      const $link = $(link);
875
      const showReplyInput = !$link.hasClass('js-diff-comment-avatar');
876 877
      this.toggleDiffNote({
        target: $link,
878
        lineType: link.dataset.lineType,
879 880
        showReplyInput
      });
881 882
    };

883 884 885 886 887 888
    Notes.prototype.toggleDiffNote = function({
      target,
      lineType,
      forceShow,
      showReplyInput = false,
    }) {
889 890
      var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
      $link = $(target);
E
Eric Eastwood 已提交
891
      row = $link.closest('tr');
892 893 894 895 896 897
      const nextRow = row.next();
      let targetRow = row;
      if (nextRow.is('.notes_holder')) {
        targetRow = nextRow;
      }

E
Eric Eastwood 已提交
898
      hasNotes = nextRow.is('.notes_holder');
F
Fatih Acet 已提交
899
      addForm = false;
900
      let lineTypeSelector = '';
E
Eric Eastwood 已提交
901
      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>';
902
      // In parallel view, look inside the correct left/right pane
F
Fatih Acet 已提交
903
      if (this.isParallelView()) {
904
        lineTypeSelector = `.${lineType}`;
E
Eric Eastwood 已提交
905
        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>';
F
Fatih Acet 已提交
906
      }
907 908
      const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
      let notesContent = targetRow.find(notesContentSelector);
909

910 911 912
      if (hasNotes && showReplyInput) {
        targetRow.show();
        notesContent = targetRow.find(notesContentSelector);
F
Fatih Acet 已提交
913
        if (notesContent.length) {
914
          notesContent.show();
E
Eric Eastwood 已提交
915
          replyButton = notesContent.find('.js-discussion-reply-button:visible');
F
Fatih Acet 已提交
916
          if (replyButton.length) {
917
            this.replyToDiscussionNote(replyButton[0]);
F
Fatih Acet 已提交
918
          } else {
919
            // In parallel view, the form may not be present in one of the panes
E
Eric Eastwood 已提交
920
            noteForm = notesContent.find('.js-discussion-note-form');
F
Fatih Acet 已提交
921 922 923 924 925
            if (noteForm.length === 0) {
              addForm = true;
            }
          }
        }
926
      } else if (showReplyInput) {
927
        // add a notes row and insert the form
F
Fatih Acet 已提交
928
        row.after(rowCssToAdd);
929 930
        targetRow = row.next();
        notesContent = targetRow.find(notesContentSelector);
F
Fatih Acet 已提交
931
        addForm = true;
932
      } else {
933 934 935
        const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
        const isForced = forceShow === true || forceShow === false;
        const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
936

937 938
        targetRow.toggle(showNow);
        notesContent.toggle(showNow);
F
Fatih Acet 已提交
939
      }
940

F
Fatih Acet 已提交
941
      if (addForm) {
942
        newForm = this.cleanForm(this.formClone.clone());
943
        newForm.appendTo(notesContent);
944
        // show the form
F
Fatih Acet 已提交
945 946 947 948 949 950
        return this.setupDiscussionNoteForm($link, newForm);
      }
    };

    /*
    Called in response to "cancel" on a diff note form.
951

F
Fatih Acet 已提交
952 953 954 955 956 957
    Shows the reply button again.
    Removes the form and if necessary it's temporary row.
     */

    Notes.prototype.removeDiscussionNoteForm = function(form) {
      var glForm, row;
E
Eric Eastwood 已提交
958
      row = form.closest('tr');
F
Fatih Acet 已提交
959 960
      glForm = form.data('gl-form');
      glForm.destroy();
E
Eric Eastwood 已提交
961
      form.find('.js-note-text').data('autosave').reset();
962
      // show the reply button (will only work for replies)
963 964 965
      form
        .prev('.discussion-reply-holder')
        .show();
E
Eric Eastwood 已提交
966
      if (row.is('.js-temp-notes-holder')) {
967
        // remove temporary row for diff lines
F
Fatih Acet 已提交
968 969
        return row.remove();
      } else {
970
        // only remove the form
F
Fatih Acet 已提交
971 972 973 974 975 976 977
        return form.remove();
      }
    };

    Notes.prototype.cancelDiscussionForm = function(e) {
      var form;
      e.preventDefault();
E
Eric Eastwood 已提交
978
      form = $(e.target).closest('.js-discussion-note-form');
F
Fatih Acet 已提交
979 980 981 982 983
      return this.removeDiscussionNoteForm(form);
    };

    /*
    Called after an attachment file has been selected.
984

F
Fatih Acet 已提交
985 986 987 988 989
    Updates the file name for the selected attachment.
     */

    Notes.prototype.updateFormAttachment = function() {
      var filename, form;
E
Eric Eastwood 已提交
990
      form = $(this).closest('form');
991
      // get only the basename
E
Eric Eastwood 已提交
992 993
      filename = $(this).val().replace(/^.*[\\\/]/, '');
      return form.find('.js-attachment-filename').text(filename);
F
Fatih Acet 已提交
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010
    };

    /*
    Called when the tab visibility changes
     */

    Notes.prototype.visibilityChange = function() {
      return this.refresh();
    };

    Notes.prototype.updateTargetButtons = function(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');
1011

F
Fatih Acet 已提交
1012
      if (textarea.val().trim().length > 0) {
1013
        reopentext = reopenbtn.attr('data-alternative-text');
L
Luke "Jared" Bennett 已提交
1014
        closetext = closebtn.attr('data-alternative-text');
F
Fatih Acet 已提交
1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
        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();
        }
      }
    };

1051
    Notes.prototype.putEditFormInPlace = function($el) {
1052
      var $editForm = $(this.getEditFormSelector($el));
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062
      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');

1063
      new gl.GLForm($editForm.find('form'), this.enableGFM);
1064

1065 1066 1067
      $editForm.find('form')
        .attr('action', postUrl)
        .attr('data-remote', 'true');
F
Fatih Acet 已提交
1068 1069
      $editForm.find('.js-form-target-id').val(targetId);
      $editForm.find('.js-form-target-type').val(targetType);
1070
      $editForm.find('.js-note-text').focus().val(originalContent);
F
Fatih Acet 已提交
1071 1072
      $editForm.find('.js-md-write-button').trigger('click');
      $editForm.find('.referenced-users').hide();
1073
    };
1074

1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087
    Notes.prototype.putConflictEditWarningInPlace = function(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'));
      }
    };

F
Fatih Acet 已提交
1088
    Notes.prototype.updateNotesCount = function(updateCount) {
1089
      return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
F
Fatih Acet 已提交
1090 1091
    };

1092
    Notes.prototype.toggleCommitList = function(e) {
1093
      const $element = $(e.currentTarget);
1094 1095
      const $closestSystemCommitList = $element.siblings('.system-note-commit-list');

1096
      $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122
      $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.
     */
    Notes.prototype.collapseLongCommitList = function() {
      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');
        }
      });
    };

1123
    Notes.prototype.addFlash = function(...flashParams) {
1124
      this.flashInstance = new Flash(...flashParams);
1125 1126 1127
    };

    Notes.prototype.clearFlash = function() {
1128 1129 1130 1131
      if (this.flashInstance && this.flashInstance.flashContainer) {
        this.flashInstance.flashContainer.hide();
        this.flashInstance = null;
      }
1132 1133
    };

1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
    Notes.prototype.cleanForm = function($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;
L
Luke "Jared" Bennett 已提交
1146
    };
1147

1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161
    /**
     * Check if note does not exists on page
     */
    Notes.isNewNote = function(noteEntity, noteIds) {
      return $.inArray(noteEntity.id, noteIds) === -1;
    };

    /**
     * Check if $note already contains the `noteEntity` content
     */
    Notes.isUpdatedNote = function(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(
1162
        $note.find('.original-note-content').first().text().trim()
1163 1164 1165 1166
      );
      return sanitizedNoteEntityText !== currentNoteText;
    };

F
Fatih Acet 已提交
1167 1168 1169 1170 1171 1172
    Notes.checkMergeRequestStatus = function() {
      if (gl.utils.getPagePath(1) === 'merge_requests') {
        gl.mrWidget.checkStatus();
      }
    };

1173 1174
    Notes.animateAppendNote = function(noteHtml, $notesList) {
      const $note = $(noteHtml);
1175

K
Kushal Pandya 已提交
1176
      $note.addClass('fade-in-full').renderGFM();
1177
      $notesList.append($note);
1178 1179 1180 1181 1182 1183 1184 1185 1186
      return $note;
    };

    Notes.animateUpdateNote = function(noteHtml, $note) {
      const $updatedNote = $(noteHtml);

      $updatedNote.addClass('fade-in').renderGFM();
      $note.replaceWith($updatedNote);
      return $updatedNote;
1187 1188
    };

K
Kushal Pandya 已提交
1189 1190 1191 1192 1193 1194
    /**
     * Get data from Form attributes to use for saving/submitting comment.
     */
    Notes.prototype.getFormData = function($form) {
      return {
        formData: $form.serialize(),
1195
        formContent: _.escape($form.find('.js-note-text').val()),
K
Kushal Pandya 已提交
1196 1197 1198 1199 1200
        formAction: $form.attr('action'),
      };
    };

    /**
1201
     * Identify if comment has any quick actions
K
Kushal Pandya 已提交
1202
     */
1203 1204
    Notes.prototype.hasQuickActions = function(formContent) {
      return REGEX_QUICK_ACTIONS.test(formContent);
K
Kushal Pandya 已提交
1205 1206 1207
    };

    /**
1208
     * Remove quick actions and leave comment with pure message
K
Kushal Pandya 已提交
1209
     */
1210 1211
    Notes.prototype.stripQuickActions = function(formContent) {
      return formContent.replace(REGEX_QUICK_ACTIONS, '').trim();
K
Kushal Pandya 已提交
1212 1213
    };

1214
    /**
1215
     * Gets appropriate description from quick actions found in provided `formContent`
1216
     */
1217
    Notes.prototype.getQuickActionDescription = function (formContent, availableQuickActions = []) {
1218 1219
      let tempFormContent;

1220 1221
      // Identify executed quick actions from `formContent`
      const executedCommands = availableQuickActions.filter((command, index) => {
1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239
        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;
    };

K
Kushal Pandya 已提交
1240 1241 1242 1243 1244 1245
    /**
     * 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.
     */
1246
    Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) {
K
Kushal Pandya 已提交
1247 1248 1249 1250 1251
      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">
1252 1253 1254
                 <a href="/${currentUsername}">
                   <img class="avatar s40" src="${currentUserAvatar}">
                 </a>
K
Kushal Pandya 已提交
1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266
              </div>
              <div class="timeline-content ${discussionClass}">
                 <div class="note-header">
                    <div class="note-header-info">
                       <a href="/${currentUsername}">
                         <span class="hidden-xs">${currentUserFullname}</span>
                         <span class="note-headline-light">@${currentUsername}</span>
                       </a>
                    </div>
                 </div>
                 <div class="note-body">
                   <div class="note-text">
1267
                     <p>${formContent}</p>
K
Kushal Pandya 已提交
1268 1269 1270 1271 1272 1273 1274 1275 1276 1277
                   </div>
                 </div>
              </div>
           </div>
        </li>`
      );

      return $tempNote;
    };

1278
    /**
1279
     * Create Placeholder System Note DOM element populated with quick action description
1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294
     */
    Notes.prototype.createPlaceholderSystemNote = function ({ 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;
    };

K
Kushal Pandya 已提交
1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325
    /**
     * 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 `gl.utils.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
     */
    Notes.prototype.postComment = function(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);
1326 1327
      let noteUniqueId;
      let systemNoteUniqueId;
1328
      let hasQuickActions = false;
K
Kushal Pandya 已提交
1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346
      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;
1347 1348 1349
      if (this.hasQuickActions(formContent)) {
        tempFormContent = this.stripQuickActions(formContent);
        hasQuickActions = true;
K
Kushal Pandya 已提交
1350 1351
      }

1352
      // Show placeholder note
K
Kushal Pandya 已提交
1353
      if (tempFormContent) {
1354
        noteUniqueId = _.uniqueId('tempNote_');
K
Kushal Pandya 已提交
1355 1356
        $notesContainer.append(this.createPlaceholderNote({
          formContent: tempFormContent,
1357
          uniqueId: noteUniqueId,
K
Kushal Pandya 已提交
1358 1359 1360
          isDiscussionNote,
          currentUsername: gon.current_username,
          currentUserFullname: gon.current_user_fullname,
1361 1362 1363 1364 1365
          currentUserAvatar: gon.current_user_avatar_url,
        }));
      }

      // Show placeholder system note
1366
      if (hasQuickActions) {
1367 1368
        systemNoteUniqueId = _.uniqueId('tempSystemNote_');
        $notesContainer.append(this.createPlaceholderSystemNote({
1369
          formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)),
1370
          uniqueId: systemNoteUniqueId,
K
Kushal Pandya 已提交
1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387
        }));
      }

      // 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
      gl.utils.ajaxPost(formAction, formData)
        .then((note) => {
          // Submission successful! remove placeholder
1388 1389 1390
          $notesContainer.find(`#${noteUniqueId}`).remove();

          // Reset cached commands list when command is applied
1391
          if (hasQuickActions) {
1392 1393 1394
            $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
          }

1395 1396
          // Clear previous form errors
          this.clearFlashWrapper();
K
Kushal Pandya 已提交
1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424

          // 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) {
1425
            this.handleQuickActions(note);
K
Kushal Pandya 已提交
1426 1427 1428 1429 1430
          }

          $form.trigger('ajax:success', [note]);
        }).fail(() => {
          // Submission failed, remove placeholder note and show Flash error message
1431 1432
          $notesContainer.find(`#${noteUniqueId}`).remove();

1433
          if (hasQuickActions) {
1434 1435
            $notesContainer.find(`#${systemNoteUniqueId}`).remove();
          }
K
Kushal Pandya 已提交
1436 1437 1438 1439

          // Show form again on UI on failure
          if (isDiscussionForm && $notesContainer.length) {
            const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
1440
            this.replyToDiscussionNote(replyButton[0]);
K
Kushal Pandya 已提交
1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480
            $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 `gl.utils.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
     */
    Notes.prototype.updateComment = function(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
1481
      $noteBodyText.html(_.escape(formContent));
1482
      $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
K
Kushal Pandya 已提交
1483 1484 1485 1486 1487 1488 1489
      $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
      gl.utils.ajaxPost(formAction, formData)
        .then((note) => {
          // Submission successful! render final note element
1490
          this.updateNote(note, $editingNote);
K
Kushal Pandya 已提交
1491 1492 1493
        })
        .fail(() => {
          // Submission failed, revert back to original note
1494
          $noteBodyText.html(_.escape(cachedNoteBodyText));
K
Kushal Pandya 已提交
1495 1496 1497 1498 1499 1500 1501 1502 1503 1504
          $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'));
    };

F
Fatih Acet 已提交
1505 1506
    return Notes;
  })();
1507
}).call(window);