notes.js 37.9 KB
Newer Older
1
/* 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 */
2 3 4
/* global Flash */
/* global Autosave */
/* global ResolveService */
5
/* global mrRefreshWidgetUrl */
Fatih Acet's avatar
Fatih Acet committed
6

7
import Cookies from 'js-cookie';
8
import CommentTypeToggle from './comment_type_toggle';
9

10 11 12 13 14 15 16
require('./autosave');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
17
require('./task_list');
Fatih Acet's avatar
Fatih Acet committed
18 19

(function() {
20
  var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
Fatih Acet's avatar
Fatih Acet committed
21 22

  this.Notes = (function() {
23
    const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
Fatih Acet's avatar
Fatih Acet committed
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

    Notes.interval = null;

    function Notes(notes_url, note_ids, last_fetched_at, view) {
      this.updateTargetButtons = bind(this.updateTargetButtons, this);
      this.updateCloseButton = bind(this.updateCloseButton, this);
      this.visibilityChange = bind(this.visibilityChange, this);
      this.cancelDiscussionForm = bind(this.cancelDiscussionForm, this);
      this.addDiffNote = bind(this.addDiffNote, this);
      this.setupDiscussionNoteForm = bind(this.setupDiscussionNoteForm, this);
      this.replyToDiscussionNote = bind(this.replyToDiscussionNote, this);
      this.removeNote = bind(this.removeNote, this);
      this.cancelEdit = bind(this.cancelEdit, this);
      this.updateNote = bind(this.updateNote, this);
      this.addDiscussionNote = bind(this.addDiscussionNote, this);
      this.addNoteError = bind(this.addNoteError, this);
      this.addNote = bind(this.addNote, this);
      this.resetMainTargetForm = bind(this.resetMainTargetForm, this);
      this.refresh = bind(this.refresh, this);
      this.keydownNoteText = bind(this.keydownNoteText, this);
44
      this.toggleCommitList = bind(this.toggleCommitList, this);
Fatih Acet's avatar
Fatih Acet committed
45 46 47 48 49 50 51 52 53 54 55
      this.notes_url = notes_url;
      this.note_ids = note_ids;
      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();
56 57
      this.taskList = new gl.TaskList({
        dataType: 'note',
58
        fieldName: 'note',
59
        selector: '.notes'
60
      });
61
      this.collapseLongCommitList();
62
      this.setViewType(view);
63 64 65 66

      // 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()
67
          .addClass('mr-note-edit-form').insertAfter('.note-edit-form');
68
      }
Fatih Acet's avatar
Fatih Acet committed
69 70
    }

71 72 73 74
    Notes.prototype.setViewType = function(view) {
      this.view = Cookies.get('diff_view') || view;
    };

Fatih Acet's avatar
Fatih Acet committed
75
    Notes.prototype.addBinding = function() {
76
      // add note to UI after creation
Fatih Acet's avatar
Fatih Acet committed
77 78
      $(document).on("ajax:success", ".js-main-target-form", this.addNote);
      $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote);
79
      // catch note ajax errors
Fatih Acet's avatar
Fatih Acet committed
80
      $(document).on("ajax:error", ".js-main-target-form", this.addNoteError);
81
      // change note in UI after update
Fatih Acet's avatar
Fatih Acet committed
82
      $(document).on("ajax:success", "form.edit-note", this.updateNote);
83
      // Edit note link
84
      $(document).on("click", ".js-note-edit", this.showEditForm.bind(this));
Fatih Acet's avatar
Fatih Acet committed
85
      $(document).on("click", ".note-edit-cancel", this.cancelEdit);
86
      // Reopen and close actions for Issue/MR combined with note form submit
Fatih Acet's avatar
Fatih Acet committed
87 88
      $(document).on("click", ".js-comment-button", this.updateCloseButton);
      $(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
89
      // resolve a discussion
90
      $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
91
      // remove a note (in general)
Fatih Acet's avatar
Fatih Acet committed
92
      $(document).on("click", ".js-note-delete", this.removeNote);
93
      // delete note attachment
Fatih Acet's avatar
Fatih Acet committed
94
      $(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
95
      // reset main target form after submit
Fatih Acet's avatar
Fatih Acet committed
96 97
      $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
      $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm);
98
      // reset main target form when clicking discard
Fatih Acet's avatar
Fatih Acet committed
99
      $(document).on("click", ".js-note-discard", this.resetMainTargetForm);
100
      // update the file name when an attachment is selected
Fatih Acet's avatar
Fatih Acet committed
101
      $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
102
      // reply to diff/discussion notes
Fatih Acet's avatar
Fatih Acet committed
103
      $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote);
104
      // add diff note
Fatih Acet's avatar
Fatih Acet committed
105
      $(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
106
      // hide diff note form
Fatih Acet's avatar
Fatih Acet committed
107
      $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
108 109
      // toggle commit list
      $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList);
110
      // fetch notes when tab becomes visible
Fatih Acet's avatar
Fatih Acet committed
111
      $(document).on("visibilitychange", this.visibilityChange);
112
      // when issue status changes, we need to refresh data
Fatih Acet's avatar
Fatih Acet committed
113
      $(document).on("issuable:change", this.refresh);
114
      // when a key is clicked on the notes
Fatih Acet's avatar
Fatih Acet committed
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
      return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
    };

    Notes.prototype.cleanBinding = function() {
      $(document).off("ajax:success", ".js-main-target-form");
      $(document).off("ajax:success", ".js-discussion-note-form");
      $(document).off("ajax:success", "form.edit-note");
      $(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("ajax:complete", ".js-main-target-form");
      $(document).off("ajax:success", ".js-main-target-form");
      $(document).off("click", ".js-discussion-reply-button");
      $(document).off("click", ".js-add-diff-note-button");
      $(document).off("visibilitychange");
      $(document).off("keyup", ".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");
136
      $(document).off('click', '.js-comment-resolve-button');
137
      $(document).off("click", '.system-note-commit-list-toggler');
Fatih Acet's avatar
Fatih Acet committed
138 139
    };

140 141
    Notes.prototype.initCommentTypeToggle = function (form) {
      this.commentTypeToggle = new CommentTypeToggle(
142 143
        form.querySelector('.js-comment-type-dropdown .dropdown-toggle'),
        form.querySelector('.js-comment-type-dropdown .dropdown-menu'),
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
144
        form.querySelector('#note_type'),
145
        form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'),
146
        form.querySelector('.js-note-target-close:not(.hidden)') || form.querySelector('.js-note-target-reopen'),
147 148 149 150 151
      );

      this.commentTypeToggle.initDroplab();
    };

Fatih Acet's avatar
Fatih Acet committed
152 153
    Notes.prototype.keydownNoteText = function(e) {
      var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
154
      if (gl.utils.isMetaKey(e)) {
Fatih Acet's avatar
Fatih Acet committed
155 156
        return;
      }
157

Fatih Acet's avatar
Fatih Acet committed
158
      $textarea = $(e.target);
159
      // Edit previous note when UP arrow is hit
Fatih Acet's avatar
Fatih Acet committed
160 161 162 163 164 165 166 167 168 169 170
      switch (e.which) {
        case 38:
          if ($textarea.val() !== '') {
            return;
          }
          myLastNote = $("li.note[data-author-id='" + gon.current_user_id + "'][data-editable]:last");
          if (myLastNote.length) {
            myLastNoteEditBtn = myLastNote.find('.js-note-edit');
            return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
          }
          break;
171
        // Cancel creating diff note or editing any note when ESCAPE is hit
Fatih Acet's avatar
Fatih Acet committed
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
        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() {
Douwe Maan's avatar
Douwe Maan committed
207
      if (!document.hidden) {
Fatih Acet's avatar
Fatih Acet committed
208 209 210 211 212 213 214 215 216 217 218
        return this.getContent();
      }
    };

    Notes.prototype.getContent = function() {
      if (this.refreshing) {
        return;
      }
      this.refreshing = true;
      return $.ajax({
        url: this.notes_url,
219
        headers: { "X-Last-Fetched-At": this.last_fetched_at },
Fatih Acet's avatar
Fatih Acet committed
220 221 222 223 224 225 226 227
        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) {
228
              _this.renderNote(note);
Fatih Acet's avatar
Fatih Acet committed
229 230 231 232 233 234 235 236 237 238 239 240 241 242
            });
          };
        })(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.
243

Fatih Acet's avatar
Fatih Acet committed
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
    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();
    };

262
    Notes.prototype.handleCreateChanges = function(note) {
mhasbini's avatar
mhasbini committed
263
      var votesBlock;
264 265 266 267
      if (typeof note === 'undefined') {
        return;
      }

mhasbini's avatar
mhasbini committed
268 269 270 271 272 273 274 275 276 277
      if (note.commands_changes) {
        if ('merge' in note.commands_changes) {
          $.get(mrRefreshWidgetUrl);
        }

        if ('emoji_award' in note.commands_changes) {
          votesBlock = $('.js-awards-block').eq(0);
          gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.commands_changes.emoji_award);
          return gl.awardsHandler.scrollToAwards();
        }
278 279 280
      }
    };

Fatih Acet's avatar
Fatih Acet committed
281 282
    /*
    Render note in main comments area.
283

Fatih Acet's avatar
Fatih Acet committed
284 285 286
    Note: for rendering inline notes use renderDiscussionNote
     */

287
    Notes.prototype.renderNote = function(note, $form) {
mhasbini's avatar
mhasbini committed
288
      var $notesList;
289
      if (note.discussion_html != null) {
290
        return this.renderDiscussionNote(note, $form);
291 292
      }

Fatih Acet's avatar
Fatih Acet committed
293
      if (!note.valid) {
mhasbini's avatar
mhasbini committed
294 295 296
        if (note.errors.commands_only) {
          new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
          this.refresh();
Fatih Acet's avatar
Fatih Acet committed
297 298 299
        }
        return;
      }
mhasbini's avatar
mhasbini committed
300 301

      if (this.isNewNote(note)) {
Fatih Acet's avatar
Fatih Acet committed
302 303 304
        this.note_ids.push(note.id);
        $notesList = $('ul.main-notes-list');
        $notesList.append(note.html).syntaxHighlight();
305
        // Update datetime format on the recent note
Fatih Acet's avatar
Fatih Acet committed
306
        gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
307
        this.collapseLongCommitList();
308
        this.taskList.init();
309
        this.refresh();
Fatih Acet's avatar
Fatih Acet committed
310 311 312 313 314 315 316 317 318 319 320 321 322
        return this.updateNotesCount(1);
      }
    };

    /*
    Check if note does not exists on page
     */

    Notes.prototype.isNewNote = function(note) {
      return $.inArray(note.id, this.note_ids) === -1;
    };

    Notes.prototype.isParallelView = function() {
323
      return Cookies.get('diff_view') === 'parallel';
Fatih Acet's avatar
Fatih Acet committed
324 325 326 327
    };

    /*
    Render note in discussion area.
328

Fatih Acet's avatar
Fatih Acet committed
329 330 331
    Note: for rendering inline notes use renderDiscussionNote
     */

332
    Notes.prototype.renderDiscussionNote = function(note, $form) {
333
      var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
Fatih Acet's avatar
Fatih Acet committed
334 335 336 337
      if (!this.isNewNote(note)) {
        return;
      }
      this.note_ids.push(note.id);
338
      form = $form || $(".js-discussion-note-form[data-discussion-id='" + note.discussion_id + "']");
Fatih Acet's avatar
Fatih Acet committed
339
      row = form.closest("tr");
340 341
      lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
      diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
Fatih Acet's avatar
Fatih Acet committed
342
      note_html = $(note.html);
343
      note_html.renderGFM();
344
      // is this the first note of discussion?
Fatih Acet's avatar
Fatih Acet committed
345
      discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
346 347
      if (!discussionContainer.length) {
        discussionContainer = form.closest('.discussion').find('.notes');
Fatih Acet's avatar
Fatih Acet committed
348 349
      }
      if (discussionContainer.length === 0) {
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
        if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
          // insert the note and the reply button after the temp row
          row.after(note.diff_discussion_html);

          // remove the note (will be added again below)
          row.next().find(".note").remove();
        } else {
          // Merge new discussion HTML in
          var $discussion = $(note.diff_discussion_html);
          var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]');
          var contentContainerClass = '.' + $notes.closest('.notes_content')
            .attr('class')
            .split(' ')
            .join('.');

          // remove the note (will be added again below)
          $notes.find('.note').remove();

          row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
        }
370
        // Before that, the container didn't exist
Fatih Acet's avatar
Fatih Acet committed
371
        discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
372
        // Add note to 'Changes' page discussions
Fatih Acet's avatar
Fatih Acet committed
373
        discussionContainer.append(note_html);
374
        // Init discussion on 'Discussion' page if it is merge request page
375
        if ($('body').attr('data-page').indexOf('projects:merge_request') === 0 || !note.diff_discussion_html) {
376
          $('ul.main-notes-list').append(note.discussion_html).renderGFM();
Fatih Acet's avatar
Fatih Acet committed
377 378
        }
      } else {
379
        // append new note to all matching discussions
Fatih Acet's avatar
Fatih Acet committed
380 381
        discussionContainer.append(note_html);
      }
382

Douwe Maan's avatar
Douwe Maan committed
383
      if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_resolvable) {
384
        gl.diffNotesCompileComponents();
385
        this.renderDiscussionAvatar(diffAvatarContainer, note);
386 387
      }

388
      gl.utils.localTimeAgo($('.js-timeago'), false);
Fatih Acet's avatar
Fatih Acet committed
389 390 391
      return this.updateNotesCount(1);
    };

392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
    Notes.prototype.getLineHolder = function(changesDiscussionContainer) {
      return $(changesDiscussionContainer).closest('.notes_holder')
        .prevAll('.line_holder')
        .first()
        .get(0);
    };

    Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) {
      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');
        avatarHolder.setAttribute('discussion-id', note.discussion_id);

        diffAvatarContainer.append(avatarHolder);

        gl.diffNotesCompileComponents();
      }

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

Fatih Acet's avatar
Fatih Acet committed
417 418
    /*
    Called in response the main target form has been successfully submitted.
419

Fatih Acet's avatar
Fatih Acet committed
420 421 422 423 424 425 426 427
    Removes any errors.
    Resets text and preview.
    Resets buttons.
     */

    Notes.prototype.resetMainTargetForm = function(e) {
      var form;
      form = $(".js-main-target-form");
428
      // remove validation errors
Fatih Acet's avatar
Fatih Acet committed
429
      form.find(".js-errors").remove();
430
      // reset text and preview
Fatih Acet's avatar
Fatih Acet committed
431 432 433
      form.find(".js-md-write-button").click();
      form.find(".js-note-text").val("").trigger("input");
      form.find(".js-note-text").data("autosave").reset();
434 435 436 437 438 439

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

      this.updateTargetButtons(e);
Fatih Acet's avatar
Fatih Acet committed
440 441 442 443 444 445 446 447 448 449
    };

    Notes.prototype.reenableTargetFormSubmitButton = function() {
      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.
450

Fatih Acet's avatar
Fatih Acet committed
451 452 453 454 455
    Sets some hidden fields in the form.
     */

    Notes.prototype.setupMainTargetNoteForm = function() {
      var form;
456
      // find the form
Fatih Acet's avatar
Fatih Acet committed
457
      form = $(".js-new-note-form");
458
      // Set a global clone of the form for later cloning
Fatih Acet's avatar
Fatih Acet committed
459
      this.formClone = form.clone();
460
      // show the form
Fatih Acet's avatar
Fatih Acet committed
461
      this.setupNoteForm(form);
462
      // fix classes
Fatih Acet's avatar
Fatih Acet committed
463 464 465 466
      form.removeClass("js-new-note-form");
      form.addClass("js-main-target-form");
      form.find("#note_line_code").remove();
      form.find("#note_position").remove();
467
      form.find("#note_type").val('');
Douwe Maan's avatar
Douwe Maan committed
468
      form.find("#in_reply_to_discussion_id").remove();
469
      form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
470 471
      this.parentTimeline = form.parents('.timeline');

472 473 474
      if (form.length) {
        this.initCommentTypeToggle(form.get(0));
      }
Fatih Acet's avatar
Fatih Acet committed
475 476 477 478
    };

    /*
    General note form setup.
479

Fatih Acet's avatar
Fatih Acet committed
480 481 482 483 484 485 486
    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) {
487
      var textarea, key;
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
488
      new gl.GLForm(form);
Fatih Acet's avatar
Fatih Acet committed
489
      textarea = form.find(".js-note-text");
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
      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("#in_reply_to_discussion_id").val(),

        // LegacyDiffNote
        form.find("#note_line_code").val(),

        // DiffNote
        form.find("#note_position").val()
      ];
      return new Autosave(textarea, key);
Fatih Acet's avatar
Fatih Acet committed
505 506 507 508
    };

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

Fatih Acet's avatar
Fatih Acet committed
510 511 512 513
    Adds new note to list.
     */

    Notes.prototype.addNote = function(xhr, note, status) {
514
      this.handleCreateChanges(note);
Fatih Acet's avatar
Fatih Acet committed
515 516 517 518 519 520 521 522 523
      return this.renderNote(note);
    };

    Notes.prototype.addNoteError = function(xhr, note, status) {
      return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
    };

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

Fatih Acet's avatar
Fatih Acet committed
525 526 527 528
    Adds new note to list.
     */

    Notes.prototype.addDiscussionNote = function(xhr, note, status) {
529 530 531
      var $form = $(xhr.target);

      if ($form.attr('data-resolve-all') != null) {
532 533 534
        var projectPath = $form.data('project-path');
        var discussionId = $form.data('discussion-id');
        var mergeRequestId = $form.data('noteable-iid');
535 536

        if (ResolveService != null) {
537
          ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
538 539
        }
      }
540

541
      this.renderNote(note, $form);
542
      // cleanup after successfully creating a diff/discussion note
543
      this.removeDiscussionNoteForm($form);
Fatih Acet's avatar
Fatih Acet committed
544 545 546 547
    };

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

Fatih Acet's avatar
Fatih Acet committed
549 550 551 552 553
    Updates the current note field.
     */

    Notes.prototype.updateNote = function(_xhr, note, _status) {
      var $html, $note_li;
554
      // Convert returned HTML to a jQuery object so we can modify it further
Fatih Acet's avatar
Fatih Acet committed
555
      $html = $(note.html);
556
      this.revertNoteEditForm();
Fatih Acet's avatar
Fatih Acet committed
557
      gl.utils.localTimeAgo($('.js-timeago', $html));
558
      $html.renderGFM();
Fatih Acet's avatar
Fatih Acet committed
559
      $html.find('.js-task-list-container').taskList('enable');
560
      // Find the note's `li` element by ID and replace it with the updated HTML
Fatih Acet's avatar
Fatih Acet committed
561
      $note_li = $('.note-row-' + note.id);
562 563 564

      $note_li.replaceWith($html);

565 566
      if (typeof gl.diffNotesCompileComponents !== 'undefined') {
        gl.diffNotesCompileComponents();
567
      }
Fatih Acet's avatar
Fatih Acet committed
568 569
    };

570 571 572 573 574 575 576 577 578 579
    Notes.prototype.checkContentToAllowEditing = function($el) {
      var initialContent = $el.find('.original-note-content').text().trim();
      var currentContent = $el.find('.note-textarea').val();
      var isAllowed = true;

      if (currentContent === initialContent) {
        this.removeNoteEditForm($el);
      }
      else {
        var $buttons = $el.find('.note-form-actions');
Fatih Acet's avatar
Fatih Acet committed
580
        var isWidgetVisible = gl.utils.isInViewport($el.get(0));
581

582
        if (!isWidgetVisible) {
Fatih Acet's avatar
Fatih Acet committed
583
          gl.utils.scrollToElement($el);
584 585 586 587 588 589 590
        }

        $el.find('.js-edit-warning').show();
        isAllowed = false;
      }

      return isAllowed;
591
    };
592

Fatih Acet's avatar
Fatih Acet committed
593 594
    /*
    Called in response to clicking the edit note link
595

Fatih Acet's avatar
Fatih Acet committed
596 597
    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
598
    */
Fatih Acet's avatar
Fatih Acet committed
599 600
    Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) {
      e.preventDefault();
601

602
      var $target = $(e.target);
603
      var $editForm = $(this.getEditFormSelector($target));
604
      var $note = $target.closest('.note');
605
      var $currentlyEditing = $('.note.is-editting:visible');
606

607 608 609 610 611 612 613 614
      if ($currentlyEditing.length) {
        var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing);

        if (!isEditAllowed) {
          return;
        }
      }

615
      $note.find('.js-note-attachment-delete').show();
616 617 618
      $editForm.addClass('current-note-edit-form');
      $note.addClass('is-editting');
      this.putEditFormInPlace($target);
Fatih Acet's avatar
Fatih Acet committed
619 620 621 622
    };

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

Fatih Acet's avatar
Fatih Acet committed
624 625 626 627 628
    Hides edit form and restores the original note text to the editor textarea.
     */

    Notes.prototype.cancelEdit = function(e) {
      e.preventDefault();
629 630
      var $target = $(e.target);
      var note = $target.closest('.note');
631
      note.find('.js-edit-warning').hide();
632
      this.revertNoteEditForm($target);
Fatih Acet's avatar
Fatih Acet committed
633 634 635
      return this.removeNoteEditForm(note);
    };

636 637 638 639
    Notes.prototype.revertNoteEditForm = function($target) {
      $target = $target || $('.note.is-editting:visible');
      var selector = this.getEditFormSelector($target);
      var $editForm = $(selector);
640 641 642

      $editForm.insertBefore('.notes-form');
      $editForm.find('.js-comment-button').enable();
643
      $editForm.find('.js-edit-warning').hide();
644
    };
645

646
    Notes.prototype.getEditFormSelector = function($el) {
647
      var selector = '.note-edit-form:not(.mr-note-edit-form)';
648 649

      if ($el.parents('#diffs').length) {
650
        selector = '.note-edit-form.mr-note-edit-form';
651 652 653 654
      }

      return selector;
    };
655

Fatih Acet's avatar
Fatih Acet committed
656
    Notes.prototype.removeNoteEditForm = function(note) {
657 658 659
      var form = note.find('.current-note-edit-form');
      note.removeClass('is-editting');
      form.removeClass('current-note-edit-form');
Fatih Acet's avatar
Fatih Acet committed
660
      form.find('.js-edit-warning').hide();
661
      // Replace markdown textarea text with original note text.
662
      return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
Fatih Acet's avatar
Fatih Acet committed
663 664 665 666
    };

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

Fatih Acet's avatar
Fatih Acet committed
668 669 670 671 672
    Removes the actual note from view.
    Removes the whole discussion if the last note is being removed.
     */

    Notes.prototype.removeNote = function(e) {
673 674 675 676 677 678 679 680
      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) {
681 682 683
        // 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.
Fatih Acet's avatar
Fatih Acet committed
684 685 686
        return function(i, el) {
          var note, notes;
          note = $(el);
687
          notes = note.closest(".discussion-notes");
688

689
          if (typeof gl.diffNotesCompileComponents !== 'undefined') {
690 691
            if (gl.diffNoteApps[noteElId]) {
              gl.diffNoteApps[noteElId].$destroy();
692 693 694
            }
          }

695 696
          note.remove();

697
          // check if this is the last note for this line
698 699 700
          if (notes.find(".note").length === 0) {
            var notesTr = notes.closest("tr");

701
            // "Discussions" tab
Fatih Acet's avatar
Fatih Acet committed
702
            notes.closest(".timeline-entry").remove();
703

704 705 706
            // The notes tr can contain multiple lists of notes, like on the parallel diff
            if (notesTr.find('.discussion-notes').length > 1) {
              notes.remove();
707
            } else {
708
              notesTr.remove();
709
            }
Fatih Acet's avatar
Fatih Acet committed
710 711 712
          }
        };
      })(this));
713
      // Decrement the "Discussions" counter only once
Fatih Acet's avatar
Fatih Acet committed
714 715 716 717 718
      return this.updateNotesCount(-1);
    };

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

Fatih Acet's avatar
Fatih Acet committed
720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
    Removes the attachment wrapper view, including image tag if it exists
    Resets the note editing form
     */

    Notes.prototype.removeAttachment = function() {
      var note;
      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.
735

Fatih Acet's avatar
Fatih Acet committed
736 737 738 739 740
    Shows the note form below the notes.
     */

    Notes.prototype.replyToDiscussionNote = function(e) {
      var form, replyLink;
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
741
      form = this.cleanForm(this.formClone.clone());
Fatih Acet's avatar
Fatih Acet committed
742
      replyLink = $(e.target).closest(".js-discussion-reply-button");
743
      // insert the form after the button
744 745 746 747
      replyLink
        .closest('.discussion-reply-holder')
        .hide()
        .after(form);
748
      // show the form
Fatih Acet's avatar
Fatih Acet committed
749 750 751 752 753
      return this.setupDiscussionNoteForm(replyLink, form);
    };

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

Fatih Acet's avatar
Fatih Acet committed
755
    Sets some hidden fields in the form.
756

757
    Note: dataHolder must have the "discussionId" and "lineCode" data attributes set.
Fatih Acet's avatar
Fatih Acet committed
758 759 760
     */

    Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
761
      // setup note target
762 763
      var discussionID = dataHolder.data("discussionId");

764 765 766 767
      if (discussionID) {
        form.attr("data-discussion-id", discussionID);
        form.find("#in_reply_to_discussion_id").val(discussionID);
      }
768

769
      form.attr("data-line-code", dataHolder.data("lineCode"));
Fatih Acet's avatar
Fatih Acet committed
770
      form.find("#line_type").val(dataHolder.data("lineType"));
771 772 773

      form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
      form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
Fatih Acet's avatar
Fatih Acet committed
774
      form.find("#note_commit_id").val(dataHolder.data("commitId"));
775 776 777
      form.find("#note_type").val(dataHolder.data("noteType"));

      // LegacyDiffNote
Fatih Acet's avatar
Fatih Acet committed
778
      form.find("#note_line_code").val(dataHolder.data("lineCode"));
779 780

      // DiffNote
Fatih Acet's avatar
Fatih Acet committed
781
      form.find("#note_position").val(dataHolder.attr("data-position"));
782

Fatih Acet's avatar
Fatih Acet committed
783
      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'));
784
      form.find('.js-note-target-close').remove();
785
      form.find('.js-note-new-discussion').remove();
Fatih Acet's avatar
Fatih Acet committed
786
      this.setupNoteForm(form);
787

Douwe Maan's avatar
Douwe Maan committed
788 789 790 791
      form
        .removeClass('js-main-target-form')
        .addClass("discussion-form js-discussion-note-form");

792
      if (typeof gl.diffNotesCompileComponents !== 'undefined') {
793
        var $commentBtn = form.find('comment-and-resolve-btn');
794
        $commentBtn.attr(':discussion-id', "'" + discussionID + "'");
Phil Hughes's avatar
Phil Hughes committed
795

796
        gl.diffNotesCompileComponents();
797 798
      }

Fatih Acet's avatar
Fatih Acet committed
799
      form.find(".js-note-text").focus();
800 801
      form
        .find('.js-comment-resolve-button')
802
        .attr('data-discussion-id', discussionID);
Fatih Acet's avatar
Fatih Acet committed
803 804 805 806
    };

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

Fatih Acet's avatar
Fatih Acet committed
808 809 810 811 812
    Inserts a temporary row for the form below the line.
    Sets up the form and shows it.
     */

    Notes.prototype.addDiffNote = function(e) {
813
      var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
Fatih Acet's avatar
Fatih Acet committed
814
      e.preventDefault();
815
      $link = $(e.currentTarget || e.target);
Fatih Acet's avatar
Fatih Acet committed
816 817 818 819
      row = $link.closest("tr");
      nextRow = row.next();
      hasNotes = nextRow.is(".notes_holder");
      addForm = false;
820 821
      notesContentSelector = ".notes_content";
      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>";
822
      isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
823
      // In parallel view, look inside the correct left/right pane
Fatih Acet's avatar
Fatih Acet committed
824 825
      if (this.isParallelView()) {
        lineType = $link.data("lineType");
826 827
        notesContentSelector += "." + 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>";
Fatih Acet's avatar
Fatih Acet committed
828
      }
829
      notesContentSelector += " .content";
830 831 832
      notesContent = nextRow.find(notesContentSelector);

      if (hasNotes && !isDiffCommentAvatar) {
833 834
        nextRow.show();
        notesContent = nextRow.find(notesContentSelector);
Fatih Acet's avatar
Fatih Acet committed
835
        if (notesContent.length) {
836
          notesContent.show();
Fatih Acet's avatar
Fatih Acet committed
837 838 839 840 841
          replyButton = notesContent.find(".js-discussion-reply-button:visible");
          if (replyButton.length) {
            e.target = replyButton[0];
            $.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
          } else {
842
            // In parallel view, the form may not be present in one of the panes
Fatih Acet's avatar
Fatih Acet committed
843 844 845 846 847 848
            noteForm = notesContent.find(".js-discussion-note-form");
            if (noteForm.length === 0) {
              addForm = true;
            }
          }
        }
849
      } else if (!isDiffCommentAvatar) {
850
        // add a notes row and insert the form
Fatih Acet's avatar
Fatih Acet committed
851
        row.after(rowCssToAdd);
852 853
        nextRow = row.next();
        notesContent = nextRow.find(notesContentSelector);
Fatih Acet's avatar
Fatih Acet committed
854
        addForm = true;
855 856 857 858 859 860 861
      } else {
        nextRow.show();
        notesContent.toggle(!notesContent.is(':visible'));

        if (!nextRow.find('.content:not(:empty)').is(':visible')) {
          nextRow.hide();
        }
Fatih Acet's avatar
Fatih Acet committed
862
      }
863

Fatih Acet's avatar
Fatih Acet committed
864
      if (addForm) {
865
        newForm = this.cleanForm(this.formClone.clone());
866
        newForm.appendTo(notesContent);
867
        // show the form
Fatih Acet's avatar
Fatih Acet committed
868 869 870 871 872 873
        return this.setupDiscussionNoteForm($link, newForm);
      }
    };

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

Fatih Acet's avatar
Fatih Acet committed
875 876 877 878 879 880 881 882 883 884
    Shows the reply button again.
    Removes the form and if necessary it's temporary row.
     */

    Notes.prototype.removeDiscussionNoteForm = function(form) {
      var glForm, row;
      row = form.closest("tr");
      glForm = form.data('gl-form');
      glForm.destroy();
      form.find(".js-note-text").data("autosave").reset();
885
      // show the reply button (will only work for replies)
886 887 888
      form
        .prev('.discussion-reply-holder')
        .show();
Fatih Acet's avatar
Fatih Acet committed
889
      if (row.is(".js-temp-notes-holder")) {
890
        // remove temporary row for diff lines
Fatih Acet's avatar
Fatih Acet committed
891 892
        return row.remove();
      } else {
893
        // only remove the form
Fatih Acet's avatar
Fatih Acet committed
894 895 896 897 898 899 900 901 902 903 904 905 906
        return form.remove();
      }
    };

    Notes.prototype.cancelDiscussionForm = function(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.
907

Fatih Acet's avatar
Fatih Acet committed
908 909 910 911 912 913
    Updates the file name for the selected attachment.
     */

    Notes.prototype.updateFormAttachment = function() {
      var filename, form;
      form = $(this).closest("form");
914
      // get only the basename
Fatih Acet's avatar
Fatih Acet committed
915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943
      filename = $(this).val().replace(/^.*[\\\/]/, "");
      return form.find(".js-attachment-filename").text(filename);
    };

    /*
    Called when the tab visibility changes
     */

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

    Notes.prototype.updateCloseButton = function(e) {
      var closebtn, form, textarea;
      textarea = $(e.target);
      form = textarea.parents('form');
      closebtn = form.find('.js-note-target-close');
      return closebtn.text(closebtn.data('original-text'));
    };

    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');
      if (textarea.val().trim().length > 0) {
        reopentext = reopenbtn.data('alternative-text');
944
        closetext = closebtn.attr('data-alternative-text');
Fatih Acet's avatar
Fatih Acet committed
945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980
        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();
        }
      }
    };

981
    Notes.prototype.putEditFormInPlace = function($el) {
982
      var $editForm = $(this.getEditFormSelector($el));
983 984 985 986 987 988 989 990 991 992
      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');

Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
993
      new gl.GLForm($editForm.find('form'));
994

995 996 997
      $editForm.find('form')
        .attr('action', postUrl)
        .attr('data-remote', 'true');
Fatih Acet's avatar
Fatih Acet committed
998 999
      $editForm.find('.js-form-target-id').val(targetId);
      $editForm.find('.js-form-target-type').val(targetType);
1000
      $editForm.find('.js-note-text').focus().val(originalContent);
Fatih Acet's avatar
Fatih Acet committed
1001 1002
      $editForm.find('.js-md-write-button').trigger('click');
      $editForm.find('.referenced-users').hide();
1003
    };
1004

Fatih Acet's avatar
Fatih Acet committed
1005
    Notes.prototype.updateNotesCount = function(updateCount) {
1006
      return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
Fatih Acet's avatar
Fatih Acet committed
1007 1008
    };

1009 1010 1011
    Notes.prototype.resolveDiscussion = function() {
      var $this = $(this);
      var discussionId = $this.attr('data-discussion-id');
1012 1013 1014 1015 1016

      $this
        .closest('form')
        .attr('data-discussion-id', discussionId)
        .attr('data-resolve-all', 'true')
1017
        .attr('data-project-path', $this.attr('data-project-path'));
1018 1019
    };

1020
    Notes.prototype.toggleCommitList = function(e) {
1021
      const $element = $(e.currentTarget);
1022 1023
      const $closestSystemCommitList = $element.siblings('.system-note-commit-list');

1024
      $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
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
      $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');
        }
      });
    };

1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062
    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;
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
1063
    };
1064

Fatih Acet's avatar
Fatih Acet committed
1065 1066
    return Notes;
  })();
1067
}).call(window);