Commit 7bf0add0d173333b87f80ead081a4e2952a1688e

Authored by Dmitriy Zaporozhets
2 parents 4cd7a563 5f7db3e9

Merge branch 'refactor/notes' of /home/git/repositories/gitlab/gitlabhq

app/assets/javascripts/notes.js
@@ -1,587 +0,0 @@ @@ -1,587 +0,0 @@
1 -var NoteList = {  
2 - id: null,  
3 - notes_path: null,  
4 - target_params: null,  
5 - target_id: 0,  
6 - target_type: null,  
7 -  
8 - init: function(tid, tt, path) {  
9 - NoteList.notes_path = path + ".json";  
10 - NoteList.target_id = tid;  
11 - NoteList.target_type = tt;  
12 - NoteList.target_params = "target_type=" + NoteList.target_type + "&target_id=" + NoteList.target_id;  
13 -  
14 - NoteList.setupMainTargetNoteForm();  
15 -  
16 - // get initial set of notes  
17 - NoteList.getContent();  
18 -  
19 - // Unbind events to prevent firing twice  
20 - $(document).off("click", ".js-add-diff-note-button");  
21 - $(document).off("click", ".js-discussion-reply-button");  
22 - $(document).off("click", ".js-note-preview-button");  
23 - $(document).off("click", ".js-note-attachment-input");  
24 - $(document).off("click", ".js-close-discussion-note-form");  
25 - $(document).off("click", ".js-note-delete");  
26 - $(document).off("click", ".js-note-edit");  
27 - $(document).off("click", ".js-note-edit-cancel");  
28 - $(document).off("click", ".js-note-attachment-delete");  
29 - $(document).off("click", ".js-choose-note-attachment-button");  
30 - $(document).off("click", ".js-show-outdated-discussion");  
31 -  
32 - $(document).off("ajax:complete", ".js-main-target-form");  
33 -  
34 -  
35 - // add a new diff note  
36 - $(document).on("click",  
37 - ".js-add-diff-note-button",  
38 - NoteList.addDiffNote);  
39 -  
40 - // reply to diff/discussion notes  
41 - $(document).on("click",  
42 - ".js-discussion-reply-button",  
43 - NoteList.replyToDiscussionNote);  
44 -  
45 - // setup note preview  
46 - $(document).on("click",  
47 - ".js-note-preview-button",  
48 - NoteList.previewNote);  
49 -  
50 - // update the file name when an attachment is selected  
51 - $(document).on("change",  
52 - ".js-note-attachment-input",  
53 - NoteList.updateFormAttachment);  
54 -  
55 - // hide diff note form  
56 - $(document).on("click",  
57 - ".js-close-discussion-note-form",  
58 - NoteList.removeDiscussionNoteForm);  
59 -  
60 - // remove a note (in general)  
61 - $(document).on("click",  
62 - ".js-note-delete",  
63 - NoteList.removeNote);  
64 -  
65 - // show the edit note form  
66 - $(document).on("click",  
67 - ".js-note-edit",  
68 - NoteList.showEditNoteForm);  
69 -  
70 - // cancel note editing  
71 - $(document).on("click",  
72 - ".note-edit-cancel",  
73 - NoteList.cancelNoteEdit);  
74 -  
75 - // delete note attachment  
76 - $(document).on("click",  
77 - ".js-note-attachment-delete",  
78 - NoteList.deleteNoteAttachment);  
79 -  
80 - // update the note after editing  
81 - $(document).on("ajax:complete",  
82 - "form.edit_note",  
83 - NoteList.updateNote);  
84 -  
85 - // reset main target form after submit  
86 - $(document).on("ajax:complete",  
87 - ".js-main-target-form",  
88 - NoteList.resetMainTargetForm);  
89 -  
90 -  
91 - $(document).on("click",  
92 - ".js-choose-note-attachment-button",  
93 - NoteList.chooseNoteAttachment);  
94 -  
95 - $(document).on("click",  
96 - ".js-show-outdated-discussion",  
97 - function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() });  
98 - },  
99 -  
100 -  
101 - /**  
102 - * When clicking on buttons  
103 - */  
104 -  
105 - /**  
106 - * Called when clicking on the "add a comment" button on the side of a diff line.  
107 - *  
108 - * Inserts a temporary row for the form below the line.  
109 - * Sets up the form and shows it.  
110 - */  
111 - addDiffNote: function(e) {  
112 - e.preventDefault();  
113 -  
114 - // find the form  
115 - var form = $(".js-new-note-form");  
116 - var row = $(this).closest("tr");  
117 - var nextRow = row.next();  
118 -  
119 - // does it already have notes?  
120 - if (nextRow.is(".notes_holder")) {  
121 - $.proxy(NoteList.replyToDiscussionNote,  
122 - nextRow.find(".js-discussion-reply-button")  
123 - ).call();  
124 - } else {  
125 - // add a notes row and insert the form  
126 - row.after('<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"></td></tr>');  
127 - form.clone().appendTo(row.next().find(".notes_content"));  
128 -  
129 - // show the form  
130 - NoteList.setupDiscussionNoteForm($(this), row.next().find("form"));  
131 - }  
132 - },  
133 -  
134 - /**  
135 - * Called when clicking the "Choose File" button.  
136 - *  
137 - * Opens the file selection dialog.  
138 - */  
139 - chooseNoteAttachment: function() {  
140 - var form = $(this).closest("form");  
141 -  
142 - form.find(".js-note-attachment-input").click();  
143 - },  
144 -  
145 - /**  
146 - * Shows the note preview.  
147 - *  
148 - * Lets the server render GFM into Html and displays it.  
149 - *  
150 - * Note: uses the Toggler behavior to toggle preview/edit views/buttons  
151 - */  
152 - previewNote: function(e) {  
153 - e.preventDefault();  
154 -  
155 - var form = $(this).closest("form");  
156 - var preview = form.find('.js-note-preview');  
157 - var noteText = form.find('.js-note-text').val();  
158 -  
159 - if(noteText.trim().length === 0) {  
160 - preview.text('Nothing to preview.');  
161 - } else {  
162 - preview.text('Loading...');  
163 - $.post($(this).data('url'), {note: noteText})  
164 - .success(function(previewData) {  
165 - preview.html(previewData);  
166 - });  
167 - }  
168 - },  
169 -  
170 - /**  
171 - * Called in response to "cancel" on a diff note form.  
172 - *  
173 - * Shows the reply button again.  
174 - * Removes the form and if necessary it's temporary row.  
175 - */  
176 - removeDiscussionNoteForm: function() {  
177 - var form = $(this).closest("form");  
178 - var row = form.closest("tr");  
179 -  
180 - // show the reply button (will only work for replies)  
181 - form.prev(".js-discussion-reply-button").show();  
182 -  
183 - if (row.is(".js-temp-notes-holder")) {  
184 - // remove temporary row for diff lines  
185 - row.remove();  
186 - } else {  
187 - // only remove the form  
188 - form.remove();  
189 - }  
190 - },  
191 -  
192 - /**  
193 - * Called in response to deleting a note of any kind.  
194 - *  
195 - * Removes the actual note from view.  
196 - * Removes the whole discussion if the last note is being removed.  
197 - */  
198 - removeNote: function() {  
199 - var note = $(this).closest(".note");  
200 - var notes = note.closest(".notes");  
201 -  
202 - // check if this is the last note for this line  
203 - if (notes.find(".note").length === 1) {  
204 - // for discussions  
205 - notes.closest(".discussion").remove();  
206 -  
207 - // for diff lines  
208 - notes.closest("tr").remove();  
209 - }  
210 -  
211 - note.remove();  
212 - NoteList.updateVotes();  
213 - },  
214 -  
215 - /**  
216 - * Called in response to clicking the edit note link  
217 - *  
218 - * Replaces the note text with the note edit form  
219 - * Adds a hidden div with the original content of the note to fill the edit note form with  
220 - * if the user cancels  
221 - */  
222 - showEditNoteForm: function(e) {  
223 - e.preventDefault();  
224 - var note = $(this).closest(".note");  
225 - note.find(".note-text").hide();  
226 -  
227 - // Show the attachment delete link  
228 - note.find(".js-note-attachment-delete").show();  
229 -  
230 - GitLab.GfmAutoComplete.setup();  
231 -  
232 - var form = note.find(".note-edit-form");  
233 - form.show();  
234 -  
235 - var textarea = form.find("textarea");  
236 - if (form.find(".note-original-content").length === 0) {  
237 - var p = $("<p></p>").text(textarea.val());  
238 - var hidden_div = $('<div class="note-original-content"></div>').append(p);  
239 - form.append(hidden_div);  
240 - hidden_div.hide();  
241 - }  
242 - textarea.focus();  
243 - },  
244 -  
245 - /**  
246 - * Called in response to clicking the cancel button when editing a note  
247 - *  
248 - * Resets and hides the note editing form  
249 - */  
250 - cancelNoteEdit: function(e) {  
251 - e.preventDefault();  
252 - var note = $(this).closest(".note");  
253 - NoteList.resetNoteEditing(note);  
254 - },  
255 -  
256 -  
257 - /**  
258 - * Called in response to clicking the delete attachment link  
259 - *  
260 - * Removes the attachment wrapper view, including image tag if it exists  
261 - * Resets the note editing form  
262 - */  
263 - deleteNoteAttachment: function() {  
264 - var note = $(this).closest(".note");  
265 - note.find(".note-attachment").remove();  
266 - NoteList.resetNoteEditing(note);  
267 - NoteList.rewriteTimestamp(note.find(".note-last-update"));  
268 - },  
269 -  
270 -  
271 - /**  
272 - * Called when clicking on the "reply" button for a diff line.  
273 - *  
274 - * Shows the note form below the notes.  
275 - */  
276 - replyToDiscussionNote: function() {  
277 - // find the form  
278 - var form = $(".js-new-note-form");  
279 -  
280 - // hide reply button  
281 - $(this).hide();  
282 - // insert the form after the button  
283 - form.clone().insertAfter($(this));  
284 -  
285 - // show the form  
286 - NoteList.setupDiscussionNoteForm($(this), $(this).next("form"));  
287 - },  
288 -  
289 -  
290 - /**  
291 - * Helper for inserting and setting up note forms.  
292 - */  
293 -  
294 -  
295 - /**  
296 - * Called in response to creating a note failing validation.  
297 - *  
298 - * Adds the rendered errors to the respective form.  
299 - * If "discussionId" is null or undefined, the main target form is assumed.  
300 - */  
301 - errorsOnForm: function(errorsHtml, discussionId) {  
302 - // find the form  
303 - if (discussionId) {  
304 - var form = $("form[rel='"+discussionId+"']");  
305 - } else {  
306 - var form = $(".js-main-target-form");  
307 - }  
308 -  
309 - form.find(".js-errors").remove();  
310 - form.prepend(errorsHtml);  
311 -  
312 - form.find(".js-note-text").focus();  
313 - },  
314 -  
315 -  
316 - /**  
317 - * Shows the diff/discussion form and does some setup on it.  
318 - *  
319 - * Sets some hidden fields in the form.  
320 - *  
321 - * Note: dataHolder must have the "discussionId", "lineCode", "noteableType"  
322 - * and "noteableId" data attributes set.  
323 - */  
324 - setupDiscussionNoteForm: function(dataHolder, form) {  
325 - // setup note target  
326 - form.attr("rel", dataHolder.data("discussionId"));  
327 - form.find("#note_commit_id").val(dataHolder.data("commitId"));  
328 - form.find("#note_line_code").val(dataHolder.data("lineCode"));  
329 - form.find("#note_noteable_type").val(dataHolder.data("noteableType"));  
330 - form.find("#note_noteable_id").val(dataHolder.data("noteableId"));  
331 -  
332 - NoteList.setupNoteForm(form);  
333 -  
334 - form.find(".js-note-text").focus();  
335 - },  
336 -  
337 - /**  
338 - * Shows the main form and does some setup on it.  
339 - *  
340 - * Sets some hidden fields in the form.  
341 - */  
342 - setupMainTargetNoteForm: function() {  
343 - // find the form  
344 - var form = $(".js-new-note-form");  
345 - // insert the form after the button  
346 - form.clone().replaceAll($(".js-main-target-form"));  
347 -  
348 - form = form.prev("form");  
349 -  
350 - // show the form  
351 - NoteList.setupNoteForm(form);  
352 -  
353 - // fix classes  
354 - form.removeClass("js-new-note-form");  
355 - form.addClass("js-main-target-form");  
356 -  
357 - // remove unnecessary fields and buttons  
358 - form.find("#note_line_code").remove();  
359 - form.find(".js-close-discussion-note-form").remove();  
360 - },  
361 -  
362 - /**  
363 - * General note form setup.  
364 - *  
365 - * * deactivates the submit button when text is empty  
366 - * * hides the preview button when text is empty  
367 - * * setup GFM auto complete  
368 - * * show the form  
369 - */  
370 - setupNoteForm: function(form) {  
371 - disableButtonIfEmptyField(form.find(".js-note-text"), form.find(".js-comment-button"));  
372 -  
373 - form.removeClass("js-new-note-form");  
374 -  
375 - // setup preview buttons  
376 - form.find(".js-note-edit-button, .js-note-preview-button")  
377 - .tooltip({ placement: 'left' });  
378 -  
379 - previewButton = form.find(".js-note-preview-button");  
380 - form.find(".js-note-text").on("input", function() {  
381 - if ($(this).val().trim() !== "") {  
382 - previewButton.removeClass("turn-off").addClass("turn-on");  
383 - } else {  
384 - previewButton.removeClass("turn-on").addClass("turn-off");  
385 - }  
386 - });  
387 -  
388 - // remove notify commit author checkbox for non-commit notes  
389 - if (form.find("#note_noteable_type").val() !== "Commit") {  
390 - form.find(".js-notify-commit-author").remove();  
391 - }  
392 -  
393 - GitLab.GfmAutoComplete.setup();  
394 -  
395 - form.show();  
396 - },  
397 -  
398 -  
399 - /**  
400 - * Handle loading the initial set of notes.  
401 - * And set up loading more notes when scrolling to the bottom of the page.  
402 - */  
403 -  
404 -  
405 - /**  
406 - * Gets an initial set of notes.  
407 - */  
408 - getContent: function() {  
409 - $.ajax({  
410 - url: NoteList.notes_path,  
411 - data: NoteList.target_params,  
412 - complete: function(){ $('.js-notes-busy').removeClass("loading")},  
413 - beforeSend: function() { $('.js-notes-busy').addClass("loading") },  
414 - success: function(data) {  
415 - NoteList.setContent(data.html);  
416 - },  
417 - dataType: "json"  
418 - });  
419 - },  
420 -  
421 - /**  
422 - * Called in response to getContent().  
423 - * Replaces the content of #notes-list with the given html.  
424 - */  
425 - setContent: function(html) {  
426 - $("#notes-list").html(html);  
427 - },  
428 -  
429 -  
430 - /**  
431 - * Adds a single common note to #notes-list.  
432 - */  
433 - appendNewNote: function(id, html) {  
434 - $("#notes-list").append(html);  
435 - NoteList.updateVotes();  
436 - },  
437 -  
438 - /**  
439 - * Adds a single discussion note to #notes-list.  
440 - *  
441 - * Also removes the corresponding form.  
442 - */  
443 - appendNewDiscussionNote: function(discussionId, diffRowHtml, noteHtml) {  
444 - var form = $("form[rel='"+discussionId+"']");  
445 - var row = form.closest("tr");  
446 -  
447 - // is this the first note of discussion?  
448 - if (row.is(".js-temp-notes-holder")) {  
449 - // insert the note and the reply button after the temp row  
450 - row.after(diffRowHtml);  
451 - // remove the note (will be added again below)  
452 - row.next().find(".note").remove();  
453 - }  
454 -  
455 - // append new note to all matching discussions  
456 - $(".notes[rel='"+discussionId+"']").append(noteHtml);  
457 -  
458 - // cleanup after successfully creating a diff/discussion note  
459 - $.proxy(NoteList.removeDiscussionNoteForm, form).call();  
460 - },  
461 -  
462 - /**  
463 - * Called in response the main target form has been successfully submitted.  
464 - *  
465 - * Removes any errors.  
466 - * Resets text and preview.  
467 - * Resets buttons.  
468 - */  
469 - resetMainTargetForm: function(){  
470 - var form = $(this);  
471 -  
472 - // remove validation errors  
473 - form.find(".js-errors").remove();  
474 -  
475 - // reset text and preview  
476 - var previewContainer = form.find(".js-toggler-container.note_text_and_preview");  
477 - if (previewContainer.is(".on")) {  
478 - previewContainer.removeClass("on");  
479 - }  
480 - form.find(".js-note-text").val("").trigger("input");  
481 - },  
482 -  
483 - /**  
484 - * Called after an attachment file has been selected.  
485 - *  
486 - * Updates the file name for the selected attachment.  
487 - */  
488 - updateFormAttachment: function() {  
489 - var form = $(this).closest("form");  
490 -  
491 - // get only the basename  
492 - var filename = $(this).val().replace(/^.*[\\\/]/, '');  
493 -  
494 - form.find(".js-attachment-filename").text(filename);  
495 - },  
496 -  
497 - /**  
498 - * Recalculates the votes and updates them (if they are displayed at all).  
499 - *  
500 - * Assumes all relevant notes are displayed (i.e. there are no more notes to  
501 - * load via getMore()).  
502 - * Might produce inaccurate results when not all notes have been loaded and a  
503 - * recalculation is triggered (e.g. when deleting a note).  
504 - */  
505 - updateVotes: function() {  
506 - var votes = $("#votes .votes");  
507 - var notes = $("#notes-list .note .vote");  
508 -  
509 - // only update if there is a vote display  
510 - if (votes.size()) {  
511 - var upvotes = notes.filter(".upvote").size();  
512 - var downvotes = notes.filter(".downvote").size();  
513 - var votesCount = upvotes + downvotes;  
514 - var upvotesPercent = votesCount ? (100.0 / votesCount * upvotes) : 0;  
515 - var downvotesPercent = votesCount ? (100.0 - upvotesPercent) : 0;  
516 -  
517 - // change vote bar lengths  
518 - votes.find(".bar-success").css("width", upvotesPercent+"%");  
519 - votes.find(".bar-danger").css("width", downvotesPercent+"%");  
520 - // replace vote numbers  
521 - votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes));  
522 - votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));  
523 - }  
524 - },  
525 -  
526 - /**  
527 - * Called in response to the edit note form being submitted  
528 - *  
529 - * Updates the current note field.  
530 - * Hides the edit note form  
531 - */  
532 - updateNote: function(e, xhr, settings) {  
533 - response = JSON.parse(xhr.responseText);  
534 - if (response.success) {  
535 - var note_li = $("#note_" + response.id);  
536 - var note_text = note_li.find(".note-text");  
537 - note_text.html(response.note).show();  
538 -  
539 - var note_form = note_li.find(".note-edit-form");  
540 - var original_content = note_form.find(".note-original-content");  
541 - original_content.remove();  
542 - note_form.hide();  
543 - note_form.find(".btn-save").enableButton();  
544 -  
545 - // Update the "Edited at xxx label" on the note to show it's just been updated  
546 - NoteList.rewriteTimestamp(note_li.find(".note-last-update"));  
547 - }  
548 - },  
549 -  
550 - /**  
551 - * Called in response to the 'cancel note' link clicked, or after deleting a note attachment  
552 - *  
553 - * Hides the edit note form and shows the note  
554 - * Resets the edit note form textarea with the original content of the note  
555 - */  
556 - resetNoteEditing: function(note) {  
557 - note.find(".note-text").show();  
558 -  
559 - // Hide the attachment delete link  
560 - note.find(".js-note-attachment-delete").hide();  
561 -  
562 - // Put the original content of the note back into the edit form textarea  
563 - var form = note.find(".note-edit-form");  
564 - var original_content = form.find(".note-original-content");  
565 - form.find("textarea").val(original_content.text());  
566 - original_content.remove();  
567 -  
568 - note.find(".note-edit-form").hide();  
569 - },  
570 -  
571 - /**  
572 - * Utility function to generate new timestamp text for a note  
573 - *  
574 - */  
575 - rewriteTimestamp: function(element) {  
576 - // Strip all newlines from the existing timestamp  
577 - var ts = element.text().replace(/\n/g, ' ').trim();  
578 -  
579 - // If the timestamp already has '(Edited xxx ago)' text, remove it  
580 - ts = ts.replace(new RegExp("\\(Edited [A-Za-z0-9 ]+\\)$", "gi"), "");  
581 -  
582 - // Append "(Edited just now)"  
583 - ts = (ts + " <small>(Edited just now)</small>");  
584 -  
585 - element.html(ts);  
586 - }  
587 -};  
app/assets/javascripts/notes.js.coffee 0 → 100644
@@ -0,0 +1,418 @@ @@ -0,0 +1,418 @@
  1 +class Notes
  2 + constructor: (notes_url, note_ids) ->
  3 + @notes_url = notes_url
  4 + @notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root?
  5 + @note_ids = note_ids
  6 + @initRefresh()
  7 + @setupMainTargetNoteForm()
  8 + @cleanBinding()
  9 + @addBinding()
  10 +
  11 + addBinding: ->
  12 + # add note to UI after creation
  13 + $(document).on "ajax:success", ".js-main-target-form", @addNote
  14 + $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
  15 +
  16 + # change note in UI after update
  17 + $(document).on "ajax:success", "form.edit_note", @updateNote
  18 +
  19 + # Edit note link
  20 + $(document).on "click", ".js-note-edit", @showEditForm
  21 + $(document).on "click", ".note-edit-cancel", @cancelEdit
  22 +
  23 + # remove a note (in general)
  24 + $(document).on "click", ".js-note-delete", @removeNote
  25 +
  26 + # delete note attachment
  27 + $(document).on "click", ".js-note-attachment-delete", @removeAttachment
  28 +
  29 + # Preview button
  30 + $(document).on "click", ".js-note-preview-button", @previewNote
  31 +
  32 + # reset main target form after submit
  33 + $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm
  34 +
  35 + # attachment button
  36 + $(document).on "click", ".js-choose-note-attachment-button", @chooseNoteAttachment
  37 +
  38 + # reply to diff/discussion notes
  39 + $(document).on "click", ".js-discussion-reply-button", @replyToDiscussionNote
  40 +
  41 + # add diff note
  42 + $(document).on "click", ".js-add-diff-note-button", @addDiffNote
  43 +
  44 + # hide diff note form
  45 + $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm
  46 +
  47 + cleanBinding: ->
  48 + $(document).off "ajax:success", ".js-main-target-form"
  49 + $(document).off "ajax:success", ".js-discussion-note-form"
  50 + $(document).off "ajax:success", "form.edit_note"
  51 + $(document).off "click", ".js-note-edit"
  52 + $(document).off "click", ".note-edit-cancel"
  53 + $(document).off "click", ".js-note-delete"
  54 + $(document).off "click", ".js-note-attachment-delete"
  55 + $(document).off "click", ".js-note-preview-button"
  56 + $(document).off "ajax:complete", ".js-main-target-form"
  57 + $(document).off "click", ".js-choose-note-attachment-button"
  58 + $(document).off "click", ".js-discussion-reply-button"
  59 + $(document).off "click", ".js-add-diff-note-button"
  60 +
  61 +
  62 + initRefresh: ->
  63 + setInterval =>
  64 + @refresh()
  65 + , 15000
  66 +
  67 + refresh: ->
  68 + @getContent()
  69 +
  70 + getContent: ->
  71 + $.ajax
  72 + url: @notes_url
  73 + dataType: "json"
  74 + success: (data) =>
  75 + notes = data.notes
  76 + $.each notes, (i, note) =>
  77 + # render note if it not present in loaded list
  78 + # or skip if rendered
  79 + if $.inArray(note.id, @note_ids) == -1
  80 + @note_ids.push(note.id)
  81 + @renderNote(note)
  82 +
  83 +
  84 + ###
  85 + Render note in main comments area.
  86 +
  87 + Note: for rendering inline notes use renderDiscussionNote
  88 + ###
  89 + renderNote: (note) ->
  90 + $('ul.main-notes-list').append(note.html)
  91 +
  92 + ###
  93 + Render note in discussion area.
  94 +
  95 + Note: for rendering inline notes use renderDiscussionNote
  96 + ###
  97 + renderDiscussionNote: (note) ->
  98 + form = $("form[rel='" + note.discussion_id + "']")
  99 + row = form.closest("tr")
  100 +
  101 + # is this the first note of discussion?
  102 + if row.is(".js-temp-notes-holder")
  103 + # insert the note and the reply button after the temp row
  104 + row.after note.discussion_html
  105 +
  106 + # remove the note (will be added again below)
  107 + row.next().find(".note").remove()
  108 +
  109 + # append new note to all matching discussions
  110 + $(".notes[rel='" + note.discussion_id + "']").append note.html
  111 +
  112 + # cleanup after successfully creating a diff/discussion note
  113 + @removeDiscussionNoteForm(form)
  114 +
  115 + ###
  116 + Shows the note preview.
  117 +
  118 + Lets the server render GFM into Html and displays it.
  119 +
  120 + Note: uses the Toggler behavior to toggle preview/edit views/buttons
  121 + ###
  122 + previewNote: (e) ->
  123 + e.preventDefault()
  124 + form = $(this).closest("form")
  125 + preview = form.find(".js-note-preview")
  126 + noteText = form.find(".js-note-text").val()
  127 + if noteText.trim().length is 0
  128 + preview.text "Nothing to preview."
  129 + else
  130 + preview.text "Loading..."
  131 + $.post($(this).data("url"),
  132 + note: noteText
  133 + ).success (previewData) ->
  134 + preview.html previewData
  135 +
  136 + ###
  137 + Called in response the main target form has been successfully submitted.
  138 +
  139 + Removes any errors.
  140 + Resets text and preview.
  141 + Resets buttons.
  142 + ###
  143 + resetMainTargetForm: ->
  144 + form = $(".js-main-target-form")
  145 +
  146 + # remove validation errors
  147 + form.find(".js-errors").remove()
  148 +
  149 + # reset text and preview
  150 + previewContainer = form.find(".js-toggler-container.note_text_and_preview")
  151 + previewContainer.removeClass "on" if previewContainer.is(".on")
  152 + form.find(".js-note-text").val("").trigger "input"
  153 +
  154 + ###
  155 + Called when clicking the "Choose File" button.
  156 +
  157 + Opens the file selection dialog.
  158 + ###
  159 + chooseNoteAttachment: ->
  160 + form = $(this).closest("form")
  161 + form.find(".js-note-attachment-input").click()
  162 +
  163 + ###
  164 + Shows the main form and does some setup on it.
  165 +
  166 + Sets some hidden fields in the form.
  167 + ###
  168 + setupMainTargetNoteForm: ->
  169 +
  170 + # find the form
  171 + form = $(".js-new-note-form")
  172 +
  173 + # insert the form after the button
  174 + form.clone().replaceAll $(".js-main-target-form")
  175 + form = form.prev("form")
  176 +
  177 + # show the form
  178 + @setupNoteForm(form)
  179 +
  180 + # fix classes
  181 + form.removeClass "js-new-note-form"
  182 + form.addClass "js-main-target-form"
  183 +
  184 + # remove unnecessary fields and buttons
  185 + form.find("#note_line_code").remove()
  186 + form.find(".js-close-discussion-note-form").remove()
  187 +
  188 + ###
  189 + General note form setup.
  190 +
  191 + deactivates the submit button when text is empty
  192 + hides the preview button when text is empty
  193 + setup GFM auto complete
  194 + show the form
  195 + ###
  196 + setupNoteForm: (form) ->
  197 + disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button")
  198 + form.removeClass "js-new-note-form"
  199 +
  200 + # setup preview buttons
  201 + form.find(".js-note-edit-button, .js-note-preview-button").tooltip placement: "left"
  202 + previewButton = form.find(".js-note-preview-button")
  203 + form.find(".js-note-text").on "input", ->
  204 + if $(this).val().trim() isnt ""
  205 + previewButton.removeClass("turn-off").addClass "turn-on"
  206 + else
  207 + previewButton.removeClass("turn-on").addClass "turn-off"
  208 +
  209 +
  210 + # remove notify commit author checkbox for non-commit notes
  211 + form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit"
  212 + GitLab.GfmAutoComplete.setup()
  213 + form.show()
  214 +
  215 +
  216 + ###
  217 + Called in response to the new note form being submitted
  218 +
  219 + Adds new note to list.
  220 + ###
  221 + addNote: (xhr, note, status) =>
  222 + @note_ids.push(note.id)
  223 + @renderNote(note)
  224 +
  225 + ###
  226 + Called in response to the new note form being submitted
  227 +
  228 + Adds new note to list.
  229 + ###
  230 + addDiscussionNote: (xhr, note, status) =>
  231 + @note_ids.push(note.id)
  232 + @renderDiscussionNote(note)
  233 +
  234 + ###
  235 + Called in response to the edit note form being submitted
  236 +
  237 + Updates the current note field.
  238 + ###
  239 + updateNote: (xhr, note, status) =>
  240 + note_li = $("#note_" + note.id)
  241 + note_li.replaceWith(note.html)
  242 +
  243 + ###
  244 + Called in response to clicking the edit note link
  245 +
  246 + Replaces the note text with the note edit form
  247 + Adds a hidden div with the original content of the note to fill the edit note form with
  248 + if the user cancels
  249 + ###
  250 + showEditForm: (e) ->
  251 + e.preventDefault()
  252 + note = $(this).closest(".note")
  253 + note.find(".note-text").hide()
  254 +
  255 + # Show the attachment delete link
  256 + note.find(".js-note-attachment-delete").show()
  257 + GitLab.GfmAutoComplete.setup()
  258 + form = note.find(".note-edit-form")
  259 + form.show()
  260 + form.find("textarea").focus()
  261 +
  262 + ###
  263 + Called in response to clicking the edit note link
  264 +
  265 + Hides edit form
  266 + ###
  267 + cancelEdit: (e) ->
  268 + e.preventDefault()
  269 + note = $(this).closest(".note")
  270 + note.find(".note-text").show()
  271 + note.find(".js-note-attachment-delete").hide()
  272 + note.find(".note-edit-form").hide()
  273 +
  274 + ###
  275 + Called in response to deleting a note of any kind.
  276 +
  277 + Removes the actual note from view.
  278 + Removes the whole discussion if the last note is being removed.
  279 + ###
  280 + removeNote: ->
  281 + note = $(this).closest(".note")
  282 + notes = note.closest(".notes")
  283 +
  284 + # check if this is the last note for this line
  285 + if notes.find(".note").length is 1
  286 +
  287 + # for discussions
  288 + notes.closest(".discussion").remove()
  289 +
  290 + # for diff lines
  291 + notes.closest("tr").remove()
  292 +
  293 + note.remove()
  294 +
  295 + ###
  296 + Called in response to clicking the delete attachment link
  297 +
  298 + Removes the attachment wrapper view, including image tag if it exists
  299 + Resets the note editing form
  300 + ###
  301 + removeAttachment: ->
  302 + note = $(this).closest(".note")
  303 + note.find(".note-attachment").remove()
  304 + note.find(".note-text").show()
  305 + note.find(".js-note-attachment-delete").hide()
  306 + note.find(".note-edit-form").hide()
  307 +
  308 + ###
  309 + Called when clicking on the "reply" button for a diff line.
  310 +
  311 + Shows the note form below the notes.
  312 + ###
  313 + replyToDiscussionNote: (e) =>
  314 + form = $(".js-new-note-form")
  315 + replyLink = $(e.target)
  316 + replyLink.hide()
  317 +
  318 + # insert the form after the button
  319 + form.clone().insertAfter replyLink
  320 +
  321 + # show the form
  322 + @setupDiscussionNoteForm(replyLink, replyLink.next("form"))
  323 +
  324 + ###
  325 + Shows the diff or discussion form and does some setup on it.
  326 +
  327 + Sets some hidden fields in the form.
  328 +
  329 + Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
  330 + and "noteableId" data attributes set.
  331 + ###
  332 + setupDiscussionNoteForm: (dataHolder, form) =>
  333 + # setup note target
  334 + form.attr "rel", dataHolder.data("discussionId")
  335 + form.find("#note_commit_id").val dataHolder.data("commitId")
  336 + form.find("#note_line_code").val dataHolder.data("lineCode")
  337 + form.find("#note_noteable_type").val dataHolder.data("noteableType")
  338 + form.find("#note_noteable_id").val dataHolder.data("noteableId")
  339 + @setupNoteForm form
  340 + form.find(".js-note-text").focus()
  341 + form.addClass "js-discussion-note-form"
  342 +
  343 + ###
  344 + General note form setup.
  345 +
  346 + deactivates the submit button when text is empty
  347 + hides the preview button when text is empty
  348 + setup GFM auto complete
  349 + show the form
  350 + ###
  351 + setupNoteForm: (form) =>
  352 + disableButtonIfEmptyField form.find(".js-note-text"), form.find(".js-comment-button")
  353 + form.removeClass "js-new-note-form"
  354 + form.removeClass "js-new-note-form"
  355 + GitLab.GfmAutoComplete.setup()
  356 +
  357 + # setup preview buttons
  358 + previewButton = form.find(".js-note-preview-button")
  359 + form.find(".js-note-text").on "input", ->
  360 + if $(this).val().trim() isnt ""
  361 + previewButton.removeClass("turn-off").addClass "turn-on"
  362 + else
  363 + previewButton.removeClass("turn-on").addClass "turn-off"
  364 +
  365 + form.show()
  366 +
  367 + ###
  368 + Called when clicking on the "add a comment" button on the side of a diff line.
  369 +
  370 + Inserts a temporary row for the form below the line.
  371 + Sets up the form and shows it.
  372 + ###
  373 + addDiffNote: (e) =>
  374 + e.preventDefault()
  375 + link = e.target
  376 + form = $(".js-new-note-form")
  377 + row = $(link).closest("tr")
  378 + nextRow = row.next()
  379 +
  380 + # does it already have notes?
  381 + if nextRow.is(".notes_holder")
  382 + replyButton = nextRow.find(".js-discussion-reply-button")
  383 + if replyButton.length > 0
  384 + $.proxy(@replyToDiscussionNote, replyButton).call()
  385 + else
  386 + # add a notes row and insert the form
  387 + row.after "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>"
  388 + form.clone().appendTo row.next().find(".notes_content")
  389 +
  390 + # show the form
  391 + @setupDiscussionNoteForm $(link), row.next().find("form")
  392 +
  393 + ###
  394 + Called in response to "cancel" on a diff note form.
  395 +
  396 + Shows the reply button again.
  397 + Removes the form and if necessary it's temporary row.
  398 + ###
  399 + removeDiscussionNoteForm: (form)->
  400 + row = form.closest("tr")
  401 +
  402 + # show the reply button (will only work for replies)
  403 + form.prev(".js-discussion-reply-button").show()
  404 + if row.is(".js-temp-notes-holder")
  405 + # remove temporary row for diff lines
  406 + row.remove()
  407 + else
  408 + # only remove the form
  409 + form.remove()
  410 +
  411 +
  412 + cancelDiscussionForm: (e) =>
  413 + e.preventDefault()
  414 + form = $(".js-new-note-form")
  415 + form = $(e.target).closest(".js-discussion-note-form")
  416 + @removeDiscussionNoteForm(form)
  417 +
  418 +@Notes = Notes
app/assets/stylesheets/sections/notes.scss
@@ -257,12 +257,12 @@ ul.notes { @@ -257,12 +257,12 @@ ul.notes {
257 .file, 257 .file,
258 .discussion { 258 .discussion {
259 .new_note { 259 .new_note {
260 - margin: 8px 5px 8px 0; 260 + margin: 0;
  261 + border: none;
261 } 262 }
262 } 263 }
263 .new_note { 264 .new_note {
264 display: none; 265 display: none;
265 -  
266 .buttons { 266 .buttons {
267 float: left; 267 float: left;
268 margin-top: 8px; 268 margin-top: 8px;
app/controllers/projects/commit_controller.rb
@@ -24,8 +24,8 @@ class Projects::CommitController &lt; Projects::ApplicationController @@ -24,8 +24,8 @@ class Projects::CommitController &lt; Projects::ApplicationController
24 @line_notes = result[:line_notes] 24 @line_notes = result[:line_notes]
25 @branches = result[:branches] 25 @branches = result[:branches]
26 @notes_count = result[:notes_count] 26 @notes_count = result[:notes_count]
27 - @target_type = :commit  
28 - @target_id = @commit.id 27 + @notes = project.notes.for_commit_id(@commit.id).not_inline.fresh
  28 + @noteable = @commit
29 29
30 @comments_allowed = @reply_allowed = true 30 @comments_allowed = @reply_allowed = true
31 @comments_target = { noteable_type: 'Commit', 31 @comments_target = { noteable_type: 'Commit',
app/controllers/projects/issues_controller.rb
@@ -49,8 +49,8 @@ class Projects::IssuesController &lt; Projects::ApplicationController @@ -49,8 +49,8 @@ class Projects::IssuesController &lt; Projects::ApplicationController
49 49
50 def show 50 def show
51 @note = @project.notes.new(noteable: @issue) 51 @note = @project.notes.new(noteable: @issue)
52 - @target_type = :issue  
53 - @target_id = @issue.id 52 + @notes = @issue.notes.inc_author.fresh
  53 + @noteable = @issue
54 54
55 respond_with(@issue) 55 respond_with(@issue)
56 end 56 end
app/controllers/projects/merge_requests_controller.rb
@@ -198,6 +198,9 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController @@ -198,6 +198,9 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController
198 def define_show_vars 198 def define_show_vars
199 # Build a note object for comment form 199 # Build a note object for comment form
200 @note = @project.notes.new(noteable: @merge_request) 200 @note = @project.notes.new(noteable: @merge_request)
  201 + @notes = @merge_request.mr_and_commit_notes.inc_author.fresh
  202 + @discussions = Note.discussions_from_notes(@notes)
  203 + @noteable = @merge_request
201 204
202 # Get commits from repository 205 # Get commits from repository
203 # or from cache if already merged 206 # or from cache if already merged
@@ -205,9 +208,6 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController @@ -205,9 +208,6 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController
205 208
206 @allowed_to_merge = allowed_to_merge? 209 @allowed_to_merge = allowed_to_merge?
207 @show_merge_controls = @merge_request.opened? && @commits.any? && @allowed_to_merge 210 @show_merge_controls = @merge_request.opened? && @commits.any? && @allowed_to_merge
208 -  
209 - @target_type = :merge_request  
210 - @target_id = @merge_request.id  
211 end 211 end
212 212
213 def allowed_to_merge? 213 def allowed_to_merge?
app/controllers/projects/notes_controller.rb
@@ -2,71 +2,54 @@ class Projects::NotesController &lt; Projects::ApplicationController @@ -2,71 +2,54 @@ class Projects::NotesController &lt; Projects::ApplicationController
2 # Authorize 2 # Authorize
3 before_filter :authorize_read_note! 3 before_filter :authorize_read_note!
4 before_filter :authorize_write_note!, only: [:create] 4 before_filter :authorize_write_note!, only: [:create]
5 -  
6 - respond_to :js 5 + before_filter :authorize_admin_note!, only: [:update, :destroy]
7 6
8 def index 7 def index
9 @notes = Notes::LoadContext.new(project, current_user, params).execute 8 @notes = Notes::LoadContext.new(project, current_user, params).execute
10 - @target_type = params[:target_type].camelize  
11 - @target_id = params[:target_id]  
12 9
13 - if params[:target_type] == "merge_request"  
14 - @discussions = discussions_from_notes  
15 - end 10 + notes_json = { notes: [] }
16 11
17 - respond_to do |format|  
18 - format.html { redirect_to :back }  
19 - format.json do  
20 - render json: {  
21 - html: view_to_html_string("projects/notes/_notes")  
22 - }  
23 - end 12 + @notes.each do |note|
  13 + notes_json[:notes] << {
  14 + id: note.id,
  15 + html: note_to_html(note)
  16 + }
24 end 17 end
  18 +
  19 + render json: notes_json
25 end 20 end
26 21
27 def create 22 def create
28 @note = Notes::CreateContext.new(project, current_user, params).execute 23 @note = Notes::CreateContext.new(project, current_user, params).execute
29 - @target_type = params[:target_type].camelize  
30 - @target_id = params[:target_id]  
31 24
32 respond_to do |format| 25 respond_to do |format|
33 - format.html {redirect_to :back}  
34 - format.js 26 + format.json { render_note_json(@note) }
  27 + format.html { redirect_to :back }
35 end 28 end
36 end 29 end
37 30
38 - def destroy  
39 - @note = @project.notes.find(params[:id])  
40 - return access_denied! unless can?(current_user, :admin_note, @note)  
41 - @note.destroy  
42 - @note.reset_events_cache 31 + def update
  32 + note.update_attributes(params[:note])
  33 + note.reset_events_cache
43 34
44 respond_to do |format| 35 respond_to do |format|
45 - format.js { render nothing: true } 36 + format.json { render_note_json(note) }
  37 + format.html { redirect_to :back }
46 end 38 end
47 end 39 end
48 40
49 - def update  
50 - @note = @project.notes.find(params[:id])  
51 - return access_denied! unless can?(current_user, :admin_note, @note)  
52 -  
53 - @note.update_attributes(params[:note])  
54 - @note.reset_events_cache 41 + def destroy
  42 + note.destroy
  43 + note.reset_events_cache
55 44
56 respond_to do |format| 45 respond_to do |format|
57 - format.js do  
58 - render js: { success: @note.valid?, id: @note.id, note: view_context.markdown(@note.note) }.to_json  
59 - end  
60 - format.html do  
61 - redirect_to :back  
62 - end 46 + format.js { render nothing: true }
63 end 47 end
64 end 48 end
65 49
66 def delete_attachment 50 def delete_attachment
67 - @note = @project.notes.find(params[:id])  
68 - @note.remove_attachment!  
69 - @note.update_attribute(:attachment, nil) 51 + note.remove_attachment!
  52 + note.update_attribute(:attachment, nil)
70 53
71 respond_to do |format| 54 respond_to do |format|
72 format.js { render nothing: true } 55 format.js { render nothing: true }
@@ -77,35 +60,40 @@ class Projects::NotesController &lt; Projects::ApplicationController @@ -77,35 +60,40 @@ class Projects::NotesController &lt; Projects::ApplicationController
77 render text: view_context.markdown(params[:note]) 60 render text: view_context.markdown(params[:note])
78 end 61 end
79 62
80 - protected 63 + private
81 64
82 - def discussion_notes_for(note)  
83 - @notes.select do |other_note|  
84 - note.discussion_id == other_note.discussion_id  
85 - end 65 + def note
  66 + @note ||= @project.notes.find(params[:id])
86 end 67 end
87 68
88 - def discussions_from_notes  
89 - discussion_ids = []  
90 - discussions = [] 69 + def note_to_html(note)
  70 + render_to_string(
  71 + "projects/notes/_note",
  72 + layout: false,
  73 + formats: [:html],
  74 + locals: { note: note }
  75 + )
  76 + end
91 77
92 - @notes.each do |note|  
93 - next if discussion_ids.include?(note.discussion_id)  
94 -  
95 - # don't group notes for the main target  
96 - if note_for_main_target?(note)  
97 - discussions << [note]  
98 - else  
99 - discussions << discussion_notes_for(note)  
100 - discussion_ids << note.discussion_id  
101 - end  
102 - end 78 + def note_to_discussion_html(note)
  79 + render_to_string(
  80 + "projects/notes/_diff_notes_with_reply",
  81 + layout: false,
  82 + formats: [:html],
  83 + locals: { notes: [note] }
  84 + )
  85 + end
103 86
104 - discussions 87 + def render_note_json(note)
  88 + render json: {
  89 + id: note.id,
  90 + discussion_id: note.discussion_id,
  91 + html: note_to_html(note),
  92 + discussion_html: note_to_discussion_html(note)
  93 + }
105 end 94 end
106 95
107 - # Helps to distinguish e.g. commit notes in mr notes list  
108 - def note_for_main_target?(note)  
109 - (@target_type.camelize == note.noteable_type && !note.for_diff_line?) 96 + def authorize_admin_note!
  97 + return access_denied! unless can?(current_user, :admin_note, note)
110 end 98 end
111 end 99 end
app/controllers/projects/snippets_controller.rb
@@ -48,8 +48,8 @@ class Projects::SnippetsController &lt; Projects::ApplicationController @@ -48,8 +48,8 @@ class Projects::SnippetsController &lt; Projects::ApplicationController
48 48
49 def show 49 def show
50 @note = @project.notes.new(noteable: @snippet) 50 @note = @project.notes.new(noteable: @snippet)
51 - @target_type = :snippet  
52 - @target_id = @snippet.id 51 + @notes = @snippet.notes.fresh
  52 + @noteable = @snippet
53 end 53 end
54 54
55 def destroy 55 def destroy
app/helpers/notes_helper.rb
1 module NotesHelper 1 module NotesHelper
2 # Helps to distinguish e.g. commit notes in mr notes list 2 # Helps to distinguish e.g. commit notes in mr notes list
3 def note_for_main_target?(note) 3 def note_for_main_target?(note)
4 - (@target_type.camelize == note.noteable_type && !note.for_diff_line?) 4 + (@noteable.class.name == note.noteable_type && !note.for_diff_line?)
5 end 5 end
6 6
7 def note_target_fields 7 def note_target_fields
@@ -21,14 +21,6 @@ module NotesHelper @@ -21,14 +21,6 @@ module NotesHelper
21 end 21 end
22 end 22 end
23 23
24 - def loading_more_notes?  
25 - params[:loading_more].present?  
26 - end  
27 -  
28 - def loading_new_notes?  
29 - params[:loading_new].present?  
30 - end  
31 -  
32 def note_timestamp(note) 24 def note_timestamp(note)
33 # Shows the created at time and the updated at time if different 25 # Shows the created at time and the updated at time if different
34 ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')} ago" 26 ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')} ago"
@@ -41,4 +33,13 @@ module NotesHelper @@ -41,4 +33,13 @@ module NotesHelper
41 end 33 end
42 ts.html_safe 34 ts.html_safe
43 end 35 end
  36 +
  37 + def noteable_json(noteable)
  38 + {
  39 + id: noteable.id,
  40 + class: noteable.class.name,
  41 + resources: noteable.class.table_name,
  42 + project_id: noteable.project.id,
  43 + }.to_json
  44 + end
44 end 45 end
app/models/note.rb
@@ -56,29 +56,52 @@ class Note &lt; ActiveRecord::Base @@ -56,29 +56,52 @@ class Note &lt; ActiveRecord::Base
56 serialize :st_diff 56 serialize :st_diff
57 before_create :set_diff, if: ->(n) { n.line_code.present? } 57 before_create :set_diff, if: ->(n) { n.line_code.present? }
58 58
59 - def self.create_status_change_note(noteable, project, author, status, source)  
60 - body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_"  
61 -  
62 - create({  
63 - noteable: noteable,  
64 - project: project,  
65 - author: author,  
66 - note: body,  
67 - system: true  
68 - }, without_protection: true)  
69 - end 59 + class << self
  60 + def create_status_change_note(noteable, project, author, status, source)
  61 + body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_"
  62 +
  63 + create({
  64 + noteable: noteable,
  65 + project: project,
  66 + author: author,
  67 + note: body,
  68 + system: true
  69 + }, without_protection: true)
  70 + end
  71 +
  72 + # +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note.
  73 + # Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+.
  74 + def create_cross_reference_note(noteable, mentioner, author, project)
  75 + create({
  76 + noteable: noteable,
  77 + commit_id: (noteable.sha if noteable.respond_to? :sha),
  78 + project: project,
  79 + author: author,
  80 + note: "_mentioned in #{mentioner.gfm_reference}_",
  81 + system: true
  82 + }, without_protection: true)
  83 + end
70 84
71 - # +noteable+ was referenced from +mentioner+, by including GFM in either +mentioner+'s description or an associated Note.  
72 - # Create a system Note associated with +noteable+ with a GFM back-reference to +mentioner+.  
73 - def self.create_cross_reference_note(noteable, mentioner, author, project)  
74 - create({  
75 - noteable: noteable,  
76 - commit_id: (noteable.sha if noteable.respond_to? :sha),  
77 - project: project,  
78 - author: author,  
79 - note: "_mentioned in #{mentioner.gfm_reference}_",  
80 - system: true  
81 - }, without_protection: true) 85 + def discussions_from_notes(notes)
  86 + discussion_ids = []
  87 + discussions = []
  88 +
  89 + notes.each do |note|
  90 + next if discussion_ids.include?(note.discussion_id)
  91 +
  92 + # don't group notes for the main target
  93 + if !note.for_diff_line? && note.noteable_type == "MergeRequest"
  94 + discussions << [note]
  95 + else
  96 + discussions << notes.select do |other_note|
  97 + note.discussion_id == other_note.discussion_id
  98 + end
  99 + discussion_ids << note.discussion_id
  100 + end
  101 + end
  102 +
  103 + discussions
  104 + end
82 end 105 end
83 106
84 # Determine whether or not a cross-reference note already exists. 107 # Determine whether or not a cross-reference note already exists.
@@ -89,7 +112,7 @@ class Note &lt; ActiveRecord::Base @@ -89,7 +112,7 @@ class Note &lt; ActiveRecord::Base
89 def commit_author 112 def commit_author
90 @commit_author ||= 113 @commit_author ||=
91 project.users.find_by_email(noteable.author_email) || 114 project.users.find_by_email(noteable.author_email) ||
92 - project.users.find_by_name(noteable.author_name) 115 + project.users.find_by_name(noteable.author_name)
93 rescue 116 rescue
94 nil 117 nil
95 end 118 end
app/views/projects/notes/_form.html.haml
1 -= 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|  
2 - 1 += 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|
3 = note_target_fields 2 = note_target_fields
4 = f.hidden_field :commit_id 3 = f.hidden_field :commit_id
5 = f.hidden_field :line_code 4 = f.hidden_field :line_code
app/views/projects/notes/_note.html.haml
@@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
34 = markdown(note.note) 34 = markdown(note.note)
35 35
36 .note-edit-form 36 .note-edit-form
37 - = form_for note, url: project_note_path(@project, note), method: :put, remote: true do |f| 37 + = form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f|
38 = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on' 38 = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on'
39 39
40 .form-actions 40 .form-actions
app/views/projects/notes/_notes.html.haml
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 - if note_for_main_target?(note) 4 - if note_for_main_target?(note)
5 = render discussion_notes 5 = render discussion_notes
6 - else 6 - else
7 - = render 'discussion', discussion_notes: discussion_notes 7 + = render 'projects/notes/discussion', discussion_notes: discussion_notes
8 - else 8 - else
9 - @notes.each do |note| 9 - @notes.each do |note|
10 - next unless note.author 10 - next unless note.author
app/views/projects/notes/_notes_with_form.html.haml
1 -%ul#notes-list.notes 1 +%ul#notes-list.notes.main-notes-list
  2 + = render "projects/notes/notes"
2 .js-notes-busy 3 .js-notes-busy
3 4
4 .js-main-target-form 5 .js-main-target-form
@@ -6,4 +7,4 @@ @@ -6,4 +7,4 @@
6 = render "projects/notes/form" 7 = render "projects/notes/form"
7 8
8 :javascript 9 :javascript
9 - NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}"); 10 + new Notes("#{project_notes_path(target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json})
app/views/projects/notes/create.js.haml
@@ -1,18 +0,0 @@ @@ -1,18 +0,0 @@
1 -- if @note.valid?  
2 - var noteHtml = "#{escape_javascript(render @note)}";  
3 -  
4 - - if note_for_main_target?(@note)  
5 - NoteList.appendNewNote(#{@note.id}, noteHtml);  
6 - - else  
7 - :plain  
8 - var firstDiscussionNoteHtml = "#{escape_javascript(render "projects/notes/diff_notes_with_reply", notes: [@note])}";  
9 - NoteList.appendNewDiscussionNote("#{@note.discussion_id}",  
10 - firstDiscussionNoteHtml,  
11 - noteHtml);  
12 -  
13 -- else  
14 - var errorsHtml = "#{escape_javascript(render 'projects/notes/form_errors', note: @note)}";  
15 - - if note_for_main_target?(@note)  
16 - NoteList.errorsOnForm(errorsHtml);  
17 - - else  
18 - NoteList.errorsOnForm(errorsHtml, "#{@note.discussion_id}");  
features/steps/project/project_merge_requests.rb
@@ -115,19 +115,26 @@ class ProjectMergeRequests &lt; Spinach::FeatureSteps @@ -115,19 +115,26 @@ class ProjectMergeRequests &lt; Spinach::FeatureSteps
115 And 'I leave a comment on the diff page' do 115 And 'I leave a comment on the diff page' do
116 init_diff_note 116 init_diff_note
117 117
118 - within('.js-temp-notes-holder') do 118 + within('.js-discussion-note-form') do
119 fill_in "note_note", with: "One comment to rule them all" 119 fill_in "note_note", with: "One comment to rule them all"
120 click_button "Add Comment" 120 click_button "Add Comment"
121 end 121 end
  122 +
  123 + within ".note-text" do
  124 + page.should have_content "One comment to rule them all"
  125 + end
122 end 126 end
123 127
124 And 'I leave a comment like "Line is wrong" on line 185 of the first file' do 128 And 'I leave a comment like "Line is wrong" on line 185 of the first file' do
125 init_diff_note 129 init_diff_note
126 130
127 - within(".js-temp-notes-holder") do 131 + within(".js-discussion-note-form") do
128 fill_in "note_note", with: "Line is wrong" 132 fill_in "note_note", with: "Line is wrong"
129 click_button "Add Comment" 133 click_button "Add Comment"
130 - sleep 0.05 134 + end
  135 +
  136 + within ".note-text" do
  137 + page.should have_content "Line is wrong"
131 end 138 end
132 end 139 end
133 140
spec/features/notes_on_merge_requests_spec.rb
@@ -108,7 +108,7 @@ describe &quot;On a merge request&quot;, js: true do @@ -108,7 +108,7 @@ describe &quot;On a merge request&quot;, js: true do
108 108
109 within("#note_#{note.id}") do 109 within("#note_#{note.id}") do
110 should have_css(".note-last-update small") 110 should have_css(".note-last-update small")
111 - find(".note-last-update small").text.should match(/Edited just now/) 111 + find(".note-last-update small").text.should match(/Edited less than a minute ago/)
112 end 112 end
113 end 113 end
114 end 114 end