notes.js.coffee 18.2 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
    @base_polling_interval = 15000
20
    @max_polling_steps = 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
    # change note in UI after update
34 35 36
    $(document).on "ajax:success", "form.edit_note", @updateNote

    # Edit note link
37
    $(document).on "click", ".js-note-edit", @showEditForm
38 39
    $(document).on "click", ".note-edit-cancel", @cancelEdit

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

44 45 46 47 48 49 50
    # 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
51 52
    $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
    $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
53

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

57 58 59 60 61 62
    # 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

63 64 65
    # hide diff note form
    $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm

66 67 68
    # fetch notes when tab becomes visible
    $(document).on "visibilitychange", @visibilityChange

J
Jacob Schatz 已提交
69
    # when issue status changes, we need to refresh data
70 71
    $(document).on "issuable:change", @refresh

72 73 74 75
  cleanBinding: ->
    $(document).off "ajax:success", ".js-main-target-form"
    $(document).off "ajax:success", ".js-discussion-note-form"
    $(document).off "ajax:success", "form.edit_note"
76
    $(document).off "click", ".js-note-edit"
77 78 79 80
    $(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"
81
    $(document).off "ajax:success", ".js-main-target-form"
82 83
    $(document).off "click", ".js-discussion-reply-button"
    $(document).off "click", ".js-add-diff-note-button"
84
    $(document).off "visibilitychange"
85 86 87
    $(document).off "keyup", ".js-note-text"
    $(document).off "click", ".js-note-target-reopen"
    $(document).off "click", ".js-note-target-close"
88

89 90 91
    $('.note .js-task-list-container').taskList('disable')
    $(document).off 'tasklist:changed', '.note .js-task-list-container'

92
  initRefresh: ->
93 94
    clearInterval(Notes.interval)
    Notes.interval = setInterval =>
95
      @refresh()
96
    , @polling_interval
97 98

  refresh: ->
99 100
    return if @refreshing is true
    refreshing = true
101
    if not document.hidden and document.URL.indexOf(@noteable_url) is 0
102
      @getContent()
103 104 105 106

  getContent: ->
    $.ajax
      url: @notes_url
107
      data: "last_fetched_at=" + @last_fetched_at
108 109 110
      dataType: "json"
      success: (data) =>
        notes = data.notes
111
        @last_fetched_at = data.last_fetched_at
112
        @setPollingInterval(data.notes.length)
113
        $.each notes, (i, note) =>
114 115 116 117
          if note.discussion_with_diff_html?
            @renderDiscussionNote(note)
          else
            @renderNote(note)
118 119
      always: =>
        @refreshing = false
120

121 122 123 124 125 126 127 128 129
  ###
  Increase @polling_interval up to 120 seconds on every function call,
  if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
  will reset to @base_polling_interval.

  Note: this function is used to gradually increase the polling interval
  if there aren't new notes coming from the server
  ###
  setPollingInterval: (shouldReset = true) ->
130
    nthInterval = @base_polling_interval * Math.pow(2, @max_polling_steps - 1)
131 132
    if shouldReset
      @polling_interval = @base_polling_interval
133
    else if @polling_interval < nthInterval
134 135 136
      @polling_interval *= 2

    @initRefresh()
137 138 139 140 141 142 143

  ###
  Render note in main comments area.

  Note: for rendering inline notes use renderDiscussionNote
  ###
  renderNote: (note) ->
144
    unless note.valid
145
      if note.award
146
        flash = new Flash('You have already used this award emoji!', 'alert')
147
        flash.pinTo('.header-content')
148 149
      return

150 151 152 153
    if note.award
      awards_handler.addAwardToEmojiBar(note.note)
      awards_handler.scrollToAwards()

154 155
    # render note if it not present in loaded list
    # or skip if rendered
156
    else if @isNewNote(note)
157
      @note_ids.push(note.id)
158

159
      $('ul.main-notes-list')
160 161
        .append(note.html)
        .syntaxHighlight()
162
      @initTaskList()
163
      @updateNotesCount(1)
164

V
Valery Sizov 已提交
165

166 167 168 169 170 171
  ###
  Check if note does not exists on page
  ###
  isNewNote: (note) ->
    $.inArray(note.id, @note_ids) == -1

172 173
  isParallelView: ->
    @view == 'parallel'
174 175 176 177 178 179 180

  ###
  Render note in discussion area.

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

183
    @note_ids.push(note.id)
184
    form = $("#new-discussion-note-form-#{note.discussion_id}")
185
    row = form.closest("tr")
186 187
    note_html = $(note.html)
    note_html.syntaxHighlight()
188 189

    # is this the first note of discussion?
190
    discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
191
    if discussionContainer.length is 0
192 193 194 195 196 197
      # 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()

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

201
      # Add note to 'Changes' page discussions
202
      discussionContainer.append note_html
203

204
      # Init discussion on 'Discussion' page if it is merge request page
205
      if $('body').attr('data-page').indexOf('projects:merge_request') is 0
206
        $('ul.main-notes-list')
207 208
          .append(note.discussion_with_diff_html)
          .syntaxHighlight()
209 210
    else
      # append new note to all matching discussions
211
      discussionContainer.append note_html
212

213
    @updateNotesCount(1)
214 215 216 217 218 219 220 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.
  ###
  resetMainTargetForm: ->
    form = $(".js-main-target-form")

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

    # reset text and preview
V
Vinnie Okada 已提交
229
    form.find(".js-md-write-button").click()
230 231
    form.find(".js-note-text").val("").trigger "input"

232 233
    form.find(".js-note-text").data("autosave").reset()

234 235 236 237 238
  reenableTargetFormSubmitButton: ->
    form = $(".js-main-target-form")

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

239 240 241 242 243 244 245 246 247 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
  ###
  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"
275
    form.find('.div-dropzone').remove()
276 277

    # setup preview buttons
V
Vinnie Okada 已提交
278 279
    form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
    previewButton = form.find(".js-md-preview-button")
280 281 282 283

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

    textarea.on "input", ->
284 285 286 287 288
      if $(this).val().trim() isnt ""
        previewButton.removeClass("turn-off").addClass "turn-on"
      else
        previewButton.removeClass("turn-on").addClass "turn-off"

R
Robert Speicher 已提交
289
    autosize(textarea)
290 291 292 293 294 295 296
    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()
    ]
297 298 299 300

    # 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()
301
    new DropzoneInput(form)
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
    form.show()

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

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

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

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

320
    # cleanup after successfully creating a diff/discussion note
321
    @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}"))
322

323 324 325 326 327
  ###
  Called in response to the edit note form being submitted

  Updates the current note field.
  ###
328 329 330 331 332 333 334
  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
335
    $note_li = $('.note-row-' + note.id)
336
    $note_li.replaceWith($html)
337 338 339 340 341 342 343 344

  ###
  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
  ###
345 346 347
  showEditForm: (e) ->
    e.preventDefault()
    note = $(this).closest(".note")
348
    note.find(".note-body > .note-text").hide()
349
    note.find(".note-header").hide()
350 351
    base_form = note.find(".note-edit-form")
    form = base_form.clone().insertAfter(base_form)
352
    form.addClass('current-note-edit-form gfm-form')
353
    form.find('.div-dropzone').remove()
354 355 356

    # Show the attachment delete link
    note.find(".js-note-attachment-delete").show()
357 358

    # Setup markdown form
359
    GitLab.GfmAutoComplete.setup()
360 361
    new DropzoneInput(form)

362
    form.show()
363 364
    textarea = form.find("textarea")
    textarea.focus()
365
    autosize(textarea)
366

367
    # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
368
    # The textarea has the correct value, Chrome just won't show it unless we
369 370 371 372
    # modify it, so let's clear it and re-set it!
    value = textarea.val()
    textarea.val ""
    textarea.val value
373

374
    disableButtonIfEmptyField textarea, form.find(".js-comment-button")
375 376 377 378 379 380 381 382 383

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

  Hides edit form
  ###
  cancelEdit: (e) ->
    e.preventDefault()
    note = $(this).closest(".note")
384
    note.find(".note-body > .note-text").show()
385 386
    note.find(".note-header").show()
    note.find(".current-note-edit-form").remove()
387 388 389 390 391 392 393

  ###
  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.
  ###
394
  removeNote: (e) =>
395
    noteId = $(e.currentTarget)
396 397
               .closest(".note")
               .attr("id")
398 399 400 401 402 403

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

406 407
      # check if this is the last note for this line
      if notes.find(".note").length is 1
408

409 410
        # "Discussions" tab
        notes.closest(".timeline-entry").remove()
411

412
        # "Changes" tab / commit view
413
        notes.closest("tr").remove()
414

415
      note.remove()
416

417 418
    # Decrement the "Discussions" counter only once
    @updateNotesCount(-1)
419

420 421 422 423 424 425 426 427 428
  ###
  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()
429
    note.find(".note-body > .note-text").show()
430 431
    note.find(".note-header").show()
    note.find(".current-note-edit-form").remove()
432 433 434 435 436 437 438 439

  ###
  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")
440
    replyLink = $(e.target).closest(".js-discussion-reply-button")
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
    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
459
    form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
460
    form.find("#line_type").val dataHolder.data("lineType")
461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
    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")
    @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 已提交
477
    link = e.currentTarget
478 479 480
    form = $(".js-new-note-form")
    row = $(link).closest("tr")
    nextRow = row.next()
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
    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
504 505
    else
      # add a notes row and insert the form
506 507 508 509 510 511
      row.after rowCssToAdd
      addForm = true

    if addForm
      newForm = form.clone()
      newForm.appendTo row.next().find(targetContent)
512 513

      # show the form
514
      @setupDiscussionNoteForm $(link), newForm
515 516 517 518 519 520 521 522 523 524

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

525 526
    form.find(".js-note-text").data("autosave").reset()

527 528 529 530 531 532 533 534 535
    # 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()

536 537 538 539 540 541 542

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

543 544 545 546 547 548 549 550 551 552 553 554
  ###
  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

555 556 557 558 559 560
  ###
  Called when the tab visibility changes
  ###
  visibilityChange: =>
    @refresh()

561 562 563 564 565
  updateCloseButton: (e) =>
    textarea = $(e.target)
    form = textarea.parents('form')
    form.find('.js-note-target-close').text('Close')

566 567 568 569 570 571
  updateTargetButtons: (e) =>
    textarea = $(e.target)
    form = textarea.parents('form')
    if textarea.val().trim().length > 0
      form.find('.js-note-target-reopen').text('Comment & reopen')
      form.find('.js-note-target-close').text('Comment & close')
572 573
      form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
      form.find('.js-note-target-close').addClass('btn-comment-and-close')
574 575 576
    else
      form.find('.js-note-target-reopen').text('Reopen')
      form.find('.js-note-target-close').text('Close')
577 578
      form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
      form.find('.js-note-target-close').removeClass('btn-comment-and-close')
579 580 581 582 583

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

584 585 586
  enableTaskList: ->
    $('.note .js-task-list-container').taskList('enable')

587 588
  updateTaskList: ->
    $('form', this).submit()
589

590 591
  updateNotesCount: (updateCount) ->
    @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount)