notes.js.coffee 17.3 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 19
    @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge")

20 21 22 23
    @initRefresh()
    @setupMainTargetNoteForm()
    @cleanBinding()
    @addBinding()
24
    @initTaskList()
25 26 27 28 29 30

  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

31
    # change note in UI after update
32 33 34
    $(document).on "ajax:success", "form.edit_note", @updateNote

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

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

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

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

55 56 57 58 59 60
    # 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

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

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

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

84 85 86
    $('.note .js-task-list-container').taskList('disable')
    $(document).off 'tasklist:changed', '.note .js-task-list-container'

87
  initRefresh: ->
88 89
    clearInterval(Notes.interval)
    Notes.interval = setInterval =>
90 91 92 93
      @refresh()
    , 15000

  refresh: ->
94
    if not document.hidden and document.URL.indexOf(@noteable_url) is 0
95
      @getContent()
96 97 98 99

  getContent: ->
    $.ajax
      url: @notes_url
100
      data: "last_fetched_at=" + @last_fetched_at
101 102 103
      dataType: "json"
      success: (data) =>
        notes = data.notes
104
        @last_fetched_at = data.last_fetched_at
105
        $.each notes, (i, note) =>
106 107 108 109
          if note.discussion_with_diff_html?
            @renderDiscussionNote(note)
          else
            @renderNote(note)
110 111 112 113 114 115 116 117


  ###
  Render note in main comments area.

  Note: for rendering inline notes use renderDiscussionNote
  ###
  renderNote: (note) ->
118
    unless note.valid
119
      if note.award
120
        flash = new Flash('You have already used this award emoji!', 'alert')
121
        flash.pinTo('.header-content')
122 123
      return

124 125 126 127
    if note.award
      awards_handler.addAwardToEmojiBar(note.note)
      awards_handler.scrollToAwards()

128 129
    # render note if it not present in loaded list
    # or skip if rendered
130
    else if @isNewNote(note)
131
      @note_ids.push(note.id)
132

133 134 135
      $('ul.main-notes-list')
      .append(note.html)
      .syntaxHighlight()
136
      @initTaskList()
137
      @updateNotesCount(1)
138

V
Valery Sizov 已提交
139

140 141 142 143 144 145
  ###
  Check if note does not exists on page
  ###
  isNewNote: (note) ->
    $.inArray(note.id, @note_ids) == -1

146 147
  isParallelView: ->
    @view == 'parallel'
148 149 150 151 152 153 154

  ###
  Render note in discussion area.

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

157
    @note_ids.push(note.id)
158
    form = $("#new-discussion-note-form-#{note.discussion_id}")
159
    row = form.closest("tr")
160 161
    note_html = $(note.html)
    note_html.syntaxHighlight()
162 163

    # is this the first note of discussion?
164
    discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
165
    if discussionContainer.length is 0
166 167 168 169 170 171
      # 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()

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

175
      # Add note to 'Changes' page discussions
176
      discussionContainer.append note_html
177

178
      # Init discussion on 'Discussion' page if it is merge request page
179
      if $('body').attr('data-page').indexOf('projects:merge_request') is 0
180 181 182
        $('ul.main-notes-list')
        .append(note.discussion_with_diff_html)
        .syntaxHighlight()
183 184
    else
      # append new note to all matching discussions
185
      discussionContainer.append note_html
186

187
    @updateNotesCount(1)
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202

  ###
  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 已提交
203
    form.find(".js-md-write-button").click()
204 205
    form.find(".js-note-text").val("").trigger "input"

206 207
    form.find(".js-note-text").data("autosave").reset()

208 209 210 211 212
  reenableTargetFormSubmitButton: ->
    form = $(".js-main-target-form")

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

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
  ###
  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"
249
    form.find('.div-dropzone').remove()
250 251

    # setup preview buttons
V
Vinnie Okada 已提交
252 253
    form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
    previewButton = form.find(".js-md-preview-button")
254 255 256 257

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

    textarea.on "input", ->
258 259 260 261 262
      if $(this).val().trim() isnt ""
        previewButton.removeClass("turn-off").addClass "turn-on"
      else
        previewButton.removeClass("turn-on").addClass "turn-off"

R
Robert Speicher 已提交
263
    autosize(textarea)
264 265 266 267 268 269 270
    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()
    ]
271 272 273 274

    # 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()
275
    new DropzoneInput(form)
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    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)

294
    # cleanup after successfully creating a diff/discussion note
295
    @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}"))
296

297 298 299 300 301
  ###
  Called in response to the edit note form being submitted

  Updates the current note field.
  ###
302 303 304 305 306 307 308
  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
309
    $note_li = $('.note-row-' + note.id)
310
    $note_li.replaceWith($html)
311 312 313 314 315 316 317 318

  ###
  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
  ###
319 320 321
  showEditForm: (e) ->
    e.preventDefault()
    note = $(this).closest(".note")
322
    note.find(".note-body > .note-text").hide()
323
    note.find(".note-header").hide()
324 325
    base_form = note.find(".note-edit-form")
    form = base_form.clone().insertAfter(base_form)
326
    form.addClass('current-note-edit-form gfm-form')
327
    form.find('.div-dropzone').remove()
328 329 330

    # Show the attachment delete link
    note.find(".js-note-attachment-delete").show()
331 332

    # Setup markdown form
333
    GitLab.GfmAutoComplete.setup()
334 335
    new DropzoneInput(form)

336
    form.show()
337 338
    textarea = form.find("textarea")
    textarea.focus()
339
    autosize(textarea)
340

341
    # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
342
    # The textarea has the correct value, Chrome just won't show it unless we
343 344 345 346
    # modify it, so let's clear it and re-set it!
    value = textarea.val()
    textarea.val ""
    textarea.val value
347

348
    disableButtonIfEmptyField textarea, form.find(".js-comment-button")
349 350 351 352 353 354 355 356 357

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

  Hides edit form
  ###
  cancelEdit: (e) ->
    e.preventDefault()
    note = $(this).closest(".note")
358
    note.find(".note-body > .note-text").show()
359 360
    note.find(".note-header").show()
    note.find(".current-note-edit-form").remove()
361 362 363 364 365 366 367

  ###
  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.
  ###
368
  removeNote: (e) =>
369 370 371 372 373 374 375 376 377
    noteId = $(e.currentTarget)
             .closest(".note")
             .attr("id")

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

380 381
      # check if this is the last note for this line
      if notes.find(".note").length is 1
382

383 384
        # "Discussions" tab
        notes.closest(".timeline-entry").remove()
385

386
        # "Changes" tab / commit view
387
        notes.closest("tr").remove()
388

389
      note.remove()
390

391 392
    # Decrement the "Discussions" counter only once
    @updateNotesCount(-1)
393

394 395 396 397 398 399 400 401 402
  ###
  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()
403
    note.find(".note-body > .note-text").show()
404 405
    note.find(".note-header").show()
    note.find(".current-note-edit-form").remove()
406 407 408 409 410 411 412 413

  ###
  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")
414
    replyLink = $(e.target).closest(".js-discussion-reply-button")
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
    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
433
    form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
434
    form.find("#line_type").val dataHolder.data("lineType")
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
    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 已提交
451
    link = e.currentTarget
452 453 454
    form = $(".js-new-note-form")
    row = $(link).closest("tr")
    nextRow = row.next()
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
    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
478 479
    else
      # add a notes row and insert the form
480 481 482 483 484 485
      row.after rowCssToAdd
      addForm = true

    if addForm
      newForm = form.clone()
      newForm.appendTo row.next().find(targetContent)
486 487

      # show the form
488
      @setupDiscussionNoteForm $(link), newForm
489 490 491 492 493 494 495 496 497 498

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

499 500
    form.find(".js-note-text").data("autosave").reset()

501 502 503 504 505 506 507 508 509
    # 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()

510 511 512 513 514 515 516

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

517 518 519 520 521 522 523 524 525 526 527 528
  ###
  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

529 530 531 532 533 534
  ###
  Called when the tab visibility changes
  ###
  visibilityChange: =>
    @refresh()

535 536 537 538 539
  updateCloseButton: (e) =>
    textarea = $(e.target)
    form = textarea.parents('form')
    form.find('.js-note-target-close').text('Close')

540 541 542 543 544 545
  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')
546 547
      form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
      form.find('.js-note-target-close').addClass('btn-comment-and-close')
548 549 550
    else
      form.find('.js-note-target-reopen').text('Reopen')
      form.find('.js-note-target-close').text('Close')
551 552
      form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
      form.find('.js-note-target-close').removeClass('btn-comment-and-close')
553 554 555 556 557

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

558 559 560
  enableTaskList: ->
    $('.note .js-task-list-container').taskList('enable')

561 562
  updateTaskList: ->
    $('form', this).submit()
563

564
  updateNotesCount: (incrementStep) ->
565
    @notesCountBadge.text parseInt(@notesCountBadge.text()) + incrementStep