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   -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 @@
  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 257 .file,
258 258 .discussion {
259 259 .new_note {
260   - margin: 8px 5px 8px 0;
  260 + margin: 0;
  261 + border: none;
261 262 }
262 263 }
263 264 .new_note {
264 265 display: none;
265   -
266 266 .buttons {
267 267 float: left;
268 268 margin-top: 8px;
... ...
app/controllers/projects/commit_controller.rb
... ... @@ -24,8 +24,8 @@ class Projects::CommitController &lt; Projects::ApplicationController
24 24 @line_notes = result[:line_notes]
25 25 @branches = result[:branches]
26 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 30 @comments_allowed = @reply_allowed = true
31 31 @comments_target = { noteable_type: 'Commit',
... ...
app/controllers/projects/issues_controller.rb
... ... @@ -49,8 +49,8 @@ class Projects::IssuesController &lt; Projects::ApplicationController
49 49  
50 50 def show
51 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 55 respond_with(@issue)
56 56 end
... ...
app/controllers/projects/merge_requests_controller.rb
... ... @@ -198,6 +198,9 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController
198 198 def define_show_vars
199 199 # Build a note object for comment form
200 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 205 # Get commits from repository
203 206 # or from cache if already merged
... ... @@ -205,9 +208,6 @@ class Projects::MergeRequestsController &lt; Projects::ApplicationController
205 208  
206 209 @allowed_to_merge = allowed_to_merge?
207 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 211 end
212 212  
213 213 def allowed_to_merge?
... ...
app/controllers/projects/notes_controller.rb
... ... @@ -2,71 +2,54 @@ class Projects::NotesController &lt; Projects::ApplicationController
2 2 # Authorize
3 3 before_filter :authorize_read_note!
4 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 7 def index
9 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 17 end
  18 +
  19 + render json: notes_json
25 20 end
26 21  
27 22 def create
28 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 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 28 end
36 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 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 38 end
47 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 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 47 end
64 48 end
65 49  
66 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 54 respond_to do |format|
72 55 format.js { render nothing: true }
... ... @@ -77,35 +60,40 @@ class Projects::NotesController &lt; Projects::ApplicationController
77 60 render text: view_context.markdown(params[:note])
78 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 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 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 98 end
111 99 end
... ...
app/controllers/projects/snippets_controller.rb
... ... @@ -48,8 +48,8 @@ class Projects::SnippetsController &lt; Projects::ApplicationController
48 48  
49 49 def show
50 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 53 end
54 54  
55 55 def destroy
... ...
app/helpers/notes_helper.rb
1 1 module NotesHelper
2 2 # Helps to distinguish e.g. commit notes in mr notes list
3 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 5 end
6 6  
7 7 def note_target_fields
... ... @@ -21,14 +21,6 @@ module NotesHelper
21 21 end
22 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 24 def note_timestamp(note)
33 25 # Shows the created at time and the updated at time if different
34 26 ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')} ago"
... ... @@ -41,4 +33,13 @@ module NotesHelper
41 33 end
42 34 ts.html_safe
43 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 45 end
... ...
app/models/note.rb
... ... @@ -56,29 +56,52 @@ class Note &lt; ActiveRecord::Base
56 56 serialize :st_diff
57 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 105 end
83 106  
84 107 # Determine whether or not a cross-reference note already exists.
... ... @@ -89,7 +112,7 @@ class Note &lt; ActiveRecord::Base
89 112 def commit_author
90 113 @commit_author ||=
91 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 116 rescue
94 117 nil
95 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 2 = note_target_fields
4 3 = f.hidden_field :commit_id
5 4 = f.hidden_field :line_code
... ...
app/views/projects/notes/_note.html.haml
... ... @@ -34,7 +34,7 @@
34 34 = markdown(note.note)
35 35  
36 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 38 = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on'
39 39  
40 40 .form-actions
... ...
app/views/projects/notes/_notes.html.haml
... ... @@ -4,7 +4,7 @@
4 4 - if note_for_main_target?(note)
5 5 = render discussion_notes
6 6 - else
7   - = render 'discussion', discussion_notes: discussion_notes
  7 + = render 'projects/notes/discussion', discussion_notes: discussion_notes
8 8 - else
9 9 - @notes.each do |note|
10 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 3 .js-notes-busy
3 4  
4 5 .js-main-target-form
... ... @@ -6,4 +7,4 @@
6 7 = render "projects/notes/form"
7 8  
8 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   -- 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 115 And 'I leave a comment on the diff page' do
116 116 init_diff_note
117 117  
118   - within('.js-temp-notes-holder') do
  118 + within('.js-discussion-note-form') do
119 119 fill_in "note_note", with: "One comment to rule them all"
120 120 click_button "Add Comment"
121 121 end
  122 +
  123 + within ".note-text" do
  124 + page.should have_content "One comment to rule them all"
  125 + end
122 126 end
123 127  
124 128 And 'I leave a comment like "Line is wrong" on line 185 of the first file' do
125 129 init_diff_note
126 130  
127   - within(".js-temp-notes-holder") do
  131 + within(".js-discussion-note-form") do
128 132 fill_in "note_note", with: "Line is wrong"
129 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 138 end
132 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 108  
109 109 within("#note_#{note.id}") do
110 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 112 end
113 113 end
114 114 end
... ...