BigW Consortium Gitlab

notes.js.coffee 19.7 KB
Newer Older
1
#= require autosave
2
#= require autosize
3 4 5 6 7 8
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
#= require jquery.atwho
#= require task_list

9
class @Notes
10 11
  @interval: null

12
  constructor: (notes_url, note_ids, last_fetched_at, view) ->
13 14
    @notes_url = notes_url
    @note_ids = note_ids
15
    @last_fetched_at = last_fetched_at
16
    @view = view
17
    @noteable_url = document.URL
18
    @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge")
19 20
    @basePollingInterval = 15000
    @maxPollingSteps = 4
21

22 23
    @cleanBinding()
    @addBinding()
24
    @setPollingInterval()
25
    @setupMainTargetNoteForm()
26
    @initTaskList()
27 28 29 30 31 32

  addBinding: ->
    # add note to UI after creation
    $(document).on "ajax:success", ".js-main-target-form", @addNote
    $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote

33 34 35
    # catch note ajax errors
    $(document).on "ajax:error", ".js-main-target-form", @addNoteError

36
    # change note in UI after update
37
    $(document).on "ajax:success", "form.edit-note", @updateNote
38 39

    # Edit note link
40
    $(document).on "click", ".js-note-edit", @showEditForm
41 42
    $(document).on "click", ".note-edit-cancel", @cancelEdit

43
    # Reopen and close actions for Issue/MR combined with note form submit
44
    $(document).on "click", ".js-comment-button", @updateCloseButton
45
    $(document).on "keyup input", ".js-note-text", @updateTargetButtons
46

47 48 49 50 51 52 53
    # remove a note (in general)
    $(document).on "click", ".js-note-delete", @removeNote

    # delete note attachment
    $(document).on "click", ".js-note-attachment-delete", @removeAttachment

    # reset main target form after submit
54 55
    $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
    $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
56

57 58 59
    # reset main target form when clicking discard
    $(document).on "click", ".js-note-discard", @resetMainTargetForm

60 61 62
    # update the file name when an attachment is selected
    $(document).on "change", ".js-note-attachment-input", @updateFormAttachment

63 64 65 66 67 68
    # reply to diff/discussion notes
    $(document).on "click", ".js-discussion-reply-button", @replyToDiscussionNote

    # add diff note
    $(document).on "click", ".js-add-diff-note-button", @addDiffNote

69 70 71
    # hide diff note form
    $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm

72 73 74
    # fetch notes when tab becomes visible
    $(document).on "visibilitychange", @visibilityChange

75
    # when issue status changes, we need to refresh data
76 77
    $(document).on "issuable:change", @refresh

78 79 80
  cleanBinding: ->
    $(document).off "ajax:success", ".js-main-target-form"
    $(document).off "ajax:success", ".js-discussion-note-form"
81
    $(document).off "ajax:success", "form.edit-note"
82
    $(document).off "click", ".js-note-edit"
83 84 85 86
    $(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"
87
    $(document).off "ajax:success", ".js-main-target-form"
88 89
    $(document).off "click", ".js-discussion-reply-button"
    $(document).off "click", ".js-add-diff-note-button"
90
    $(document).off "visibilitychange"
91 92 93
    $(document).off "keyup", ".js-note-text"
    $(document).off "click", ".js-note-target-reopen"
    $(document).off "click", ".js-note-target-close"
94
    $(document).off "click", ".js-note-discard"
95

96 97 98
    $('.note .js-task-list-container').taskList('disable')
    $(document).off 'tasklist:changed', '.note .js-task-list-container'

99
  initRefresh: ->
100 101
    clearInterval(Notes.interval)
    Notes.interval = setInterval =>
102
      @refresh()
103
    , @pollingInterval
104 105

  refresh: ->
106 107
    return if @refreshing is true
    refreshing = true
108
    if not document.hidden and document.URL.indexOf(@noteable_url) is 0
109
      @getContent()
110 111 112 113

  getContent: ->
    $.ajax
      url: @notes_url
114
      data: "last_fetched_at=" + @last_fetched_at
115 116 117
      dataType: "json"
      success: (data) =>
        notes = data.notes
118
        @last_fetched_at = data.last_fetched_at
119
        @setPollingInterval(data.notes.length)
120
        $.each notes, (i, note) =>
121 122 123 124
          if note.discussion_with_diff_html?
            @renderDiscussionNote(note)
          else
            @renderNote(note)
125 126
      always: =>
        @refreshing = false
127

128
  ###
129
  Increase @pollingInterval up to 120 seconds on every function call,
130
  if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
131
  will reset to @basePollingInterval.
132 133 134 135 136

  Note: this function is used to gradually increase the polling interval
  if there aren't new notes coming from the server
  ###
  setPollingInterval: (shouldReset = true) ->
137
    nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1)
138
    if shouldReset
139 140 141
      @pollingInterval = @basePollingInterval
    else if @pollingInterval < nthInterval
      @pollingInterval *= 2
142 143

    @initRefresh()
144 145 146 147 148 149 150

  ###
  Render note in main comments area.

  Note: for rendering inline notes use renderDiscussionNote
  ###
  renderNote: (note) ->
151
    unless note.valid
152
      if note.award
153
        flash = new Flash('You have already used this award emoji!', 'alert')
154
        flash.pinTo('.header-content')
155 156
      return

157 158 159 160
    if note.award
      awards_handler.addAwardToEmojiBar(note.note)
      awards_handler.scrollToAwards()

161 162
    # render note if it not present in loaded list
    # or skip if rendered
163
    else if @isNewNote(note)
164
      @note_ids.push(note.id)
165

166
      $('ul.main-notes-list')
167 168
        .append(note.html)
        .syntaxHighlight()
169
      @initTaskList()
170
      @updateNotesCount(1)
171

Valery Sizov committed
172

173 174 175 176 177 178
  ###
  Check if note does not exists on page
  ###
  isNewNote: (note) ->
    $.inArray(note.id, @note_ids) == -1

179 180
  isParallelView: ->
    @view == 'parallel'
181 182 183 184 185 186 187

  ###
  Render note in discussion area.

  Note: for rendering inline notes use renderDiscussionNote
  ###
  renderDiscussionNote: (note) ->
188 189
    return unless @isNewNote(note)

190
    @note_ids.push(note.id)
191
    form = $("#new-discussion-note-form-#{note.discussion_id}")
192
    row = form.closest("tr")
193 194
    note_html = $(note.html)
    note_html.syntaxHighlight()
195 196

    # is this the first note of discussion?
197
    discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
198
    if discussionContainer.length is 0
199 200 201 202 203 204
      # insert the note and the reply button after the temp row
      row.after note.discussion_html

      # remove the note (will be added again below)
      row.next().find(".note").remove()

205
      # Before that, the container didn't exist
206
      discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
207

208
      # Add note to 'Changes' page discussions
209
      discussionContainer.append note_html
210

211
      # Init discussion on 'Discussion' page if it is merge request page
212
      if $('body').attr('data-page').indexOf('projects:merge_request') is 0
213
        $('ul.main-notes-list')
214 215
          .append(note.discussion_with_diff_html)
          .syntaxHighlight()
216 217
    else
      # append new note to all matching discussions
218
      discussionContainer.append note_html
219

220
    @updateNotesCount(1)
221 222 223 224 225 226 227 228

  ###
  Called in response the main target form has been successfully submitted.

  Removes any errors.
  Resets text and preview.
  Resets buttons.
  ###
229
  resetMainTargetForm: (e) =>
230 231 232 233 234 235
    form = $(".js-main-target-form")

    # remove validation errors
    form.find(".js-errors").remove()

    # reset text and preview
236
    form.find(".js-md-write-button").click()
237 238
    form.find(".js-note-text").val("").trigger "input"

239 240
    form.find(".js-note-text").data("autosave").reset()

241 242
    @updateTargetButtons(e)

243 244 245 246 247
  reenableTargetFormSubmitButton: ->
    form = $(".js-main-target-form")

    form.find(".js-note-text").trigger "input"

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
  ###
  Shows the main form and does some setup on it.

  Sets some hidden fields in the form.
  ###
  setupMainTargetNoteForm: ->

    # find the form
    form = $(".js-new-note-form")

    # insert the form after the button
    form.clone().replaceAll $(".js-main-target-form")
    form = form.prev("form")

    # show the form
    @setupNoteForm(form)

    # fix classes
    form.removeClass "js-new-note-form"
    form.addClass "js-main-target-form"

    # remove unnecessary fields and buttons
    form.find("#note_line_code").remove()
    form.find(".js-close-discussion-note-form").remove()

  ###
  General note form setup.

  deactivates the submit button when text is empty
  hides the preview button when text is empty
  setup GFM auto complete
  show the form
  ###
  setupNoteForm: (form) ->
    disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button")
    form.removeClass "js-new-note-form"
284
    form.find('.div-dropzone').remove()
285

286 287 288
    # hide discard button
    form.find('.js-note-discard').hide()

289
    # setup preview buttons
290
    previewButton = form.find(".js-md-preview-button")
291 292 293 294

    textarea = form.find(".js-note-text")

    textarea.on "input", ->
295 296 297 298 299
      if $(this).val().trim() isnt ""
        previewButton.removeClass("turn-off").addClass "turn-on"
      else
        previewButton.removeClass("turn-on").addClass "turn-off"

Robert Speicher committed
300
    autosize(textarea)
301 302 303 304 305 306 307
    new Autosave textarea, [
      "Note"
      form.find("#note_commit_id").val()
      form.find("#note_line_code").val()
      form.find("#note_noteable_type").val()
      form.find("#note_noteable_id").val()
    ]
308 309 310 311

    # remove notify commit author checkbox for non-commit notes
    form.find(".js-notify-commit-author").remove()  if form.find("#note_noteable_type").val() isnt "Commit"
    GitLab.GfmAutoComplete.setup()
312
    new DropzoneInput(form)
313 314 315 316 317 318 319 320 321 322
    form.show()

  ###
  Called in response to the new note form being submitted

  Adds new note to list.
  ###
  addNote: (xhr, note, status) =>
    @renderNote(note)

323 324 325 326
  addNoteError: (xhr, note, status) =>
    flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
    flash.pinTo('.md-area')

327 328 329 330 331 332 333 334
  ###
  Called in response to the new note form being submitted

  Adds new note to list.
  ###
  addDiscussionNote: (xhr, note, status) =>
    @renderDiscussionNote(note)

335
    # cleanup after successfully creating a diff/discussion note
336
    @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}"))
337

338 339 340 341 342
  ###
  Called in response to the edit note form being submitted

  Updates the current note field.
  ###
343 344 345 346 347 348 349
  updateNote: (_xhr, note, _status) =>
    # Convert returned HTML to a jQuery object so we can modify it further
    $html = $(note.html)
    $html.syntaxHighlight()
    $html.find('.js-task-list-container').taskList('enable')

    # Find the note's `li` element by ID and replace it with the updated HTML
350
    $note_li = $('.note-row-' + note.id)
351
    $note_li.replaceWith($html)
352 353 354 355 356 357 358 359

  ###
  Called in response to clicking the edit note link

  Replaces the note text with the note edit form
  Adds a hidden div with the original content of the note to fill the edit note form with
  if the user cancels
  ###
360 361 362
  showEditForm: (e) ->
    e.preventDefault()
    note = $(this).closest(".note")
363
    note.find(".note-body > .note-text").hide()
364
    note.find(".note-header").hide()
365 366 367 368 369 370
    form = note.find(".note-edit-form")
    isNewForm = form.is(':not(.gfm-form)')
    if isNewForm
      form.addClass('gfm-form')
    form.addClass('current-note-edit-form')
    form.show()
371 372 373

    # Show the attachment delete link
    note.find(".js-note-attachment-delete").show()
374 375

    # Setup markdown form
376 377 378
    if isNewForm
      GitLab.GfmAutoComplete.setup()
      new DropzoneInput(form)
379

380 381
    textarea = form.find("textarea")
    textarea.focus()
382 383 384

    if isNewForm
      autosize(textarea)
385

386
    # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
387
    # The textarea has the correct value, Chrome just won't show it unless we
388 389 390 391
    # modify it, so let's clear it and re-set it!
    value = textarea.val()
    textarea.val ""
    textarea.val value
392

393 394
    if isNewForm
      disableButtonIfEmptyField textarea, form.find(".js-comment-button")
395 396 397 398 399 400 401 402 403

  ###
  Called in response to clicking the edit note link

  Hides edit form
  ###
  cancelEdit: (e) ->
    e.preventDefault()
    note = $(this).closest(".note")
404
    note.find(".note-body > .note-text").show()
405
    note.find(".note-header").show()
406 407 408
    note.find(".current-note-edit-form")
      .removeClass("current-note-edit-form")
      .hide()
409 410 411 412 413 414 415

  ###
  Called in response to deleting a note of any kind.

  Removes the actual note from view.
  Removes the whole discussion if the last note is being removed.
  ###
416
  removeNote: (e) =>
417
    noteId = $(e.currentTarget)
418 419
               .closest(".note")
               .attr("id")
420 421 422 423 424 425

    # 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.
    $(".note[id='#{noteId}']").each (i, el) =>
      note  = $(el)
426
      notes = note.closest(".notes")
427

428 429
      # check if this is the last note for this line
      if notes.find(".note").length is 1
430

431 432
        # "Discussions" tab
        notes.closest(".timeline-entry").remove()
433

434
        # "Changes" tab / commit view
435
        notes.closest("tr").remove()
436

437
      note.remove()
438

439 440
    # Decrement the "Discussions" counter only once
    @updateNotesCount(-1)
441

442 443 444 445 446 447 448 449 450
  ###
  Called in response to clicking the delete attachment link

  Removes the attachment wrapper view, including image tag if it exists
  Resets the note editing form
  ###
  removeAttachment: ->
    note = $(this).closest(".note")
    note.find(".note-attachment").remove()
451
    note.find(".note-body > .note-text").show()
452 453
    note.find(".note-header").show()
    note.find(".current-note-edit-form").remove()
454 455 456 457 458 459 460 461

  ###
  Called when clicking on the "reply" button for a diff line.

  Shows the note form below the notes.
  ###
  replyToDiscussionNote: (e) =>
    form = $(".js-new-note-form")
462
    replyLink = $(e.target).closest(".js-discussion-reply-button")
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
    replyLink.hide()

    # insert the form after the button
    form.clone().insertAfter replyLink

    # show the form
    @setupDiscussionNoteForm(replyLink, replyLink.next("form"))

  ###
  Shows the diff or discussion form and does some setup on it.

  Sets some hidden fields in the form.

  Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
  and "noteableId" data attributes set.
  ###
  setupDiscussionNoteForm: (dataHolder, form) =>
    # setup note target
481
    form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
482
    form.find("#line_type").val dataHolder.data("lineType")
483 484 485 486
    form.find("#note_commit_id").val dataHolder.data("commitId")
    form.find("#note_line_code").val dataHolder.data("lineCode")
    form.find("#note_noteable_type").val dataHolder.data("noteableType")
    form.find("#note_noteable_id").val dataHolder.data("noteableId")
487 488 489 490 491
    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'))
492 493 494 495 496 497 498 499 500 501 502 503
    @setupNoteForm form
    form.find(".js-note-text").focus()
    form.addClass "js-discussion-note-form"

  ###
  Called when clicking on the "add a comment" button on the side of a diff line.

  Inserts a temporary row for the form below the line.
  Sets up the form and shows it.
  ###
  addDiffNote: (e) =>
    e.preventDefault()
Dmitriy Zaporozhets committed
504
    link = e.currentTarget
505 506 507
    form = $(".js-new-note-form")
    row = $(link).closest("tr")
    nextRow = row.next()
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
    hasNotes = nextRow.is(".notes_holder")
    addForm = false
    targetContent = ".notes_content"
    rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>"

    # In parallel view, look inside the correct left/right pane
    if @isParallelView()
      lineType = $(link).data("lineType")
      targetContent += "." + lineType
      rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>"

    if hasNotes
      notesContent = nextRow.find(targetContent)
      if notesContent.length
        replyButton = notesContent.find(".js-discussion-reply-button:visible")
        if replyButton.length
          e.target = replyButton[0]
          $.proxy(@replyToDiscussionNote, replyButton[0], e).call()
        else
          # In parallel view, the form may not be present in one of the panes
          noteForm = notesContent.find(".js-discussion-note-form")
          if noteForm.length == 0
            addForm = true
531 532
    else
      # add a notes row and insert the form
533 534 535 536 537 538
      row.after rowCssToAdd
      addForm = true

    if addForm
      newForm = form.clone()
      newForm.appendTo row.next().find(targetContent)
539 540

      # show the form
541
      @setupDiscussionNoteForm $(link), newForm
542 543 544 545 546 547 548 549 550 551

  ###
  Called in response to "cancel" on a diff note form.

  Shows the reply button again.
  Removes the form and if necessary it's temporary row.
  ###
  removeDiscussionNoteForm: (form)->
    row = form.closest("tr")

552 553
    form.find(".js-note-text").data("autosave").reset()

554 555 556 557 558 559 560 561 562
    # show the reply button (will only work for replies)
    form.prev(".js-discussion-reply-button").show()
    if row.is(".js-temp-notes-holder")
      # remove temporary row for diff lines
      row.remove()
    else
      # only remove the form
      form.remove()

563 564 565 566 567 568 569

  cancelDiscussionForm: (e) =>
    e.preventDefault()
    form = $(".js-new-note-form")
    form = $(e.target).closest(".js-discussion-note-form")
    @removeDiscussionNoteForm(form)

570 571 572 573 574 575 576 577 578 579 580 581
  ###
  Called after an attachment file has been selected.

  Updates the file name for the selected attachment.
  ###
  updateFormAttachment: ->
    form = $(this).closest("form")

    # get only the basename
    filename = $(this).val().replace(/^.*[\\\/]/, "")
    form.find(".js-attachment-filename").text filename

582 583 584 585 586 587
  ###
  Called when the tab visibility changes
  ###
  visibilityChange: =>
    @refresh()

588 589 590
  updateCloseButton: (e) =>
    textarea = $(e.target)
    form = textarea.parents('form')
591 592
    closebtn = form.find('.js-note-target-close')
    closebtn.text(closebtn.data('original-text'))
593

594 595 596
  updateTargetButtons: (e) =>
    textarea = $(e.target)
    form = textarea.parents('form')
597 598 599 600
    reopenbtn = form.find('.js-note-target-reopen')
    closebtn = form.find('.js-note-target-close')
    discardbtn = form.find('.js-note-discard')

601
    if textarea.val().trim().length > 0
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
      reopentext = reopenbtn.data('alternative-text')
      closetext = closebtn.data('alternative-text')

      if reopenbtn.text() isnt reopentext
        reopenbtn.text(reopentext)

      if closebtn.text() isnt 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')
        discardbtn.show()
619
    else
620 621 622 623 624 625 626
      reopentext = reopenbtn.data('original-text')
      closetext = closebtn.data('original-text')

      if reopenbtn.text() isnt reopentext
        reopenbtn.text(reopentext)

      if closebtn.text() isnt closetext
627
        closebtn.text(closetext)
628 629 630 631 632 633 634 635 636

      if reopenbtn.is(':not(.btn-comment-and-reopen)')
        reopenbtn.removeClass('btn-comment-and-reopen')

      if closebtn.is(':not(.btn-comment-and-close)')
        closebtn.removeClass('btn-comment-and-close')

      if discardbtn.is(':visible')
        discardbtn.hide()
637 638 639 640 641

  initTaskList: ->
    @enableTaskList()
    $(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList

642 643 644
  enableTaskList: ->
    $('.note .js-task-list-container').taskList('enable')

645 646
  updateTaskList: ->
    $('form', this).submit()
647

648 649
  updateNotesCount: (updateCount) ->
    @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount)