BigW Consortium Gitlab

notes.js.coffee 17.4 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 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
    # when issue status changes, we need to refresh data
68 69
    $(document).on "issuable:change", @refresh

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

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

90
  initRefresh: ->
91 92
    clearInterval(Notes.interval)
    Notes.interval = setInterval =>
93 94 95 96
      @refresh()
    , 15000

  refresh: ->
97
    if not document.hidden and document.URL.indexOf(@noteable_url) is 0
98
      @getContent()
99 100 101 102

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


  ###
  Render note in main comments area.

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

127 128 129 130
    if note.award
      awards_handler.addAwardToEmojiBar(note.note)
      awards_handler.scrollToAwards()

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

136
      $('ul.main-notes-list')
137 138
        .append(note.html)
        .syntaxHighlight()
139
      @initTaskList()
140
      @updateNotesCount(1)
141

Valery Sizov committed
142

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

149 150
  isParallelView: ->
    @view == 'parallel'
151 152 153 154 155 156 157

  ###
  Render note in discussion area.

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

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

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

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

178
      # Add note to 'Changes' page discussions
179
      discussionContainer.append note_html
180

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

190
    @updateNotesCount(1)
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205

  ###
  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
206
    form.find(".js-md-write-button").click()
207 208
    form.find(".js-note-text").val("").trigger "input"

209 210
    form.find(".js-note-text").data("autosave").reset()

211 212 213 214 215
  reenableTargetFormSubmitButton: ->
    form = $(".js-main-target-form")

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

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 249 250 251
  ###
  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"
252
    form.find('.div-dropzone').remove()
253 254

    # setup preview buttons
255 256
    form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
    previewButton = form.find(".js-md-preview-button")
257 258 259 260

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

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

Robert Speicher committed
266
    autosize(textarea)
267 268 269 270 271 272 273
    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()
    ]
274 275 276 277

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

297
    # cleanup after successfully creating a diff/discussion note
298
    @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}"))
299

300 301 302 303 304
  ###
  Called in response to the edit note form being submitted

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

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

    # Show the attachment delete link
    note.find(".js-note-attachment-delete").show()
334 335

    # Setup markdown form
336
    GitLab.GfmAutoComplete.setup()
337 338
    new DropzoneInput(form)

339
    form.show()
340 341
    textarea = form.find("textarea")
    textarea.focus()
342
    autosize(textarea)
343

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

351
    disableButtonIfEmptyField textarea, form.find(".js-comment-button")
352 353 354 355 356 357 358 359 360

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

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

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

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

383 384
      # check if this is the last note for this line
      if notes.find(".note").length is 1
385

386 387
        # "Discussions" tab
        notes.closest(".timeline-entry").remove()
388

389
        # "Changes" tab / commit view
390
        notes.closest("tr").remove()
391

392
      note.remove()
393

394 395
    # Decrement the "Discussions" counter only once
    @updateNotesCount(-1)
396

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

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

    if addForm
      newForm = form.clone()
      newForm.appendTo row.next().find(targetContent)
489 490

      # show the form
491
      @setupDiscussionNoteForm $(link), newForm
492 493 494 495 496 497 498 499 500 501

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

502 503
    form.find(".js-note-text").data("autosave").reset()

504 505 506 507 508 509 510 511 512
    # 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()

513 514 515 516 517 518 519

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

520 521 522 523 524 525 526 527 528 529 530 531
  ###
  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

532 533 534 535 536 537
  ###
  Called when the tab visibility changes
  ###
  visibilityChange: =>
    @refresh()

538 539 540 541 542
  updateCloseButton: (e) =>
    textarea = $(e.target)
    form = textarea.parents('form')
    form.find('.js-note-target-close').text('Close')

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

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

561 562 563
  enableTaskList: ->
    $('.note .js-task-list-container').taskList('enable')

564 565
  updateTaskList: ->
    $('form', this).submit()
566

567 568
  updateNotesCount: (updateCount) ->
    @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount)