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

C
Ciro Santilli 已提交
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
P
Phil Hughes 已提交
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

J
Jacob Schatz 已提交
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"
P
Phil Hughes 已提交
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

V
Valery Sizov 已提交
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
V
Vinnie Okada 已提交
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
V
Vinnie Okada 已提交
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"

R
Robert Speicher 已提交
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
  updateNote: (_xhr, note, _status) =>
    # Convert returned HTML to a jQuery object so we can modify it further
    $html = $(note.html)
P
Phil Hughes 已提交
346
    $('.js-timeago', $html).timeago()
347 348 349 350
    $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
351
    $note_li = $('.note-row-' + note.id)
352
    $note_li.replaceWith($html)
353 354 355 356 357 358 359 360

  ###
  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
  ###
361 362 363
  showEditForm: (e) ->
    e.preventDefault()
    note = $(this).closest(".note")
364
    note.find(".note-body > .note-text").hide()
365
    note.find(".note-header").hide()
P
Phil Hughes 已提交
366 367 368 369 370 371
    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()
372 373 374

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

    # Setup markdown form
P
Phil Hughes 已提交
377 378 379
    if isNewForm
      GitLab.GfmAutoComplete.setup()
      new DropzoneInput(form)
380

381 382
    textarea = form.find("textarea")
    textarea.focus()
P
Phil Hughes 已提交
383 384 385

    if isNewForm
      autosize(textarea)
386

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

P
Phil Hughes 已提交
394 395
    if isNewForm
      disableButtonIfEmptyField textarea, form.find(".js-comment-button")
396 397 398 399 400 401 402 403 404

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

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

  ###
  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.
  ###
417
  removeNote: (e) =>
418
    noteId = $(e.currentTarget)
419 420
               .closest(".note")
               .attr("id")
421 422 423 424 425 426

    # 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)
427
      notes = note.closest(".notes")
428

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

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

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

438
      note.remove()
439

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

443 444 445 446 447 448 449 450 451
  ###
  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()
452
    note.find(".note-body > .note-text").show()
453 454
    note.find(".note-header").show()
    note.find(".current-note-edit-form").remove()
455 456 457 458 459 460 461 462

  ###
  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")
463
    replyLink = $(e.target).closest(".js-discussion-reply-button")
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
    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
482
    form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
483
    form.find("#line_type").val dataHolder.data("lineType")
484 485 486 487
    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")
P
Phil Hughes 已提交
488 489 490 491 492
    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'))
493 494 495 496 497 498 499 500 501 502 503 504
    @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()
D
Dmitriy Zaporozhets 已提交
505
    link = e.currentTarget
506 507 508
    form = $(".js-new-note-form")
    row = $(link).closest("tr")
    nextRow = row.next()
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
    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
532 533
    else
      # add a notes row and insert the form
534 535 536 537 538 539
      row.after rowCssToAdd
      addForm = true

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

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

  ###
  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")

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

555 556 557 558 559 560 561 562 563
    # 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()

564 565 566 567 568 569 570

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

571 572 573 574 575 576 577 578 579 580 581 582
  ###
  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

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

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

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

602
    if textarea.val().trim().length > 0
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
      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()
620
    else
621 622 623 624 625 626 627
      reopentext = reopenbtn.data('original-text')
      closetext = closebtn.data('original-text')

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

      if closebtn.text() isnt closetext
628
        closebtn.text(closetext)
629

630
      if reopenbtn.is('.btn-comment-and-reopen')
631 632
        reopenbtn.removeClass('btn-comment-and-reopen')

633
      if closebtn.is('.btn-comment-and-close')
634 635 636 637
        closebtn.removeClass('btn-comment-and-close')

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

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

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

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

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