diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js deleted file mode 100644 index e39bfc0f79..0000000000 --- a/app/assets/javascripts/notes.js +++ /dev/null @@ -1,587 +0,0 @@ -var NoteList = { - id: null, - notes_path: null, - target_params: null, - target_id: 0, - target_type: null, - - init: function(tid, tt, path) { - NoteList.notes_path = path + ".json"; - NoteList.target_id = tid; - NoteList.target_type = tt; - NoteList.target_params = "target_type=" + NoteList.target_type + "&target_id=" + NoteList.target_id; - - NoteList.setupMainTargetNoteForm(); - - // get initial set of notes - NoteList.getContent(); - - // Unbind events to prevent firing twice - $(document).off("click", ".js-add-diff-note-button"); - $(document).off("click", ".js-discussion-reply-button"); - $(document).off("click", ".js-note-preview-button"); - $(document).off("click", ".js-note-attachment-input"); - $(document).off("click", ".js-close-discussion-note-form"); - $(document).off("click", ".js-note-delete"); - $(document).off("click", ".js-note-edit"); - $(document).off("click", ".js-note-edit-cancel"); - $(document).off("click", ".js-note-attachment-delete"); - $(document).off("click", ".js-choose-note-attachment-button"); - $(document).off("click", ".js-show-outdated-discussion"); - - $(document).off("ajax:complete", ".js-main-target-form"); - - - // add a new diff note - $(document).on("click", - ".js-add-diff-note-button", - NoteList.addDiffNote); - - // reply to diff/discussion notes - $(document).on("click", - ".js-discussion-reply-button", - NoteList.replyToDiscussionNote); - - // setup note preview - $(document).on("click", - ".js-note-preview-button", - NoteList.previewNote); - - // update the file name when an attachment is selected - $(document).on("change", - ".js-note-attachment-input", - NoteList.updateFormAttachment); - - // hide diff note form - $(document).on("click", - ".js-close-discussion-note-form", - NoteList.removeDiscussionNoteForm); - - // remove a note (in general) - $(document).on("click", - ".js-note-delete", - NoteList.removeNote); - - // show the edit note form - $(document).on("click", - ".js-note-edit", - NoteList.showEditNoteForm); - - // cancel note editing - $(document).on("click", - ".note-edit-cancel", - NoteList.cancelNoteEdit); - - // delete note attachment - $(document).on("click", - ".js-note-attachment-delete", - NoteList.deleteNoteAttachment); - - // update the note after editing - $(document).on("ajax:complete", - "form.edit_note", - NoteList.updateNote); - - // reset main target form after submit - $(document).on("ajax:complete", - ".js-main-target-form", - NoteList.resetMainTargetForm); - - - $(document).on("click", - ".js-choose-note-attachment-button", - NoteList.chooseNoteAttachment); - - $(document).on("click", - ".js-show-outdated-discussion", - function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() }); - }, - - - /** - * When clicking on buttons - */ - - /** - * 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: function(e) { - e.preventDefault(); - - // find the form - var form = $(".js-new-note-form"); - var row = $(this).closest("tr"); - var nextRow = row.next(); - - // does it already have notes? - if (nextRow.is(".notes_holder")) { - $.proxy(NoteList.replyToDiscussionNote, - nextRow.find(".js-discussion-reply-button") - ).call(); - } else { - // add a notes row and insert the form - row.after(''); - form.clone().appendTo(row.next().find(".notes_content")); - - // show the form - NoteList.setupDiscussionNoteForm($(this), row.next().find("form")); - } - }, - - /** - * Called when clicking the "Choose File" button. - * - * Opens the file selection dialog. - */ - chooseNoteAttachment: function() { - var form = $(this).closest("form"); - - form.find(".js-note-attachment-input").click(); - }, - - /** - * Shows the note preview. - * - * Lets the server render GFM into Html and displays it. - * - * Note: uses the Toggler behavior to toggle preview/edit views/buttons - */ - previewNote: function(e) { - e.preventDefault(); - - var form = $(this).closest("form"); - var preview = form.find('.js-note-preview'); - var noteText = form.find('.js-note-text').val(); - - if(noteText.trim().length === 0) { - preview.text('Nothing to preview.'); - } else { - preview.text('Loading...'); - $.post($(this).data('url'), {note: noteText}) - .success(function(previewData) { - preview.html(previewData); - }); - } - }, - - /** - * 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: function() { - var form = $(this).closest("form"); - var row = form.closest("tr"); - - // 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(); - } - }, - - /** - * 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. - */ - removeNote: function() { - var note = $(this).closest(".note"); - var notes = note.closest(".notes"); - - // check if this is the last note for this line - if (notes.find(".note").length === 1) { - // for discussions - notes.closest(".discussion").remove(); - - // for diff lines - notes.closest("tr").remove(); - } - - note.remove(); - NoteList.updateVotes(); - }, - - /** - * 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 - */ - showEditNoteForm: function(e) { - e.preventDefault(); - var note = $(this).closest(".note"); - note.find(".note-text").hide(); - - // Show the attachment delete link - note.find(".js-note-attachment-delete").show(); - - GitLab.GfmAutoComplete.setup(); - - var form = note.find(".note-edit-form"); - form.show(); - - var textarea = form.find("textarea"); - if (form.find(".note-original-content").length === 0) { - var p = $("

").text(textarea.val()); - var hidden_div = $('
').append(p); - form.append(hidden_div); - hidden_div.hide(); - } - textarea.focus(); - }, - - /** - * Called in response to clicking the cancel button when editing a note - * - * Resets and hides the note editing form - */ - cancelNoteEdit: function(e) { - e.preventDefault(); - var note = $(this).closest(".note"); - NoteList.resetNoteEditing(note); - }, - - - /** - * 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 - */ - deleteNoteAttachment: function() { - var note = $(this).closest(".note"); - note.find(".note-attachment").remove(); - NoteList.resetNoteEditing(note); - NoteList.rewriteTimestamp(note.find(".note-last-update")); - }, - - - /** - * Called when clicking on the "reply" button for a diff line. - * - * Shows the note form below the notes. - */ - replyToDiscussionNote: function() { - // find the form - var form = $(".js-new-note-form"); - - // hide reply button - $(this).hide(); - // insert the form after the button - form.clone().insertAfter($(this)); - - // show the form - NoteList.setupDiscussionNoteForm($(this), $(this).next("form")); - }, - - - /** - * Helper for inserting and setting up note forms. - */ - - - /** - * Called in response to creating a note failing validation. - * - * Adds the rendered errors to the respective form. - * If "discussionId" is null or undefined, the main target form is assumed. - */ - errorsOnForm: function(errorsHtml, discussionId) { - // find the form - if (discussionId) { - var form = $("form[rel='"+discussionId+"']"); - } else { - var form = $(".js-main-target-form"); - } - - form.find(".js-errors").remove(); - form.prepend(errorsHtml); - - form.find(".js-note-text").focus(); - }, - - - /** - * Shows the diff/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: function(dataHolder, form) { - // setup note target - form.attr("rel", dataHolder.data("discussionId")); - 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")); - - NoteList.setupNoteForm(form); - - form.find(".js-note-text").focus(); - }, - - /** - * Shows the main form and does some setup on it. - * - * Sets some hidden fields in the form. - */ - setupMainTargetNoteForm: function() { - // find the form - var 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 - NoteList.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: function(form) { - disableButtonIfEmptyField(form.find(".js-note-text"), form.find(".js-comment-button")); - - form.removeClass("js-new-note-form"); - - // setup preview buttons - form.find(".js-note-edit-button, .js-note-preview-button") - .tooltip({ placement: 'left' }); - - previewButton = form.find(".js-note-preview-button"); - form.find(".js-note-text").on("input", function() { - if ($(this).val().trim() !== "") { - previewButton.removeClass("turn-off").addClass("turn-on"); - } else { - previewButton.removeClass("turn-on").addClass("turn-off"); - } - }); - - // remove notify commit author checkbox for non-commit notes - if (form.find("#note_noteable_type").val() !== "Commit") { - form.find(".js-notify-commit-author").remove(); - } - - GitLab.GfmAutoComplete.setup(); - - form.show(); - }, - - - /** - * Handle loading the initial set of notes. - * And set up loading more notes when scrolling to the bottom of the page. - */ - - - /** - * Gets an initial set of notes. - */ - getContent: function() { - $.ajax({ - url: NoteList.notes_path, - data: NoteList.target_params, - complete: function(){ $('.js-notes-busy').removeClass("loading")}, - beforeSend: function() { $('.js-notes-busy').addClass("loading") }, - success: function(data) { - NoteList.setContent(data.html); - }, - dataType: "json" - }); - }, - - /** - * Called in response to getContent(). - * Replaces the content of #notes-list with the given html. - */ - setContent: function(html) { - $("#notes-list").html(html); - }, - - - /** - * Adds a single common note to #notes-list. - */ - appendNewNote: function(id, html) { - $("#notes-list").append(html); - NoteList.updateVotes(); - }, - - /** - * Adds a single discussion note to #notes-list. - * - * Also removes the corresponding form. - */ - appendNewDiscussionNote: function(discussionId, diffRowHtml, noteHtml) { - var form = $("form[rel='"+discussionId+"']"); - var row = form.closest("tr"); - - // is this the first note of discussion? - if (row.is(".js-temp-notes-holder")) { - // insert the note and the reply button after the temp row - row.after(diffRowHtml); - // remove the note (will be added again below) - row.next().find(".note").remove(); - } - - // append new note to all matching discussions - $(".notes[rel='"+discussionId+"']").append(noteHtml); - - // cleanup after successfully creating a diff/discussion note - $.proxy(NoteList.removeDiscussionNoteForm, form).call(); - }, - - /** - * Called in response the main target form has been successfully submitted. - * - * Removes any errors. - * Resets text and preview. - * Resets buttons. - */ - resetMainTargetForm: function(){ - var form = $(this); - - // remove validation errors - form.find(".js-errors").remove(); - - // reset text and preview - var previewContainer = form.find(".js-toggler-container.note_text_and_preview"); - if (previewContainer.is(".on")) { - previewContainer.removeClass("on"); - } - form.find(".js-note-text").val("").trigger("input"); - }, - - /** - * Called after an attachment file has been selected. - * - * Updates the file name for the selected attachment. - */ - updateFormAttachment: function() { - var form = $(this).closest("form"); - - // get only the basename - var filename = $(this).val().replace(/^.*[\\\/]/, ''); - - form.find(".js-attachment-filename").text(filename); - }, - - /** - * Recalculates the votes and updates them (if they are displayed at all). - * - * Assumes all relevant notes are displayed (i.e. there are no more notes to - * load via getMore()). - * Might produce inaccurate results when not all notes have been loaded and a - * recalculation is triggered (e.g. when deleting a note). - */ - updateVotes: function() { - var votes = $("#votes .votes"); - var notes = $("#notes-list .note .vote"); - - // only update if there is a vote display - if (votes.size()) { - var upvotes = notes.filter(".upvote").size(); - var downvotes = notes.filter(".downvote").size(); - var votesCount = upvotes + downvotes; - var upvotesPercent = votesCount ? (100.0 / votesCount * upvotes) : 0; - var downvotesPercent = votesCount ? (100.0 - upvotesPercent) : 0; - - // change vote bar lengths - votes.find(".bar-success").css("width", upvotesPercent+"%"); - votes.find(".bar-danger").css("width", downvotesPercent+"%"); - // replace vote numbers - votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes)); - votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes)); - } - }, - - /** - * Called in response to the edit note form being submitted - * - * Updates the current note field. - * Hides the edit note form - */ - updateNote: function(e, xhr, settings) { - response = JSON.parse(xhr.responseText); - if (response.success) { - var note_li = $("#note_" + response.id); - var note_text = note_li.find(".note-text"); - note_text.html(response.note).show(); - - var note_form = note_li.find(".note-edit-form"); - var original_content = note_form.find(".note-original-content"); - original_content.remove(); - note_form.hide(); - note_form.find(".btn-save").enableButton(); - - // Update the "Edited at xxx label" on the note to show it's just been updated - NoteList.rewriteTimestamp(note_li.find(".note-last-update")); - } - }, - - /** - * Called in response to the 'cancel note' link clicked, or after deleting a note attachment - * - * Hides the edit note form and shows the note - * Resets the edit note form textarea with the original content of the note - */ - resetNoteEditing: function(note) { - note.find(".note-text").show(); - - // Hide the attachment delete link - note.find(".js-note-attachment-delete").hide(); - - // Put the original content of the note back into the edit form textarea - var form = note.find(".note-edit-form"); - var original_content = form.find(".note-original-content"); - form.find("textarea").val(original_content.text()); - original_content.remove(); - - note.find(".note-edit-form").hide(); - }, - - /** - * Utility function to generate new timestamp text for a note - * - */ - rewriteTimestamp: function(element) { - // Strip all newlines from the existing timestamp - var ts = element.text().replace(/\n/g, ' ').trim(); - - // If the timestamp already has '(Edited xxx ago)' text, remove it - ts = ts.replace(new RegExp("\\(Edited [A-Za-z0-9 ]+\\)$", "gi"), ""); - - // Append "(Edited just now)" - ts = (ts + " (Edited just now)"); - - element.html(ts); - } -}; diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee new file mode 100644 index 0000000000..b3e71965a0 --- /dev/null +++ b/app/assets/javascripts/notes.js.coffee @@ -0,0 +1,418 @@ +class Notes + constructor: (notes_url, note_ids) -> + @notes_url = notes_url + @notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root? + @note_ids = note_ids + @initRefresh() + @setupMainTargetNoteForm() + @cleanBinding() + @addBinding() + + 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 + + # change note in UI after update + $(document).on "ajax:success", "form.edit_note", @updateNote + + # Edit note link + $(document).on "click", ".js-note-edit", @showEditForm + $(document).on "click", ".note-edit-cancel", @cancelEdit + + # remove a note (in general) + $(document).on "click", ".js-note-delete", @removeNote + + # delete note attachment + $(document).on "click", ".js-note-attachment-delete", @removeAttachment + + # Preview button + $(document).on "click", ".js-note-preview-button", @previewNote + + # reset main target form after submit + $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm + + # attachment button + $(document).on "click", ".js-choose-note-attachment-button", @chooseNoteAttachment + + # 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 + + # hide diff note form + $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm + + cleanBinding: -> + $(document).off "ajax:success", ".js-main-target-form" + $(document).off "ajax:success", ".js-discussion-note-form" + $(document).off "ajax:success", "form.edit_note" + $(document).off "click", ".js-note-edit" + $(document).off "click", ".note-edit-cancel" + $(document).off "click", ".js-note-delete" + $(document).off "click", ".js-note-attachment-delete" + $(document).off "click", ".js-note-preview-button" + $(document).off "ajax:complete", ".js-main-target-form" + $(document).off "click", ".js-choose-note-attachment-button" + $(document).off "click", ".js-discussion-reply-button" + $(document).off "click", ".js-add-diff-note-button" + + + initRefresh: -> + setInterval => + @refresh() + , 15000 + + refresh: -> + @getContent() + + getContent: -> + $.ajax + url: @notes_url + dataType: "json" + success: (data) => + notes = data.notes + $.each notes, (i, note) => + # render note if it not present in loaded list + # or skip if rendered + if $.inArray(note.id, @note_ids) == -1 + @note_ids.push(note.id) + @renderNote(note) + + + ### + Render note in main comments area. + + Note: for rendering inline notes use renderDiscussionNote + ### + renderNote: (note) -> + $('ul.main-notes-list').append(note.html) + + ### + Render note in discussion area. + + Note: for rendering inline notes use renderDiscussionNote + ### + renderDiscussionNote: (note) -> + form = $("form[rel='" + note.discussion_id + "']") + row = form.closest("tr") + + # is this the first note of discussion? + if row.is(".js-temp-notes-holder") + # 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() + + # append new note to all matching discussions + $(".notes[rel='" + note.discussion_id + "']").append note.html + + # cleanup after successfully creating a diff/discussion note + @removeDiscussionNoteForm(form) + + ### + Shows the note preview. + + Lets the server render GFM into Html and displays it. + + Note: uses the Toggler behavior to toggle preview/edit views/buttons + ### + previewNote: (e) -> + e.preventDefault() + form = $(this).closest("form") + preview = form.find(".js-note-preview") + noteText = form.find(".js-note-text").val() + if noteText.trim().length is 0 + preview.text "Nothing to preview." + else + preview.text "Loading..." + $.post($(this).data("url"), + note: noteText + ).success (previewData) -> + preview.html previewData + + ### + 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 + previewContainer = form.find(".js-toggler-container.note_text_and_preview") + previewContainer.removeClass "on" if previewContainer.is(".on") + form.find(".js-note-text").val("").trigger "input" + + ### + Called when clicking the "Choose File" button. + + Opens the file selection dialog. + ### + chooseNoteAttachment: -> + form = $(this).closest("form") + form.find(".js-note-attachment-input").click() + + ### + 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" + + # setup preview buttons + form.find(".js-note-edit-button, .js-note-preview-button").tooltip placement: "left" + previewButton = form.find(".js-note-preview-button") + form.find(".js-note-text").on "input", -> + if $(this).val().trim() isnt "" + previewButton.removeClass("turn-off").addClass "turn-on" + else + previewButton.removeClass("turn-on").addClass "turn-off" + + + # 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() + form.show() + + + ### + Called in response to the new note form being submitted + + Adds new note to list. + ### + addNote: (xhr, note, status) => + @note_ids.push(note.id) + @renderNote(note) + + ### + Called in response to the new note form being submitted + + Adds new note to list. + ### + addDiscussionNote: (xhr, note, status) => + @note_ids.push(note.id) + @renderDiscussionNote(note) + + ### + Called in response to the edit note form being submitted + + Updates the current note field. + ### + updateNote: (xhr, note, status) => + note_li = $("#note_" + note.id) + note_li.replaceWith(note.html) + + ### + 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 + ### + showEditForm: (e) -> + e.preventDefault() + note = $(this).closest(".note") + note.find(".note-text").hide() + + # Show the attachment delete link + note.find(".js-note-attachment-delete").show() + GitLab.GfmAutoComplete.setup() + form = note.find(".note-edit-form") + form.show() + form.find("textarea").focus() + + ### + Called in response to clicking the edit note link + + Hides edit form + ### + cancelEdit: (e) -> + e.preventDefault() + note = $(this).closest(".note") + note.find(".note-text").show() + note.find(".js-note-attachment-delete").hide() + note.find(".note-edit-form").hide() + + ### + 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. + ### + removeNote: -> + note = $(this).closest(".note") + notes = note.closest(".notes") + + # check if this is the last note for this line + if notes.find(".note").length is 1 + + # for discussions + notes.closest(".discussion").remove() + + # for diff lines + notes.closest("tr").remove() + + note.remove() + + ### + 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() + note.find(".note-text").show() + note.find(".js-note-attachment-delete").hide() + note.find(".note-edit-form").hide() + + ### + 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") + replyLink = $(e.target) + 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 + form.attr "rel", dataHolder.data("discussionId") + 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" + + ### + 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" + form.removeClass "js-new-note-form" + GitLab.GfmAutoComplete.setup() + + # setup preview buttons + previewButton = form.find(".js-note-preview-button") + form.find(".js-note-text").on "input", -> + if $(this).val().trim() isnt "" + previewButton.removeClass("turn-off").addClass "turn-on" + else + previewButton.removeClass("turn-on").addClass "turn-off" + + form.show() + + ### + 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() + link = e.target + form = $(".js-new-note-form") + row = $(link).closest("tr") + nextRow = row.next() + + # does it already have notes? + if nextRow.is(".notes_holder") + replyButton = nextRow.find(".js-discussion-reply-button") + if replyButton.length > 0 + $.proxy(@replyToDiscussionNote, replyButton).call() + else + # add a notes row and insert the form + row.after "" + form.clone().appendTo row.next().find(".notes_content") + + # show the form + @setupDiscussionNoteForm $(link), row.next().find("form") + + ### + 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") + + # 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() + + + cancelDiscussionForm: (e) => + e.preventDefault() + form = $(".js-new-note-form") + form = $(e.target).closest(".js-discussion-note-form") + @removeDiscussionNoteForm(form) + +@Notes = Notes diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index ac0caa3004..724b8d414a 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -257,12 +257,12 @@ ul.notes { .file, .discussion { .new_note { - margin: 8px 5px 8px 0; + margin: 0; + border: none; } } .new_note { display: none; - .buttons { float: left; margin-top: 8px; diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 242aa41182..65b8a7283a 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -24,8 +24,8 @@ class Projects::CommitController < Projects::ApplicationController @line_notes = result[:line_notes] @branches = result[:branches] @notes_count = result[:notes_count] - @target_type = :commit - @target_id = @commit.id + @notes = project.notes.for_commit_id(@commit.id).not_inline.fresh + @noteable = @commit @comments_allowed = @reply_allowed = true @comments_target = { noteable_type: 'Commit', diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e7b4c837ae..c58ac71277 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -49,8 +49,8 @@ class Projects::IssuesController < Projects::ApplicationController def show @note = @project.notes.new(noteable: @issue) - @target_type = :issue - @target_id = @issue.id + @notes = @issue.notes.inc_author.fresh + @noteable = @issue respond_with(@issue) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d644026b2b..bba08bdd78 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -198,6 +198,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) + @notes = @merge_request.mr_and_commit_notes.inc_author.fresh + @discussions = Note.discussions_from_notes(@notes) + @noteable = @merge_request # Get commits from repository # or from cache if already merged @@ -205,9 +208,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController @allowed_to_merge = allowed_to_merge? @show_merge_controls = @merge_request.opened? && @commits.any? && @allowed_to_merge - - @target_type = :merge_request - @target_id = @merge_request.id end def allowed_to_merge? diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 5ff5c5b7d9..261841f1ea 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -2,71 +2,54 @@ class Projects::NotesController < Projects::ApplicationController # Authorize before_filter :authorize_read_note! before_filter :authorize_write_note!, only: [:create] - - respond_to :js + before_filter :authorize_admin_note!, only: [:update, :destroy] def index @notes = Notes::LoadContext.new(project, current_user, params).execute - @target_type = params[:target_type].camelize - @target_id = params[:target_id] - if params[:target_type] == "merge_request" - @discussions = discussions_from_notes + notes_json = { notes: [] } + + @notes.each do |note| + notes_json[:notes] << { + id: note.id, + html: note_to_html(note) + } end - respond_to do |format| - format.html { redirect_to :back } - format.json do - render json: { - html: view_to_html_string("projects/notes/_notes") - } - end - end + render json: notes_json end def create @note = Notes::CreateContext.new(project, current_user, params).execute - @target_type = params[:target_type].camelize - @target_id = params[:target_id] respond_to do |format| - format.html {redirect_to :back} - format.js + format.json { render_note_json(@note) } + format.html { redirect_to :back } + end + end + + def update + note.update_attributes(params[:note]) + note.reset_events_cache + + respond_to do |format| + format.json { render_note_json(note) } + format.html { redirect_to :back } end end def destroy - @note = @project.notes.find(params[:id]) - return access_denied! unless can?(current_user, :admin_note, @note) - @note.destroy - @note.reset_events_cache + note.destroy + note.reset_events_cache respond_to do |format| format.js { render nothing: true } end end - def update - @note = @project.notes.find(params[:id]) - return access_denied! unless can?(current_user, :admin_note, @note) - - @note.update_attributes(params[:note]) - @note.reset_events_cache - - respond_to do |format| - format.js do - render js: { success: @note.valid?, id: @note.id, note: view_context.markdown(@note.note) }.to_json - end - format.html do - redirect_to :back - end - end - end - def delete_attachment - @note = @project.notes.find(params[:id]) - @note.remove_attachment! - @note.update_attribute(:attachment, nil) + note.remove_attachment! + note.update_attribute(:attachment, nil) respond_to do |format| format.js { render nothing: true } @@ -77,35 +60,40 @@ class Projects::NotesController < Projects::ApplicationController render text: view_context.markdown(params[:note]) end - protected + private - def discussion_notes_for(note) - @notes.select do |other_note| - note.discussion_id == other_note.discussion_id - end + def note + @note ||= @project.notes.find(params[:id]) end - def discussions_from_notes - discussion_ids = [] - discussions = [] - - @notes.each do |note| - next if discussion_ids.include?(note.discussion_id) - - # don't group notes for the main target - if note_for_main_target?(note) - discussions << [note] - else - discussions << discussion_notes_for(note) - discussion_ids << note.discussion_id - end - end - - discussions + def note_to_html(note) + render_to_string( + "projects/notes/_note", + layout: false, + formats: [:html], + locals: { note: note } + ) end - # Helps to distinguish e.g. commit notes in mr notes list - def note_for_main_target?(note) - (@target_type.camelize == note.noteable_type && !note.for_diff_line?) + def note_to_discussion_html(note) + render_to_string( + "projects/notes/_diff_notes_with_reply", + layout: false, + formats: [:html], + locals: { notes: [note] } + ) + end + + def render_note_json(note) + render json: { + id: note.id, + discussion_id: note.discussion_id, + html: note_to_html(note), + discussion_html: note_to_discussion_html(note) + } + end + + def authorize_admin_note! + return access_denied! unless can?(current_user, :admin_note, note) end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index dd0c1a5708..f93f2d5f9b 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -48,8 +48,8 @@ class Projects::SnippetsController < Projects::ApplicationController def show @note = @project.notes.new(noteable: @snippet) - @target_type = :snippet - @target_id = @snippet.id + @notes = @snippet.notes.fresh + @noteable = @snippet end def destroy diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 822d27cf17..ec5f8a6ded 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,7 +1,7 @@ module NotesHelper # Helps to distinguish e.g. commit notes in mr notes list def note_for_main_target?(note) - (@target_type.camelize == note.noteable_type && !note.for_diff_line?) + (@noteable.class.name == note.noteable_type && !note.for_diff_line?) end def note_target_fields @@ -21,14 +21,6 @@ module NotesHelper end end - def loading_more_notes? - params[:loading_more].present? - end - - def loading_new_notes? - params[:loading_new].present? - end - def note_timestamp(note) # Shows the created at time and the updated at time if different ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')} ago" @@ -41,4 +33,13 @@ module NotesHelper end ts.html_safe end + + def noteable_json(noteable) + { + id: noteable.id, + class: noteable.class.name, + resources: noteable.class.table_name, + project_id: noteable.project.id, + }.to_json + end end diff --git a/app/models/note.rb b/app/models/note.rb index b23f7df774..48b36bcafd 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -56,29 +56,52 @@ class Note < ActiveRecord::Base serialize :st_diff before_create :set_diff, if: ->(n) { n.line_code.present? } - def self.create_status_change_note(noteable, project, author, status, source) - body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_" + class << self + def create_status_change_note(noteable, project, author, status, source) + body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_" - create({ - noteable: noteable, - project: project, - author: author, - note: body, - system: true - }, without_protection: true) - end + create({ + noteable: noteable, + project: project, + author: author, + note: body, + system: true + }, without_protection: true) + end - # +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note. - # Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+. - def self.create_cross_reference_note(noteable, mentioner, author, project) - create({ - noteable: noteable, - commit_id: (noteable.sha if noteable.respond_to? :sha), - project: project, - author: author, - note: "_mentioned in #{mentioner.gfm_reference}_", - system: true - }, without_protection: true) + # +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note. + # Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+. + def create_cross_reference_note(noteable, mentioner, author, project) + create({ + noteable: noteable, + commit_id: (noteable.sha if noteable.respond_to? :sha), + project: project, + author: author, + note: "_mentioned in #{mentioner.gfm_reference}_", + system: true + }, without_protection: true) + end + + def discussions_from_notes(notes) + discussion_ids = [] + discussions = [] + + notes.each do |note| + next if discussion_ids.include?(note.discussion_id) + + # don't group notes for the main target + if !note.for_diff_line? && note.noteable_type == "MergeRequest" + discussions << [note] + else + discussions << notes.select do |other_note| + note.discussion_id == other_note.discussion_id + end + discussion_ids << note.discussion_id + end + end + + discussions + end end # Determine whether or not a cross-reference note already exists. @@ -89,7 +112,7 @@ class Note < ActiveRecord::Base def commit_author @commit_author ||= project.users.find_by_email(noteable.author_email) || - project.users.find_by_name(noteable.author_name) + project.users.find_by_name(noteable.author_name) rescue nil end diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 0572e4f6fb..33667d3f46 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,5 +1,4 @@ -= form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note js-new-note-form common-note-form" }, authenticity_token: true do |f| - += form_for [@project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form" }, authenticity_token: true do |f| = note_target_fields = f.hidden_field :commit_id = f.hidden_field :line_code diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index e2de98b3ba..1c3bd614ba 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -34,7 +34,7 @@ = markdown(note.note) .note-edit-form - = form_for note, url: project_note_path(@project, note), method: :put, remote: true do |f| + = form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f| = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on' .form-actions diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index ac8901fe70..ca60dd239b 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -4,7 +4,7 @@ - if note_for_main_target?(note) = render discussion_notes - else - = render 'discussion', discussion_notes: discussion_notes + = render 'projects/notes/discussion', discussion_notes: discussion_notes - else - @notes.each do |note| - next unless note.author diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index ac28c7432e..3bd592e398 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,4 +1,5 @@ -%ul#notes-list.notes +%ul#notes-list.notes.main-notes-list + = render "projects/notes/notes" .js-notes-busy .js-main-target-form @@ -6,4 +7,4 @@ = render "projects/notes/form" :javascript - NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}"); + new Notes("#{project_notes_path(target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}) diff --git a/app/views/projects/notes/create.js.haml b/app/views/projects/notes/create.js.haml deleted file mode 100644 index c113b3482e..0000000000 --- a/app/views/projects/notes/create.js.haml +++ /dev/null @@ -1,18 +0,0 @@ -- if @note.valid? - var noteHtml = "#{escape_javascript(render @note)}"; - - - if note_for_main_target?(@note) - NoteList.appendNewNote(#{@note.id}, noteHtml); - - else - :plain - var firstDiscussionNoteHtml = "#{escape_javascript(render "projects/notes/diff_notes_with_reply", notes: [@note])}"; - NoteList.appendNewDiscussionNote("#{@note.discussion_id}", - firstDiscussionNoteHtml, - noteHtml); - -- else - var errorsHtml = "#{escape_javascript(render 'projects/notes/form_errors', note: @note)}"; - - if note_for_main_target?(@note) - NoteList.errorsOnForm(errorsHtml); - - else - NoteList.errorsOnForm(errorsHtml, "#{@note.discussion_id}"); diff --git a/features/steps/project/project_merge_requests.rb b/features/steps/project/project_merge_requests.rb index 3dc934a611..7b0f8cc7ad 100644 --- a/features/steps/project/project_merge_requests.rb +++ b/features/steps/project/project_merge_requests.rb @@ -115,19 +115,26 @@ class ProjectMergeRequests < Spinach::FeatureSteps And 'I leave a comment on the diff page' do init_diff_note - within('.js-temp-notes-holder') do + within('.js-discussion-note-form') do fill_in "note_note", with: "One comment to rule them all" click_button "Add Comment" end + + within ".note-text" do + page.should have_content "One comment to rule them all" + end end And 'I leave a comment like "Line is wrong" on line 185 of the first file' do init_diff_note - within(".js-temp-notes-holder") do + within(".js-discussion-note-form") do fill_in "note_note", with: "Line is wrong" click_button "Add Comment" - sleep 0.05 + end + + within ".note-text" do + page.should have_content "Line is wrong" end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index d29bed4dea..f92d479d02 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -108,7 +108,7 @@ describe "On a merge request", js: true do within("#note_#{note.id}") do should have_css(".note-last-update small") - find(".note-last-update small").text.should match(/Edited just now/) + find(".note-last-update small").text.should match(/Edited less than a minute ago/) end end end