Commit 6ddacaf9bfe02378e610ce277cd901097dcf6fdd

Authored by Dmitriy Zaporozhets
2 parents d8e697ac ac983319

Merge branch 'riyad-discussions'

Showing 73 changed files with 2253 additions and 859 deletions   Show diff stats
app/assets/images/comment_add.png

781 Bytes

app/assets/images/diff_note_add.png 0 → 100644

691 Bytes

app/assets/javascripts/behaviors/details_behavior.coffee 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +$ ->
  2 + $("body").on "click", ".js-details-target", ->
  3 + container = $(@).closest(".js-details-container")
  4 +
  5 + container.toggleClass("open")
app/assets/javascripts/behaviors/toggler_behavior.coffee 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +$ ->
  2 + $("body").on "click", ".js-toggler-target", ->
  3 + container = $(@).closest(".js-toggler-container")
  4 +
  5 + container.toggleClass("on")
app/assets/javascripts/extensions/array.js 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +Array.prototype.first = function() {
  2 + return this[0];
  3 +}
  4 +
  5 +Array.prototype.last = function() {
  6 + return this[this.length-1];
  7 +}
0 \ No newline at end of file 8 \ No newline at end of file
app/assets/javascripts/notes.js
@@ -9,72 +9,315 @@ var NoteList = { @@ -9,72 +9,315 @@ var NoteList = {
9 loading_more_disabled: false, 9 loading_more_disabled: false,
10 reversed: false, 10 reversed: false,
11 11
12 - init:  
13 - function(tid, tt, path) {  
14 - this.notes_path = path + ".js";  
15 - this.target_id = tid;  
16 - this.target_type = tt;  
17 - this.reversed = $("#notes-list").is(".reversed");  
18 - this.target_params = "target_type=" + this.target_type + "&target_id=" + this.target_id;  
19 -  
20 - // get initial set of notes  
21 - this.getContent();  
22 -  
23 - $("#notes-list, #new-notes-list").on("ajax:success", ".delete-note", function() {  
24 - $(this).closest('li').fadeOut(function() {  
25 - $(this).remove();  
26 - NoteList.updateVotes();  
27 - }); 12 + init: function(tid, tt, path) {
  13 + NoteList.notes_path = path + ".js";
  14 + NoteList.target_id = tid;
  15 + NoteList.target_type = tt;
  16 + NoteList.reversed = $("#notes-list").is(".reversed");
  17 + NoteList.target_params = "target_type=" + NoteList.target_type + "&target_id=" + NoteList.target_id;
  18 +
  19 + NoteList.setupMainTargetNoteForm();
  20 +
  21 + if(NoteList.reversed) {
  22 + var form = $(".js-main-target-form");
  23 + form.find(".buttons, .note_options").hide();
  24 + var textarea = form.find(".js-note-text");
  25 + textarea.css("height", "40px");
  26 + textarea.on("focus", function(){
  27 + textarea.css("height", "80px");
  28 + form.find(".buttons, .note_options").show();
28 }); 29 });
  30 + }
29 31
30 - $(".note-form-holder").on("ajax:before", function(){  
31 - $(".submit_note").disable();  
32 - }) 32 + // get initial set of notes
  33 + NoteList.getContent();
33 34
34 - $(".note-form-holder").on("ajax:complete", function(){  
35 - $(".submit_note").enable();  
36 - $('#preview-note').hide();  
37 - $('#note_note').show();  
38 - }) 35 + // add a new diff note
  36 + $(document).on("click",
  37 + ".js-add-diff-note-button",
  38 + NoteList.addDiffNote);
39 39
40 - disableButtonIfEmptyField(".note-text", ".submit_note"); 40 + // reply to diff/discussion notes
  41 + $(document).on("click",
  42 + ".js-discussion-reply-button",
  43 + NoteList.replyToDiscussionNote);
41 44
42 - $("#note_attachment").change(function(e){  
43 - var val = $('.input-file').val();  
44 - var filename = val.replace(/^.*[\\\/]/, '');  
45 - $(".file_name").text(filename);  
46 - }); 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 + // reset main target form after submit
  66 + $(document).on("ajax:complete",
  67 + ".js-main-target-form",
  68 + NoteList.resetMainTargetForm);
  69 +
  70 +
  71 + $(document).on("click",
  72 + ".js-choose-note-attachment-button",
  73 + NoteList.chooseNoteAttachment);
  74 +
  75 + $(document).on("click",
  76 + ".js-show-outdated-discussion",
  77 + function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() });
  78 + },
  79 +
  80 +
  81 + /**
  82 + * When clicking on buttons
  83 + */
  84 +
  85 + /**
  86 + * Called when clicking on the "add a comment" button on the side of a diff line.
  87 + *
  88 + * Inserts a temporary row for the form below the line.
  89 + * Sets up the form and shows it.
  90 + */
  91 + addDiffNote: function(e) {
  92 + e.preventDefault();
  93 +
  94 + // find the form
  95 + var form = $(".js-new-note-form");
  96 + var row = $(this).closest("tr");
  97 + var nextRow = row.next();
  98 +
  99 + // does it already have notes?
  100 + if (nextRow.is(".notes_holder")) {
  101 + $.proxy(NoteList.replyToDiscussionNote,
  102 + nextRow.find(".js-discussion-reply-button")
  103 + ).call();
  104 + } else {
  105 + // add a notes row and insert the form
  106 + row.after('<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"></td></tr>');
  107 + form.clone().appendTo(row.next().find(".notes_content"));
  108 +
  109 + // show the form
  110 + NoteList.setupDiscussionNoteForm($(this), row.next().find("form"));
  111 + }
  112 + },
  113 +
  114 + /**
  115 + * Called when clicking the "Choose File" button.
  116 + *
  117 + * Opesn the file selection dialog.
  118 + */
  119 + chooseNoteAttachment: function() {
  120 + var form = $(this).closest("form");
47 121
48 - if(this.reversed) {  
49 - var textarea = $(".note-text");  
50 - $('.note_advanced_opts').hide();  
51 - textarea.css("height", "40px");  
52 - textarea.on("focus", function(){  
53 - $(this).css("height", "80px");  
54 - $('.note_advanced_opts').show(); 122 + form.find(".js-note-attachment-input").click();
  123 + },
  124 +
  125 + /**
  126 + * Shows the note preview.
  127 + *
  128 + * Lets the server render GFM into Html and displays it.
  129 + *
  130 + * Note: uses the Toggler behavior to toggle preview/edit views/buttons
  131 + */
  132 + previewNote: function(e) {
  133 + e.preventDefault();
  134 +
  135 + var form = $(this).closest("form");
  136 + var preview = form.find('.js-note-preview');
  137 + var noteText = form.find('.js-note-text').val();
  138 +
  139 + if(noteText.trim().length === 0) {
  140 + preview.text('Nothing to preview.');
  141 + } else {
  142 + preview.text('Loading...');
  143 + $.post($(this).data('url'), {note: noteText})
  144 + .success(function(previewData) {
  145 + preview.html(previewData);
55 }); 146 });
56 - } 147 + }
  148 + },
  149 +
  150 + /**
  151 + * Called in response to "cancel" on a diff note form.
  152 + *
  153 + * Shows the reply button again.
  154 + * Removes the form and if necessary it's temporary row.
  155 + */
  156 + removeDiscussionNoteForm: function() {
  157 + var form = $(this).closest("form");
  158 + var row = form.closest("tr");
  159 +
  160 + // show the reply button (will only work for replys)
  161 + form.prev(".js-discussion-reply-button").show();
  162 +
  163 + if (row.is(".js-temp-notes-holder")) {
  164 + // remove temporary row for diff lines
  165 + row.remove();
  166 + } else {
  167 + // only remove the form
  168 + form.remove();
  169 + }
  170 + },
57 171
58 - // Setup note preview  
59 - $(document).on('click', '#preview-link', function(e) {  
60 - $('#preview-note').text('Loading...'); 172 + /**
  173 + * Called in response to deleting a note of any kind.
  174 + *
  175 + * Removes the actual note from view.
  176 + * Removes the whole discussion if the last note is being removed.
  177 + */
  178 + removeNote: function() {
  179 + var note = $(this).closest(".note");
  180 + var notes = note.closest(".notes");
61 181
62 - $(this).text($(this).text() === "Edit" ? "Preview" : "Edit"); 182 + // check if this is the last note for this line
  183 + if (notes.find(".note").length === 1) {
  184 + // for discussions
  185 + notes.closest(".discussion").remove();
63 186
64 - var note_text = $('#note_note').val(); 187 + // for diff lines
  188 + notes.closest("tr").remove();
  189 + }
65 190
66 - if(note_text.trim().length === 0) {  
67 - $('#preview-note').text('Nothing to preview.');  
68 - } else {  
69 - $.post($(this).attr('href'), {note: note_text}).success(function(data) {  
70 - $('#preview-note').html(data);  
71 - });  
72 - } 191 + note.remove();
  192 + NoteList.updateVotes();
  193 + },
73 194
74 - $('#preview-note, #note_note').toggle();  
75 - e.preventDefault();  
76 - });  
77 - }, 195 + /**
  196 + * Called when clicking on the "reply" button for a diff line.
  197 + *
  198 + * Shows the note form below the notes.
  199 + */
  200 + replyToDiscussionNote: function() {
  201 + // find the form
  202 + var form = $(".js-new-note-form");
  203 +
  204 + // hide reply button
  205 + $(this).hide();
  206 + // insert the form after the button
  207 + form.clone().insertAfter($(this));
  208 +
  209 + // show the form
  210 + NoteList.setupDiscussionNoteForm($(this), $(this).next("form"));
  211 + },
  212 +
  213 +
  214 + /**
  215 + * Helper for inserting and setting up note forms.
  216 + */
  217 +
  218 +
  219 + /**
  220 + * Called in response to creating a note failing validation.
  221 + *
  222 + * Adds the rendered errors to the respective form.
  223 + * If "discussionId" is null or undefined, the main target form is assumed.
  224 + */
  225 + errorsOnForm: function(errorsHtml, discussionId) {
  226 + // find the form
  227 + if (discussionId) {
  228 + var form = $("form[rel='"+discussionId+"']");
  229 + } else {
  230 + var form = $(".js-main-target-form");
  231 + }
  232 +
  233 + form.find(".js-errors").remove();
  234 + form.prepend(errorsHtml);
  235 +
  236 + form.find(".js-note-text").focus();
  237 + },
  238 +
  239 +
  240 + /**
  241 + * Shows the diff/discussion form and does some setup on it.
  242 + *
  243 + * Sets some hidden fields in the form.
  244 + *
  245 + * Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
  246 + * and "noteableId" data attributes set.
  247 + */
  248 + setupDiscussionNoteForm: function(dataHolder, form) {
  249 + // setup note target
  250 + form.attr("rel", dataHolder.data("discussionId"));
  251 + form.find("#note_commit_id").val(dataHolder.data("commitId"));
  252 + form.find("#note_line_code").val(dataHolder.data("lineCode"));
  253 + form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
  254 + form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
  255 +
  256 + NoteList.setupNoteForm(form);
  257 +
  258 + form.find(".js-note-text").focus();
  259 + },
  260 +
  261 + /**
  262 + * Shows the main form and does some setup on it.
  263 + *
  264 + * Sets some hidden fields in the form.
  265 + */
  266 + setupMainTargetNoteForm: function() {
  267 + // find the form
  268 + var form = $(".js-new-note-form");
  269 + // insert the form after the button
  270 + form.clone().replaceAll($(".js-main-target-form"));
  271 +
  272 + form = form.prev("form");
  273 +
  274 + // show the form
  275 + NoteList.setupNoteForm(form);
  276 +
  277 + // fix classes
  278 + form.removeClass("js-new-note-form");
  279 + form.addClass("js-main-target-form");
  280 +
  281 + // remove unnecessary fields and buttons
  282 + form.find("#note_line_code").remove();
  283 + form.find(".js-close-discussion-note-form").remove();
  284 + },
  285 +
  286 + /**
  287 + * General note form setup.
  288 + *
  289 + * * deactivates the submit button when text is empty
  290 + * * hides the preview button when text is empty
  291 + * * setup GFM auto complete
  292 + * * show the form
  293 + */
  294 + setupNoteForm: function(form) {
  295 + disableButtonIfEmptyField(form.find(".js-note-text"), form.find(".js-comment-button"));
  296 +
  297 + form.removeClass("js-new-note-form");
  298 +
  299 + // setup preview buttons
  300 + form.find(".js-note-edit-button, .js-note-preview-button")
  301 + .tooltip({ placement: 'left' });
  302 +
  303 + previewButton = form.find(".js-note-preview-button");
  304 + form.find(".js-note-text").on("input", function() {
  305 + if ($(this).val().trim() !== "") {
  306 + previewButton.removeClass("turn-off").addClass("turn-on");
  307 + } else {
  308 + previewButton.removeClass("turn-on").addClass("turn-off");
  309 + }
  310 + });
  311 +
  312 + // remove notify commit author checkbox for non-commit notes
  313 + if (form.find("#note_noteable_type").val() !== "Commit") {
  314 + form.find(".js-notify-commit-author").remove();
  315 + }
  316 +
  317 + GitLab.GfmAutoComplete.setup();
  318 +
  319 + form.show();
  320 + },
78 321
79 322
80 /** 323 /**
@@ -86,40 +329,39 @@ var NoteList = { @@ -86,40 +329,39 @@ var NoteList = {
86 /** 329 /**
87 * Gets an inital set of notes. 330 * Gets an inital set of notes.
88 */ 331 */
89 - getContent:  
90 - function() {  
91 - $.ajax({  
92 - type: "GET",  
93 - url: this.notes_path,  
94 - data: this.target_params,  
95 - complete: function(){ $('.notes-status').removeClass("loading")},  
96 - beforeSend: function() { $('.notes-status').addClass("loading") },  
97 - dataType: "script"});  
98 - }, 332 + getContent: function() {
  333 + $.ajax({
  334 + url: NoteList.notes_path,
  335 + data: NoteList.target_params,
  336 + complete: function(){ $('.js-notes-busy').removeClass("loading")},
  337 + beforeSend: function() { $('.js-notes-busy').addClass("loading") },
  338 + dataType: "script"
  339 + });
  340 + },
99 341
100 /** 342 /**
101 * Called in response to getContent(). 343 * Called in response to getContent().
102 * Replaces the content of #notes-list with the given html. 344 * Replaces the content of #notes-list with the given html.
103 */ 345 */
104 - setContent:  
105 - function(first_id, last_id, html) {  
106 - this.top_id = first_id;  
107 - this.bottom_id = last_id;  
108 - $("#notes-list").html(html); 346 + setContent: function(newNoteIds, html) {
  347 + NoteList.top_id = newNoteIds.first();
  348 + NoteList.bottom_id = newNoteIds.last();
  349 + $("#notes-list").html(html);
109 350
  351 + // for the wall
  352 + if (NoteList.reversed) {
110 // init infinite scrolling 353 // init infinite scrolling
111 - this.initLoadMore(); 354 + NoteList.initLoadMore();
112 355
113 // init getting new notes 356 // init getting new notes
114 - if (this.reversed) {  
115 - this.initRefreshNew();  
116 - }  
117 - }, 357 + NoteList.initRefreshNew();
  358 + }
  359 + },
118 360
119 361
120 /** 362 /**
121 * Handle loading more notes when scrolling to the bottom of the page. 363 * Handle loading more notes when scrolling to the bottom of the page.
122 - * The id of the last note in the list is in this.bottom_id. 364 + * The id of the last note in the list is in NoteList.bottom_id.
123 * 365 *
124 * Set up refreshing only new notes after all notes have been loaded. 366 * Set up refreshing only new notes after all notes have been loaded.
125 */ 367 */
@@ -128,65 +370,57 @@ var NoteList = { @@ -128,65 +370,57 @@ var NoteList = {
128 /** 370 /**
129 * Initializes loading more notes when scrolling to the bottom of the page. 371 * Initializes loading more notes when scrolling to the bottom of the page.
130 */ 372 */
131 - initLoadMore:  
132 - function() {  
133 - $(document).endlessScroll({  
134 - bottomPixels: 400,  
135 - fireDelay: 1000,  
136 - fireOnce:true,  
137 - ceaseFire: function() {  
138 - return NoteList.loading_more_disabled;  
139 - },  
140 - callback: function(i) {  
141 - NoteList.getMore();  
142 - }  
143 - });  
144 - }, 373 + initLoadMore: function() {
  374 + $(document).endlessScroll({
  375 + bottomPixels: 400,
  376 + fireDelay: 1000,
  377 + fireOnce:true,
  378 + ceaseFire: function() {
  379 + return NoteList.loading_more_disabled;
  380 + },
  381 + callback: function(i) {
  382 + NoteList.getMore();
  383 + }
  384 + });
  385 + },
145 386
146 /** 387 /**
147 * Gets an additional set of notes. 388 * Gets an additional set of notes.
148 */ 389 */
149 - getMore:  
150 - function() {  
151 - // only load more notes if there are no "new" notes  
152 - $('.loading').show();  
153 - $.ajax({  
154 - type: "GET",  
155 - url: this.notes_path,  
156 - data: this.target_params + "&loading_more=1&" + (this.reversed ? "before_id" : "after_id") + "=" + this.bottom_id,  
157 - complete: function(){ $('.notes-status').removeClass("loading")},  
158 - beforeSend: function() { $('.notes-status').addClass("loading") },  
159 - dataType: "script"});  
160 - }, 390 + getMore: function() {
  391 + // only load more notes if there are no "new" notes
  392 + $('.loading').show();
  393 + $.ajax({
  394 + url: NoteList.notes_path,
  395 + data: NoteList.target_params + "&loading_more=1&" + (NoteList.reversed ? "before_id" : "after_id") + "=" + NoteList.bottom_id,
  396 + complete: function(){ $('.js-notes-busy').removeClass("loading")},
  397 + beforeSend: function() { $('.js-notes-busy').addClass("loading") },
  398 + dataType: "script"
  399 + });
  400 + },
161 401
162 /** 402 /**
163 * Called in response to getMore(). 403 * Called in response to getMore().
164 * Append notes to #notes-list. 404 * Append notes to #notes-list.
165 */ 405 */
166 - appendMoreNotes:  
167 - function(id, html) {  
168 - if(id != this.bottom_id) {  
169 - this.bottom_id = id;  
170 - $("#notes-list").append(html);  
171 - }  
172 - }, 406 + appendMoreNotes: function(newNoteIds, html) {
  407 + var lastNewNoteId = newNoteIds.last();
  408 + if(lastNewNoteId != NoteList.bottom_id) {
  409 + NoteList.bottom_id = lastNewNoteId;
  410 + $("#notes-list").append(html);
  411 + }
  412 + },
173 413
174 /** 414 /**
175 * Called in response to getMore(). 415 * Called in response to getMore().
176 * Disables loading more notes when scrolling to the bottom of the page. 416 * Disables loading more notes when scrolling to the bottom of the page.
177 - * Initalizes refreshing new notes.  
178 */ 417 */
179 - finishedLoadingMore:  
180 - function() {  
181 - this.loading_more_disabled = true; 418 + finishedLoadingMore: function() {
  419 + NoteList.loading_more_disabled = true;
182 420
183 - // from now on only get new notes  
184 - if (!this.reversed) {  
185 - this.initRefreshNew();  
186 - }  
187 - // make sure we are up to date  
188 - this.updateVotes();  
189 - }, 421 + // make sure we are up to date
  422 + NoteList.updateVotes();
  423 + },
190 424
191 425
192 /** 426 /**
@@ -194,52 +428,118 @@ var NoteList = { @@ -194,52 +428,118 @@ var NoteList = {
194 * 428 *
195 * New notes are all notes that are created after the site has been loaded. 429 * New notes are all notes that are created after the site has been loaded.
196 * The "old" notes are in #notes-list the "new" ones will be in #new-notes-list. 430 * The "old" notes are in #notes-list the "new" ones will be in #new-notes-list.
197 - * The id of the last "old" note is in this.bottom_id. 431 + * The id of the last "old" note is in NoteList.bottom_id.
198 */ 432 */
199 433
200 434
201 /** 435 /**
202 * Initializes getting new notes every n seconds. 436 * Initializes getting new notes every n seconds.
  437 + *
  438 + * Note: only used on wall.
203 */ 439 */
204 - initRefreshNew:  
205 - function() {  
206 - setInterval("NoteList.getNew()", 10000);  
207 - }, 440 + initRefreshNew: function() {
  441 + setInterval("NoteList.getNew()", 10000);
  442 + },
208 443
209 /** 444 /**
210 * Gets the new set of notes. 445 * Gets the new set of notes.
  446 + *
  447 + * Note: only used on wall.
211 */ 448 */
212 - getNew:  
213 - function() {  
214 - $.ajax({  
215 - type: "GET",  
216 - url: this.notes_path,  
217 - data: this.target_params + "&loading_new=1&after_id=" + (this.reversed ? this.top_id : this.bottom_id),  
218 - dataType: "script"});  
219 - }, 449 + getNew: function() {
  450 + $.ajax({
  451 + url: NoteList.notes_path,
  452 + data: NoteList.target_params + "&loading_new=1&after_id=" + (NoteList.reversed ? NoteList.top_id : NoteList.bottom_id),
  453 + dataType: "script"
  454 + });
  455 + },
220 456
221 /** 457 /**
222 * Called in response to getNew(). 458 * Called in response to getNew().
223 * Replaces the content of #new-notes-list with the given html. 459 * Replaces the content of #new-notes-list with the given html.
  460 + *
  461 + * Note: only used on wall.
224 */ 462 */
225 - replaceNewNotes:  
226 - function(html) {  
227 - $("#new-notes-list").html(html);  
228 - this.updateVotes();  
229 - }, 463 + replaceNewNotes: function(newNoteIds, html) {
  464 + $("#new-notes-list").html(html);
  465 + NoteList.updateVotes();
  466 + },
230 467
231 /** 468 /**
232 - * Adds a single note to #new-notes-list. 469 + * Adds a single common note to #notes-list.
233 */ 470 */
234 - appendNewNote:  
235 - function(id, html) {  
236 - if (this.reversed) {  
237 - $("#new-notes-list").prepend(html);  
238 - } else {  
239 - $("#new-notes-list").append(html);  
240 - }  
241 - this.updateVotes();  
242 - }, 471 + appendNewNote: function(id, html) {
  472 + $("#notes-list").append(html);
  473 + NoteList.updateVotes();
  474 + },
  475 +
  476 + /**
  477 + * Adds a single discussion note to #notes-list.
  478 + *
  479 + * Also removes the corresponding form.
  480 + */
  481 + appendNewDiscussionNote: function(discussionId, diffRowHtml, noteHtml) {
  482 + var form = $("form[rel='"+discussionId+"']");
  483 + var row = form.closest("tr");
  484 +
  485 + // is this the first note of discussion?
  486 + if (row.is(".js-temp-notes-holder")) {
  487 + // insert the note and the reply button after the temp row
  488 + row.after(diffRowHtml);
  489 + // remove the note (will be added again below)
  490 + row.next().find(".note").remove();
  491 + }
  492 +
  493 + // append new note to all matching discussions
  494 + $(".notes[rel='"+discussionId+"']").append(noteHtml);
  495 +
  496 + // cleanup after successfully creating a diff/discussion note
  497 + $.proxy(NoteList.removeDiscussionNoteForm, form).call();
  498 + },
  499 +
  500 + /**
  501 + * Adds a single wall note to #new-notes-list.
  502 + *
  503 + * Note: only used on wall.
  504 + */
  505 + appendNewWallNote: function(id, html) {
  506 + $("#new-notes-list").prepend(html);
  507 + },
  508 +
  509 + /**
  510 + * Called in response the main target form has been successfully submitted.
  511 + *
  512 + * Removes any errors.
  513 + * Resets text and preview.
  514 + * Resets buttons.
  515 + */
  516 + resetMainTargetForm: function(){
  517 + var form = $(this);
  518 +
  519 + // remove validation errors
  520 + form.find(".js-errors").remove();
  521 +
  522 + // reset text and preview
  523 + var previewContainer = form.find(".js-toggler-container.note_text_and_preview");
  524 + if (previewContainer.is(".on")) {
  525 + previewContainer.removeClass("on");
  526 + }
  527 + form.find(".js-note-text").val("").trigger("input");
  528 + },
  529 +
  530 + /**
  531 + * Called after an attachment file has been selected.
  532 + *
  533 + * Updates the file name for the selected attachment.
  534 + */
  535 + updateFormAttachment: function() {
  536 + var form = $(this).closest("form");
  537 +
  538 + // get only the basename
  539 + var filename = $(this).val().replace(/^.*[\\\/]/, '');
  540 +
  541 + form.find(".js-attachment-filename").text(filename);
  542 + },
243 543
244 /** 544 /**
245 * Recalculates the votes and updates them (if they are displayed at all). 545 * Recalculates the votes and updates them (if they are displayed at all).
@@ -249,67 +549,24 @@ var NoteList = { @@ -249,67 +549,24 @@ var NoteList = {
249 * Might produce inaccurate results when not all notes have been loaded and a 549 * Might produce inaccurate results when not all notes have been loaded and a
250 * recalculation is triggered (e.g. when deleting a note). 550 * recalculation is triggered (e.g. when deleting a note).
251 */ 551 */
252 - updateVotes:  
253 - function() {  
254 - var votes = $("#votes .votes");  
255 - var notes = $("#notes-list, #new-notes-list").find(".note .vote");  
256 -  
257 - // only update if there is a vote display  
258 - if (votes.size()) {  
259 - var upvotes = notes.filter(".upvote").size();  
260 - var downvotes = notes.filter(".downvote").size();  
261 - var votesCount = upvotes + downvotes;  
262 - var upvotesPercent = votesCount ? (100.0 / votesCount * upvotes) : 0;  
263 - var downvotesPercent = votesCount ? (100.0 - upvotesPercent) : 0;  
264 -  
265 - // change vote bar lengths  
266 - votes.find(".bar-success").css("width", upvotesPercent+"%");  
267 - votes.find(".bar-danger").css("width", downvotesPercent+"%");  
268 - // replace vote numbers  
269 - votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes));  
270 - votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));  
271 - } 552 + updateVotes: function() {
  553 + var votes = $("#votes .votes");
  554 + var notes = $("#notes-list .note .vote");
  555 +
  556 + // only update if there is a vote display
  557 + if (votes.size()) {
  558 + var upvotes = notes.filter(".upvote").size();
  559 + var downvotes = notes.filter(".downvote").size();
  560 + var votesCount = upvotes + downvotes;
  561 + var upvotesPercent = votesCount ? (100.0 / votesCount * upvotes) : 0;
  562 + var downvotesPercent = votesCount ? (100.0 - upvotesPercent) : 0;
  563 +
  564 + // change vote bar lengths
  565 + votes.find(".bar-success").css("width", upvotesPercent+"%");
  566 + votes.find(".bar-danger").css("width", downvotesPercent+"%");
  567 + // replace vote numbers
  568 + votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes));
  569 + votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));
272 } 570 }
  571 + }
273 }; 572 };
274 -  
275 -var PerLineNotes = {  
276 - init:  
277 - function() {  
278 - /**  
279 - * Called when clicking on the "add note" or "reply" button for a diff line.  
280 - *  
281 - * Shows the note form below the line.  
282 - * Sets some hidden fields in the form.  
283 - */  
284 - $(".diff_file_content").on("click", ".line_note_link, .line_note_reply_link", function(e) {  
285 - var form = $(".per_line_form");  
286 - $(this).closest("tr").after(form);  
287 - form.find("#note_line_code").val($(this).data("lineCode"));  
288 - form.show();  
289 - e.preventDefault();  
290 - });  
291 -  
292 - disableButtonIfEmptyField(".line-note-text", ".submit_inline_note");  
293 -  
294 - /**  
295 - * Called in response to successfully deleting a note on a diff line.  
296 - *  
297 - * Removes the actual note from view.  
298 - * Removes the reply button if the last note for that line has been removed.  
299 - */  
300 - $(".diff_file_content").on("ajax:success", ".delete-note", function() {  
301 - var trNote = $(this).closest("tr");  
302 - trNote.fadeOut(function() {  
303 - $(this).remove();  
304 - });  
305 -  
306 - // check if this is the last note for this line  
307 - // elements must really be removed for this to work reliably  
308 - var trLine = trNote.prev();  
309 - var trRpl = trNote.next();  
310 - if (trLine.is(".line_holder") && trRpl.is(".reply")) {  
311 - trRpl.fadeOut(function() { $(this).remove(); });  
312 - }  
313 - });  
314 - }  
315 -}  
app/assets/stylesheets/application.scss
@@ -46,3 +46,8 @@ @@ -46,3 +46,8 @@
46 @import "themes/ui_gray.scss"; 46 @import "themes/ui_gray.scss";
47 @import "themes/ui_color.scss"; 47 @import "themes/ui_color.scss";
48 48
  49 +/**
  50 + * Styles for JS behaviors.
  51 + */
  52 +@import "behaviors.scss";
  53 +
app/assets/stylesheets/behaviors.scss 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +// Details
  2 +//--------
  3 +.js-details-container .content { display: none; }
  4 +.js-details-container .content.hide { display: block; }
  5 +.js-details-container.open .content { display: block; }
  6 +.js-details-container.open .content.hide { display: none; }
  7 +
  8 +
  9 +// Toggler
  10 +//--------
  11 +.js-toggler-container .turn-on { display: inherit; }
  12 +.js-toggler-container .turn-off { display: none; }
  13 +.js-toggler-container.on .turn-on { display: none; }
  14 +.js-toggler-container.on .turn-off { display: inherit; }
app/assets/stylesheets/common.scss
@@ -546,3 +546,9 @@ h1.http_status_code { @@ -546,3 +546,9 @@ h1.http_status_code {
546 } 546 }
547 } 547 }
548 } 548 }
  549 +
  550 +img.emoji {
  551 + height: 20px;
  552 + vertical-align: middle;
  553 + width: 20px;
  554 +}
app/assets/stylesheets/sections/commits.scss
@@ -119,12 +119,14 @@ @@ -119,12 +119,14 @@
119 } 119 }
120 } 120 }
121 } 121 }
122 - .old_line, .new_line {  
123 - margin: 0px;  
124 - padding: 0px;  
125 - border: none;  
126 - background: #EEE;  
127 - color: #666; 122 + .new_line,
  123 + .old_line,
  124 + .notes_line {
  125 + margin:0px;
  126 + padding:0px;
  127 + border:none;
  128 + background:#EEE;
  129 + color:#666;
128 padding: 0px 5px; 130 padding: 0px 5px;
129 border-right: 1px solid #ccc; 131 border-right: 1px solid #ccc;
130 text-align: right; 132 text-align: right;
@@ -134,6 +136,7 @@ @@ -134,6 +136,7 @@
134 moz-user-select: none; 136 moz-user-select: none;
135 -khtml-user-select: none; 137 -khtml-user-select: none;
136 user-select: none; 138 user-select: none;
  139 +
137 a { 140 a {
138 float: left; 141 float: left;
139 width: 35px; 142 width: 35px;
app/assets/stylesheets/sections/notes.scss
1 /** 1 /**
2 * Notes 2 * Notes
3 - *  
4 */ 3 */
5 -#notes-list,  
6 -#new-notes-list { 4 +ul.notes {
7 display: block; 5 display: block;
8 list-style: none; 6 list-style: none;
9 margin: 0px; 7 margin: 0px;
10 padding: 0px; 8 padding: 0px;
11 -}  
12 9
13 -.issue_notes,  
14 -.wiki_notes {  
15 - .note_content {  
16 - float: left;  
17 - width: 400px;  
18 - }  
19 -} 10 + .discussion-header,
  11 + .note-header {
  12 + @extend .cgray;
  13 + padding-top: 5px;
  14 + padding-bottom: 15px;
20 15
21 -/* Note textare */  
22 -#note_note {  
23 - height: 80px;  
24 - width: 98%;  
25 - font-size: 14px;  
26 -} 16 + .avatar {
  17 + float: left;
  18 + margin-right: 10px;
  19 + }
27 20
28 -#new_note {  
29 - .attach_holder {  
30 - display: none; 21 + .discussion-last-update,
  22 + .note-last-update {
  23 + font-style: italic;
  24 + }
  25 + .note-author {
  26 + color: $style_color;
  27 + font-weight: bold;
  28 + &:hover {
  29 + color: $primary_color;
  30 + }
  31 + }
31 } 32 }
32 -}  
33 33
34 -.preview_note {  
35 - margin: 2px;  
36 - border: 1px solid #ddd;  
37 - padding: 10px;  
38 - min-height: 60px;  
39 - background: #f5f5f5;  
40 -} 34 + .discussion {
  35 + padding: 8px 0;
  36 + overflow: hidden;
  37 + display: block;
  38 + position:relative;
41 39
42 -.note {  
43 - padding: 8px 0;  
44 - overflow: hidden;  
45 - display: block;  
46 - position: relative;  
47 - img {float: left; margin-right: 10px;}  
48 - img.emoji {float: none;margin: 0;}  
49 - .note-author cite{font-style: italic;}  
50 - p { color: $style_color; }  
51 - .note-author { color: $style_color;} 40 + .discussion-body {
  41 + margin-left: 50px;
52 42
53 - .note-title { margin-left: 45px; padding-top: 5px;}  
54 - .avatar {  
55 - margin-top: 3px;  
56 - } 43 + .diff_file,
  44 + .discussion-hidden,
  45 + .notes {
  46 + @extend .borders;
  47 + background-color: #F9F9F9;
  48 + }
  49 + .diff_file .notes {
  50 + /* reset */
  51 + background: inherit;
  52 + border: none;
  53 + @include box-shadow(none);
57 54
58 - .delete-note {  
59 - display: none;  
60 - position: absolute;  
61 - right: 0;  
62 - top: 0; 55 + }
  56 + .discussion-hidden .note {
  57 + @extend .cgray;
  58 + padding: 8px;
  59 + text-align: center;
  60 + }
  61 + .notes .note {
  62 + border-color: #ddd;
  63 + padding: 8px;
  64 + }
  65 + .reply-btn {
  66 + margin-top: 8px;
  67 + }
  68 + }
63 } 69 }
64 70
65 - &:hover {  
66 - .delete-note { display: block; }  
67 - }  
68 -}  
69 -#notes-list:not(.reversed) .note,  
70 -#new-notes-list:not(.reversed) .note {  
71 - border-bottom: 1px solid #eee;  
72 -}  
73 -#notes-list.reversed .note,  
74 -#new-notes-list.reversed .note {  
75 - border-top: 1px solid #eee;  
76 -}  
77 -  
78 -/* mark vote notes */  
79 -.voting_notes .note {  
80 - padding: 8px 0;  
81 -} 71 + .note {
  72 + padding: 8px 0;
  73 + overflow: hidden;
  74 + display: block;
  75 + position:relative;
  76 + p { color: $style_color; }
82 77
83 -.notes-status {  
84 - margin: 18px;  
85 -} 78 + .avatar {
  79 + margin-top: 3px;
  80 + }
  81 + .attachment {
  82 + font-size: 14px;
  83 + margin-top: -20px;
86 84
  85 + .icon-attachment {
  86 + @extend .icon-paper-clip;
  87 + font-size: 24px;
  88 + position: relative;
  89 + text-align: right;
  90 + top: 6px;
  91 + }
  92 + }
  93 + .note-body {
  94 + margin-left: 45px;
  95 + }
  96 + .note-header {
  97 + padding-bottom: 5px;
  98 + }
  99 + }
87 100
88 -p.notify_controls input{  
89 - margin: 5px; 101 + // paint top or bottom borders depending on notes direction
  102 + &:not(.reversed) .note,
  103 + &:not(.reversed) .discussion {
  104 + border-bottom: 1px solid #eee;
  105 + }
  106 + &.reversed .note,
  107 + &.reversed .discussion {
  108 + border-top: 1px solid #eee;
  109 + }
90 } 110 }
91 111
92 -p.notify_controls span{  
93 - font-weight: 700;  
94 -} 112 +.diff_file .notes_holder {
  113 + font-family: $sansFontFamily;
  114 + font-size: 13px;
  115 + line-height: 18px;
95 116
96 -tr.line_notes_row {  
97 - border-bottom: 1px solid #DDD;  
98 - border-left: 7px solid #2A79A3; 117 + td {
  118 + border: 1px solid #ddd;
  119 + border-left: none;
99 120
100 - &.reply {  
101 - background: #eee;  
102 - border-left: 7px solid #2A79A3;  
103 - border-top: 1px solid #ddd;  
104 - td {  
105 - padding: 7px 10px; 121 + &.notes_line {
  122 + text-align: center;
  123 + padding: 10px 0;
106 } 124 }
107 - a.line_note_reply_link {  
108 - border: 1px solid #eaeaea;  
109 - @include border-radius(4px);  
110 - padding: 3px 10px;  
111 - margin-left: 5px;  
112 - color: white;  
113 - background: #2A79A3;  
114 - border-color: #2A79A3; 125 + &.notes_content {
  126 + background-color: $white;
  127 + border-width: 1px 0;
  128 + padding-top: 0;
115 } 129 }
116 } 130 }
117 - ul {  
118 - margin: 0;  
119 - li {  
120 - padding: 0;  
121 - border: none;  
122 - } 131 +
  132 + .reply-btn {
  133 + margin-top: 8px;
123 } 134 }
124 } 135 }
125 136
126 -.line_notes_row, .per_line_form { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }  
127 137
128 -.per_line_form {  
129 - background: #f5f5f5;  
130 - border-top: 1px solid #eee;  
131 - form { margin: 0; }  
132 - td {  
133 - border-bottom: 1px solid #ddd; 138 +
  139 +/**
  140 + * Actions for Discussions/Notes
  141 + */
  142 +
  143 +.discussion,
  144 +.note {
  145 + &.note:hover {
  146 + .note-actions { display: block; }
  147 + }
  148 + .discussion-header:hover {
  149 + .discussion-actions { display: block; }
134 } 150 }
135 - .note_actions {  
136 - margin: 0;  
137 - padding-top: 10px;  
138 151
139 - .buttons {  
140 - float: left;  
141 - width: 300px;  
142 - }  
143 - .options {  
144 - .labels {  
145 - float: left;  
146 - padding-left: 10px;  
147 - label {  
148 - padding: 6px 0;  
149 - margin: 0;  
150 - width: 120px;  
151 - } 152 + .discussion-actions,
  153 + .note-actions {
  154 + display: none;
  155 + float: right;
  156 +
  157 + [class^="icon-"],
  158 + [class*="icon-"] {
  159 + font-size: 16px;
  160 + line-height: 16px;
  161 + vertical-align: middle;
  162 + }
  163 +
  164 + a {
  165 + @extend .cgray;
  166 +
  167 + &:hover {
  168 + color: $primary_color;
  169 + &.danger { @extend .cred; }
152 } 170 }
153 } 171 }
154 } 172 }
155 } 173 }
  174 +.diff_file .note .note-actions {
  175 + right: 0;
  176 + top: 0;
  177 +}
  178 +
  179 +
  180 +
  181 +/**
  182 + * Line note button on the side of diffs
  183 + */
156 184
157 -td .line_note_link {  
158 - position: absolute;  
159 - margin-left:-70px;  
160 - margin-top:-10px;  
161 - z-index: 10;  
162 - background: url("comment_add.png") no-repeat left 0;  
163 - width: 32px;  
164 - height: 32px; 185 +.diff_file tr.line_holder {
  186 + .add-diff-note {
  187 + background: url("diff_note_add.png") no-repeat left 0;
  188 + height: 22px;
  189 + margin-left: -65px;
  190 + position: absolute;
  191 + width: 22px;
  192 + z-index: 10;
165 193
166 - opacity: 0.0;  
167 - filter: alpha(opacity=0); 194 + // "hide" it by default
  195 + opacity: 0.0;
  196 + filter: alpha(opacity=0);
168 197
169 - &:hover {  
170 - opacity: 1.0;  
171 - filter: alpha(opacity=100); 198 + &:hover {
  199 + opacity: 1.0;
  200 + filter: alpha(opacity=100);
  201 + }
  202 + }
  203 +
  204 + // "show" the icon also if we just hover somwhere over the line
  205 + &:hover > td {
  206 + background: $hover !important;
  207 +
  208 + .add-diff-note {
  209 + opacity: 1.0;
  210 + filter: alpha(opacity=100);
  211 + }
172 } 212 }
173 } 213 }
174 214
175 -.diff_file_content tr.line_holder:hover > td { background: $hover !important; }  
176 -.diff_file_content tr.line_holder:hover > td .line_note_link {  
177 - opacity: 1.0;  
178 - filter: alpha(opacity=100); 215 +
  216 +
  217 +/**
  218 + * Note Form
  219 + */
  220 +
  221 +.comment-btn,
  222 +.reply-btn {
  223 + @extend .save-btn;
179 } 224 }
  225 +.diff_file,
  226 +.discussion {
  227 + .new_note {
  228 + margin: 8px 5px 8px 0;
180 229
181 -.new_note {  
182 - .input-file {  
183 - font: 500px monospace;  
184 - opacity: 0;  
185 - filter: alpha(opacity=0);  
186 - position: absolute;  
187 - z-index: 1;  
188 - top: 0;  
189 - right: 0;  
190 - padding: 0;  
191 - margin: 0; 230 + .note_options {
  231 + // because of the smaller width and the extra "cancel" button
  232 + margin-top: 8px;
  233 + }
192 } 234 }
  235 +}
  236 +.new_note {
  237 + display: none;
193 238
194 - .note_advanced_opts { 239 + .buttons {
  240 + float: left;
  241 + margin-top: 8px;
  242 + }
  243 + .clearfix {
  244 + margin-bottom: 0;
  245 + }
  246 + .note_options {
195 h6 { 247 h6 {
196 - line-height: 32px;  
197 - padding-right: 15px; 248 + @extend .left;
  249 + line-height: 20px;
  250 + padding-right: 16px;
  251 + padding-bottom: 16px;
  252 + }
  253 + label {
  254 + padding: 0;
198 } 255 }
199 - }  
200 256
201 - .attachments {  
202 - position: relative;  
203 - width: 350px;  
204 - height: 50px;  
205 - overflow: hidden;  
206 - margin:0 0 5px !important; 257 + .attachment {
  258 + @extend .right;
  259 + position: relative;
  260 + width: 350px;
  261 + height: 50px;
  262 + margin:0 0 5px !important;
207 263
208 - .input_file {  
209 - .file_upload {  
210 - position: absolute;  
211 - right: 14px;  
212 - top: 7px; 264 + // hide the actual file field
  265 + input {
  266 + display: none;
213 } 267 }
214 268
215 - .file_name {  
216 - line-height: 30px;  
217 - width: 240px;  
218 - height: 28px;  
219 - overflow: hidden;  
220 - }  
221 - .input-file {  
222 - width: 260px;  
223 - height: 41px; 269 + .choose-btn {
224 float: right; 270 float: right;
225 } 271 }
226 } 272 }
  273 + .notify_options {
  274 + @extend .right;
  275 + }
227 } 276 }
  277 + .note_text_and_preview {
  278 + // makes the "absolute" position for links relative to this
  279 + position: relative;
  280 +
  281 + // preview/edit buttons
  282 + > a {
  283 + font-size: 24px;
  284 + padding: 4px;
  285 + position: absolute;
  286 + right: 10px;
  287 + }
  288 + .note_preview {
  289 + background: #f5f5f5;
  290 + border: 1px solid #ddd;
  291 + @include border-radius(4px);
  292 + min-height: 80px;
  293 + padding: 4px 6px;
  294 + }
  295 + .note_text {
  296 + border: 1px solid #DDD;
  297 + box-shadow: none;
  298 + font-size: 14px;
  299 + height: 80px;
  300 + width: 98.6%;
  301 + }
  302 + }
  303 +}
  304 +
  305 +/* loading indicator */
  306 +.notes-busy {
  307 + margin: 18px;
228 } 308 }
229 309
230 -.note-text {  
231 - border: 1px solid #DDD;  
232 - box-shadow: none; 310 +.note-image-attach {
  311 + @extend .span4;
  312 + @extend .thumbnail;
  313 + margin-left: 45px;
233 } 314 }
app/contexts/notes/create_context.rb
@@ -3,8 +3,8 @@ module Notes @@ -3,8 +3,8 @@ module Notes
3 def execute 3 def execute
4 note = project.notes.new(params[:note]) 4 note = project.notes.new(params[:note])
5 note.author = current_user 5 note.author = current_user
6 - note.notify = true if params[:notify] == '1'  
7 - note.notify_author = true if params[:notify_author] == '1' 6 + note.notify = params[:notify].present?
  7 + note.notify_author = params[:notify_author].present?
8 note.save 8 note.save
9 note 9 note
10 end 10 end
app/contexts/notes/load_context.rb
@@ -9,11 +9,11 @@ module Notes @@ -9,11 +9,11 @@ module Notes
9 9
10 @notes = case target_type 10 @notes = case target_type
11 when "commit" 11 when "commit"
12 - project.notes.for_commit_id(target_id).not_inline.fresh.limit(20) 12 + project.notes.for_commit_id(target_id).not_inline.fresh
13 when "issue" 13 when "issue"
14 - project.issues.find(target_id).notes.inc_author.fresh.limit(20) 14 + project.issues.find(target_id).notes.inc_author.fresh
15 when "merge_request" 15 when "merge_request"
16 - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh.limit(20) 16 + project.merge_requests.find(target_id).mr_and_commit_notes.inc_author.fresh
17 when "snippet" 17 when "snippet"
18 project.snippets.find(target_id).notes.fresh 18 project.snippets.find(target_id).notes.fresh
19 when "wall" 19 when "wall"
app/controllers/commit_controller.rb
@@ -13,11 +13,17 @@ class CommitController &lt; ProjectResourceController @@ -13,11 +13,17 @@ class CommitController &lt; ProjectResourceController
13 @commit = result[:commit] 13 @commit = result[:commit]
14 git_not_found! unless @commit 14 git_not_found! unless @commit
15 15
16 - @suppress_diff = result[:suppress_diff]  
17 - @note = result[:note]  
18 - @line_notes = result[:line_notes]  
19 - @notes_count = result[:notes_count]  
20 - @comments_allowed = true 16 + @suppress_diff = result[:suppress_diff]
  17 +
  18 + @note = result[:note]
  19 + @line_notes = result[:line_notes]
  20 + @notes_count = result[:notes_count]
  21 + @target_type = :commit
  22 + @target_id = @commit.id
  23 +
  24 + @comments_allowed = @reply_allowed = true
  25 + @comments_target = { noteable_type: 'Commit',
  26 + commit_id: @commit.id }
21 27
22 respond_to do |format| 28 respond_to do |format|
23 format.html do 29 format.html do
app/controllers/issues_controller.rb
@@ -35,6 +35,8 @@ class IssuesController &lt; ProjectResourceController @@ -35,6 +35,8 @@ class IssuesController &lt; ProjectResourceController
35 35
36 def show 36 def show
37 @note = @project.notes.new(noteable: @issue) 37 @note = @project.notes.new(noteable: @issue)
  38 + @target_type = :issue
  39 + @target_id = @issue.id
38 40
39 respond_to do |format| 41 respond_to do |format|
40 format.html 42 format.html
app/controllers/merge_requests_controller.rb
@@ -18,6 +18,9 @@ class MergeRequestsController &lt; ProjectResourceController @@ -18,6 +18,9 @@ class MergeRequestsController &lt; ProjectResourceController
18 end 18 end
19 19
20 def show 20 def show
  21 + @target_type = :merge_request
  22 + @target_id = @merge_request.id
  23 +
21 respond_to do |format| 24 respond_to do |format|
22 format.html 25 format.html
23 format.js 26 format.js
@@ -31,7 +34,9 @@ class MergeRequestsController &lt; ProjectResourceController @@ -31,7 +34,9 @@ class MergeRequestsController &lt; ProjectResourceController
31 @diffs = @merge_request.diffs 34 @diffs = @merge_request.diffs
32 @commit = @merge_request.last_commit 35 @commit = @merge_request.last_commit
33 36
34 - @comments_allowed = true 37 + @comments_allowed = @reply_allowed = true
  38 + @comments_target = { noteable_type: 'MergeRequest',
  39 + noteable_id: @merge_request.id }
35 @line_notes = @merge_request.notes.where("line_code is not null") 40 @line_notes = @merge_request.notes.where("line_code is not null")
36 end 41 end
37 42
app/controllers/notes_controller.rb
@@ -6,10 +6,12 @@ class NotesController &lt; ProjectResourceController @@ -6,10 +6,12 @@ class NotesController &lt; ProjectResourceController
6 respond_to :js 6 respond_to :js
7 7
8 def index 8 def index
9 - notes 9 + @notes = Notes::LoadContext.new(project, current_user, params).execute
  10 + @target_type = params[:target_type].camelize
  11 + @target_id = params[:target_id]
  12 +
10 if params[:target_type] == "merge_request" 13 if params[:target_type] == "merge_request"
11 - @mixed_targets = true  
12 - @main_target_type = params[:target_type].camelize 14 + @discussions = discussions_from_notes
13 end 15 end
14 16
15 respond_with(@notes) 17 respond_with(@notes)
@@ -17,6 +19,8 @@ class NotesController &lt; ProjectResourceController @@ -17,6 +19,8 @@ class NotesController &lt; ProjectResourceController
17 19
18 def create 20 def create
19 @note = Notes::CreateContext.new(project, current_user, params).execute 21 @note = Notes::CreateContext.new(project, current_user, params).execute
  22 + @target_type = params[:target_type].camelize
  23 + @target_id = params[:target_id]
20 24
21 respond_to do |format| 25 respond_to do |format|
22 format.html {redirect_to :back} 26 format.html {redirect_to :back}
@@ -40,7 +44,34 @@ class NotesController &lt; ProjectResourceController @@ -40,7 +44,34 @@ class NotesController &lt; ProjectResourceController
40 44
41 protected 45 protected
42 46
43 - def notes  
44 - @notes = Notes::LoadContext.new(project, current_user, params).execute 47 + def discussion_notes_for(note)
  48 + @notes.select do |other_note|
  49 + note.discussion_id == other_note.discussion_id
  50 + end
  51 + end
  52 +
  53 + def discussions_from_notes
  54 + discussion_ids = []
  55 + discussions = []
  56 +
  57 + @notes.each do |note|
  58 + next if discussion_ids.include?(note.discussion_id)
  59 +
  60 + # don't group notes for the main target
  61 + if note_for_main_target?(note)
  62 + discussions << [note]
  63 + else
  64 + discussions << discussion_notes_for(note)
  65 + discussion_ids << note.discussion_id
  66 + end
  67 + end
  68 +
  69 + discussions
  70 + end
  71 +
  72 + # Helps to distinguish e.g. commit notes in mr notes list
  73 + def note_for_main_target?(note)
  74 + note.for_wall? ||
  75 + (@target_type.camelize == note.noteable_type && !note.for_diff_line?)
45 end 76 end
46 end 77 end
app/controllers/projects_controller.rb
@@ -80,7 +80,10 @@ class ProjectsController &lt; ProjectResourceController @@ -80,7 +80,10 @@ class ProjectsController &lt; ProjectResourceController
80 80
81 def wall 81 def wall
82 return render_404 unless @project.wall_enabled 82 return render_404 unless @project.wall_enabled
83 - @note = Note.new 83 +
  84 + @target_type = :wall
  85 + @target_id = nil
  86 + @note = @project.notes.new
84 87
85 respond_to do |format| 88 respond_to do |format|
86 format.html 89 format.html
app/controllers/snippets_controller.rb
@@ -50,6 +50,8 @@ class SnippetsController &lt; ProjectResourceController @@ -50,6 +50,8 @@ class SnippetsController &lt; ProjectResourceController
50 50
51 def show 51 def show
52 @note = @project.notes.new(noteable: @snippet) 52 @note = @project.notes.new(noteable: @snippet)
  53 + @target_type = :snippet
  54 + @target_id = @snippet.id
53 end 55 end
54 56
55 def destroy 57 def destroy
app/helpers/commits_helper.rb
@@ -9,11 +9,13 @@ module CommitsHelper @@ -9,11 +9,13 @@ module CommitsHelper
9 end 9 end
10 end 10 end
11 11
12 - def build_line_anchor(index, line_new, line_old)  
13 - "#{index}_#{line_old}_#{line_new}" 12 + def build_line_anchor(diff, line_new, line_old)
  13 + "#{hexdigest(diff.new_path)}_#{line_old}_#{line_new}"
14 end 14 end
15 15
16 - def each_diff_line(diff_arr, index) 16 + def each_diff_line(diff, index)
  17 + diff_arr = diff.diff.lines.to_a
  18 +
17 line_old = 1 19 line_old = 1
18 line_new = 1 20 line_new = 1
19 type = nil 21 type = nil
@@ -39,7 +41,7 @@ module CommitsHelper @@ -39,7 +41,7 @@ module CommitsHelper
39 next 41 next
40 else 42 else
41 type = identification_type(line) 43 type = identification_type(line)
42 - line_code = build_line_anchor(index, line_new, line_old) 44 + line_code = build_line_anchor(diff, line_new, line_old)
43 yield(full_line, type, line_code, line_new, line_old) 45 yield(full_line, type, line_code, line_new, line_old)
44 end 46 end
45 47
app/helpers/notes_helper.rb
1 module NotesHelper 1 module NotesHelper
2 - def loading_more_notes?  
3 - params[:loading_more].present? 2 + # Helps to distinguish e.g. commit notes in mr notes list
  3 + def note_for_main_target?(note)
  4 + note.for_wall? ||
  5 + (@target_type.camelize == note.noteable_type && !note.for_diff_line?)
4 end 6 end
5 7
6 - def loading_new_notes?  
7 - params[:loading_new].present? 8 + def note_target_fields
  9 + hidden_field_tag(:target_type, @target_type) +
  10 + hidden_field_tag(:target_id, @target_id)
8 end 11 end
9 12
10 - # Helps to distinguish e.g. commit notes in mr notes list  
11 - def note_for_main_target?(note)  
12 - !@mixed_targets || @main_target_type == note.noteable_type 13 + def link_to_commit_diff_line_note(note)
  14 + if note.for_commit_diff_line?
  15 + link_to "#{note.diff_file_name}:L#{note.diff_new_line}", project_commit_path(@project, note.noteable, anchor: note.line_code)
  16 + end
13 end 17 end
14 18
15 - def link_to_commit_diff_line_note(note)  
16 - commit = note.noteable  
17 - diff_index, diff_old_line, diff_new_line = note.line_code.split('_') 19 + def link_to_merge_request_diff_line_note(note)
  20 + if note.for_merge_request_diff_line? and note.diff
  21 + link_to "#{note.diff_file_name}:L#{note.diff_new_line}", diffs_project_merge_request_path(note.project, note.noteable_id, anchor: note.line_code)
  22 + end
  23 + end
18 24
19 - link_file = commit.diffs[diff_index.to_i].new_path  
20 - link_line = diff_new_line 25 + def loading_more_notes?
  26 + params[:loading_more].present?
  27 + end
21 28
22 - link_to "#{link_file}:L#{link_line}", project_commit_path(@project, commit, anchor: note.line_code) 29 + def loading_new_notes?
  30 + params[:loading_new].present?
23 end 31 end
24 end 32 end
app/mailers/notify.rb
@@ -63,12 +63,12 @@ class Notify &lt; ActionMailer::Base @@ -63,12 +63,12 @@ class Notify &lt; ActionMailer::Base
63 # Note 63 # Note
64 # 64 #
65 65
66 - def note_commit_email(recipient_id, note_id) 66 + def note_commit_email(commit_autor_email, note_id)
67 @note = Note.find(note_id) 67 @note = Note.find(note_id)
68 @commit = @note.noteable 68 @commit = @note.noteable
69 @commit = CommitDecorator.decorate(@commit) 69 @commit = CommitDecorator.decorate(@commit)
70 @project = @note.project 70 @project = @note.project
71 - mail(to: recipient(recipient_id), subject: subject("note for commit #{@commit.short_id}", @commit.title)) 71 + mail(to: recipient(commit_autor_email), subject: subject("note for commit #{@commit.short_id}", @commit.title))
72 end 72 end
73 73
74 def note_issue_email(recipient_id, note_id) 74 def note_issue_email(recipient_id, note_id)
app/models/concerns/issuable.rb
@@ -69,29 +69,33 @@ module Issuable @@ -69,29 +69,33 @@ module Issuable
69 closed_changed? && !closed 69 closed_changed? && !closed
70 end 70 end
71 71
72 - # Return the number of +1 comments (upvotes)  
73 - def upvotes  
74 - notes.select(&:upvote?).size 72 + #
  73 + # Votes
  74 + #
  75 +
  76 + # Return the number of -1 comments (downvotes)
  77 + def downvotes
  78 + notes.select(&:downvote?).size
75 end 79 end
76 80
77 - def upvotes_in_percent 81 + def downvotes_in_percent
78 if votes_count.zero? 82 if votes_count.zero?
79 0 83 0
80 else 84 else
81 - 100.0 / votes_count * upvotes 85 + 100.0 - upvotes_in_percent
82 end 86 end
83 end 87 end
84 88
85 - # Return the number of -1 comments (downvotes)  
86 - def downvotes  
87 - notes.select(&:downvote?).size 89 + # Return the number of +1 comments (upvotes)
  90 + def upvotes
  91 + notes.select(&:upvote?).size
88 end 92 end
89 93
90 - def downvotes_in_percent 94 + def upvotes_in_percent
91 if votes_count.zero? 95 if votes_count.zero?
92 0 96 0
93 else 97 else
94 - 100.0 - upvotes_in_percent 98 + 100.0 / votes_count * upvotes
95 end 99 end
96 end 100 end
97 101
app/models/note.rb
@@ -19,7 +19,6 @@ require &#39;carrierwave/orm/activerecord&#39; @@ -19,7 +19,6 @@ require &#39;carrierwave/orm/activerecord&#39;
19 require 'file_size_validator' 19 require 'file_size_validator'
20 20
21 class Note < ActiveRecord::Base 21 class Note < ActiveRecord::Base
22 -  
23 attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id, 22 attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id,
24 :attachment, :line_code, :commit_id 23 :attachment, :line_code, :commit_id
25 24
@@ -34,6 +33,7 @@ class Note &lt; ActiveRecord::Base @@ -34,6 +33,7 @@ class Note &lt; ActiveRecord::Base
34 delegate :name, :email, to: :author, prefix: true 33 delegate :name, :email, to: :author, prefix: true
35 34
36 validates :note, :project, presence: true 35 validates :note, :project, presence: true
  36 + validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
37 validates :attachment, file_size: { maximum: 10.megabytes.to_i } 37 validates :attachment, file_size: { maximum: 10.megabytes.to_i }
38 38
39 validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } 39 validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
@@ -60,12 +60,74 @@ class Note &lt; ActiveRecord::Base @@ -60,12 +60,74 @@ class Note &lt; ActiveRecord::Base
60 }, without_protection: true) 60 }, without_protection: true)
61 end 61 end
62 62
63 - def notify  
64 - @notify ||= false 63 + def commit_author
  64 + @commit_author ||=
  65 + project.users.find_by_email(noteable.author_email) ||
  66 + project.users.find_by_name(noteable.author_name)
  67 + rescue
  68 + nil
65 end 69 end
66 70
67 - def notify_author  
68 - @notify_author ||= false 71 + def diff
  72 + if noteable.diffs.present?
  73 + noteable.diffs.select do |d|
  74 + if d.b_path
  75 + Digest::SHA1.hexdigest(d.b_path) == diff_file_index
  76 + end
  77 + end.first
  78 + end
  79 + end
  80 +
  81 + def diff_file_index
  82 + line_code.split('_')[0]
  83 + end
  84 +
  85 + def diff_file_name
  86 + diff.b_path
  87 + end
  88 +
  89 + def diff_new_line
  90 + line_code.split('_')[2].to_i
  91 + end
  92 +
  93 + def discussion_id
  94 + @discussion_id ||= [:discussion, noteable_type.try(:underscore), noteable_id, line_code].join("-").to_sym
  95 + end
  96 +
  97 + # Returns true if this is a downvote note,
  98 + # otherwise false is returned
  99 + def downvote?
  100 + votable? && (note.start_with?('-1') ||
  101 + note.start_with?(':-1:')
  102 + )
  103 + end
  104 +
  105 + def for_commit?
  106 + noteable_type == "Commit"
  107 + end
  108 +
  109 + def for_commit_diff_line?
  110 + for_commit? && for_diff_line?
  111 + end
  112 +
  113 + def for_diff_line?
  114 + line_code.present?
  115 + end
  116 +
  117 + def for_issue?
  118 + noteable_type == "Issue"
  119 + end
  120 +
  121 + def for_merge_request?
  122 + noteable_type == "MergeRequest"
  123 + end
  124 +
  125 + def for_merge_request_diff_line?
  126 + for_merge_request? && for_diff_line?
  127 + end
  128 +
  129 + def for_wall?
  130 + noteable_type.blank?
69 end 131 end
70 132
71 # override to return commits, which are not active record 133 # override to return commits, which are not active record
@@ -81,50 +143,24 @@ class Note &lt; ActiveRecord::Base @@ -81,50 +143,24 @@ class Note &lt; ActiveRecord::Base
81 nil 143 nil
82 end 144 end
83 145
84 - # Check if we can notify commit author  
85 - # with email about our comment  
86 - #  
87 - # If commit author email exist in project  
88 - # and commit author is not passed user we can  
89 - # send email to him  
90 - #  
91 - # params:  
92 - # user - current user  
93 - #  
94 - # return:  
95 - # Boolean  
96 - #  
97 - def notify_only_author?(user)  
98 - for_commit? && commit_author &&  
99 - commit_author.email != user.email  
100 - end  
101 -  
102 - def for_commit?  
103 - noteable_type == "Commit"  
104 - end  
105 -  
106 - def for_diff_line?  
107 - line_code.present? 146 + def notify
  147 + @notify ||= false
108 end 148 end
109 149
110 - def commit_author  
111 - @commit_author ||=  
112 - project.users.find_by_email(noteable.author_email) ||  
113 - project.users.find_by_name(noteable.author_name)  
114 - rescue  
115 - nil 150 + def notify_author
  151 + @notify_author ||= false
116 end 152 end
117 153
118 # Returns true if this is an upvote note, 154 # Returns true if this is an upvote note,
119 # otherwise false is returned 155 # otherwise false is returned
120 def upvote? 156 def upvote?
121 - note.start_with?('+1') || note.start_with?(':+1:') 157 + votable? && (note.start_with?('+1') ||
  158 + note.start_with?(':+1:')
  159 + )
122 end 160 end
123 161
124 - # Returns true if this is a downvote note,  
125 - # otherwise false is returned  
126 - def downvote?  
127 - note.start_with?('-1') || note.start_with?(':-1:') 162 + def votable?
  163 + for_issue? || (for_merge_request? && !for_diff_line?)
128 end 164 end
129 165
130 def noteable_type_name 166 def noteable_type_name
app/observers/note_observer.rb
@@ -11,7 +11,7 @@ class NoteObserver &lt; ActiveRecord::Observer @@ -11,7 +11,7 @@ class NoteObserver &lt; ActiveRecord::Observer
11 notify_team(note) 11 notify_team(note)
12 elsif note.notify_author 12 elsif note.notify_author
13 # Notify only author of resource 13 # Notify only author of resource
14 - Notify.delay.note_commit_email(note.commit_author.id, note.id) 14 + Notify.delay.note_commit_email(note.noteable.author_email, note.id)
15 else 15 else
16 # Otherwise ignore it 16 # Otherwise ignore it
17 nil 17 nil
app/views/commit/show.html.haml
@@ -7,12 +7,10 @@ @@ -7,12 +7,10 @@
7 %span.cred #{@commit.stats.deletions} deletions 7 %span.cred #{@commit.stats.deletions} deletions
8 8
9 = render "commits/diffs", diffs: @commit.diffs 9 = render "commits/diffs", diffs: @commit.diffs
10 -= render "notes/notes_with_form", tid: @commit.id, tt: "commit"  
11 -= render "notes/per_line_form" 10 += render "notes/notes_with_form"
12 11
13 :javascript 12 :javascript
14 $(function(){ 13 $(function(){
15 - PerLineNotes.init();  
16 var w, h; 14 var w, h;
17 $('.diff_file').each(function(){ 15 $('.diff_file').each(function(){
18 $('.image.diff_removed img', this).on('load', $.proxy(function(event){ 16 $('.image.diff_removed img', this).on('load', $.proxy(function(event){
app/views/commits/_diffs.html.haml
@@ -38,10 +38,10 @@ @@ -38,10 +38,10 @@
38 38
39 %br/ 39 %br/
40 .diff_file_content 40 .diff_file_content
41 - -# Skipp all non non-supported blobs 41 + -# Skip all non-supported blobs
42 - next unless file.respond_to?('text?') 42 - next unless file.respond_to?('text?')
43 - if file.text? 43 - if file.text?
44 - = render "commits/text_file", diff: diff, index: i 44 + = render "commits/text_diff", diff: diff, index: i
45 - elsif file.image? 45 - elsif file.image?
46 - old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil? 46 - old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil?
47 - if diff.renamed_file || diff.new_file || diff.deleted_file 47 - if diff.renamed_file || diff.new_file || diff.deleted_file
app/views/commits/_text_diff.html.haml 0 → 100644
@@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
  1 +- too_big = diff.diff.lines.count > 1000
  2 +- if too_big
  3 + %a.supp_diff_link Diff suppressed. Click to show
  4 +
  5 +%table{class: "#{'hide' if too_big}"}
  6 + - each_diff_line(diff, index) do |line, type, line_code, line_new, line_old|
  7 + %tr.line_holder{ id: line_code }
  8 + - if type == "match"
  9 + %td.old_line= "..."
  10 + %td.new_line= "..."
  11 + %td.line_content.matched= line
  12 + - else
  13 + %td.old_line
  14 + = link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code
  15 + - if @comments_allowed
  16 + = render "notes/diff_note_link", line_code: line_code
  17 + %td.new_line= link_to raw(type == "old" ? "&nbsp;" : line_new) , "##{line_code}", id: line_code
  18 + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line)
  19 +
  20 + - if @reply_allowed
  21 + - comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at)
  22 + - unless comments.empty?
  23 + = render "notes/diff_notes_with_reply", notes: comments
app/views/commits/_text_file.html.haml
@@ -1,23 +0,0 @@ @@ -1,23 +0,0 @@
1 -- too_big = diff.diff.lines.count > 1000  
2 -- if too_big  
3 - %a.supp_diff_link Diff suppressed. Click to show  
4 -  
5 -%table{class: "#{'hide' if too_big}"}  
6 - - each_diff_line(diff.diff.lines.to_a, index) do |line, type, line_code, line_new, line_old|  
7 - %tr.line_holder{ id: line_code }  
8 - - if type == "match"  
9 - %td.old_line= "..."  
10 - %td.new_line= "..."  
11 - %td.line_content.matched= line  
12 - - else  
13 - %td.old_line  
14 - = link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code  
15 - - if @comments_allowed  
16 - = render "notes/per_line_note_link", line_code: line_code  
17 - %td.new_line= link_to raw(type == "old" ? "&nbsp;" : line_new) , "##{line_code}", id: line_code  
18 - %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line)  
19 -  
20 - - if @comments_allowed  
21 - - comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at)  
22 - - unless comments.empty?  
23 - = render "notes/per_line_notes_with_reply", notes: comments  
app/views/issues/show.html.haml
@@ -56,4 +56,4 @@ @@ -56,4 +56,4 @@
56 = markdown @issue.description 56 = markdown @issue.description
57 57
58 58
59 -.issue_notes.voting_notes#notes= render "notes/notes_with_form", tid: @issue.id, tt: "issue" 59 +.voting_notes#notes= render "notes/notes_with_form"
app/views/merge_requests/_show.html.haml
@@ -12,20 +12,18 @@ @@ -12,20 +12,18 @@
12 %li.notes-tab{data: {action: 'notes'}} 12 %li.notes-tab{data: {action: 'notes'}}
13 = link_to project_merge_request_path(@project, @merge_request) do 13 = link_to project_merge_request_path(@project, @merge_request) do
14 %i.icon-comment 14 %i.icon-comment
15 - Comments 15 + Discussion
16 %li.diffs-tab{data: {action: 'diffs'}} 16 %li.diffs-tab{data: {action: 'diffs'}}
17 = link_to diffs_project_merge_request_path(@project, @merge_request) do 17 = link_to diffs_project_merge_request_path(@project, @merge_request) do
18 %i.icon-list-alt 18 %i.icon-list-alt
19 Diff 19 Diff
20 20
21 .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" } 21 .notes.tab-content.voting_notes#notes{ class: (controller.action_name == 'show') ? "" : "hide" }
22 - = render("notes/notes_with_form", tid: @merge_request.id, tt: "merge_request") 22 + = render "notes/notes_with_form"
23 .diffs.tab-content 23 .diffs.tab-content
24 = render "merge_requests/show/diffs" if @diffs 24 = render "merge_requests/show/diffs" if @diffs
25 .status 25 .status
26 26
27 - = render "notes/per_line_form"  
28 -  
29 :javascript 27 :javascript
30 var merge_request; 28 var merge_request;
31 $(function(){ 29 $(function(){
app/views/merge_requests/diffs.html.haml
1 = render "show" 1 = render "show"
2 -  
3 -:javascript  
4 - $(function(){  
5 - PerLineNotes.init();  
6 - });  
app/views/merge_requests/diffs.js.haml
1 :plain 1 :plain
2 merge_request.$(".diffs").html("#{escape_javascript(render(partial: "merge_requests/show/diffs"))}"); 2 merge_request.$(".diffs").html("#{escape_javascript(render(partial: "merge_requests/show/diffs"))}");
3 -  
4 - PerLineNotes.init();  
5 -  
app/views/merge_requests/show.js.haml
1 :plain 1 :plain
2 - merge_request.$(".notes").html("#{escape_javascript(render "notes/notes_with_form", tid: @merge_request.id, tt: "merge_request")}"); 2 + merge_request.$(".notes").html("#{escape_javascript(render "notes/notes_with_form")}");
app/views/notes/_common_form.html.haml
@@ -1,39 +0,0 @@ @@ -1,39 +0,0 @@
1 -.note-form-holder  
2 - = form_for [@project, @note], remote: "true", multipart: true do |f|  
3 - %h3.page_title Leave a comment  
4 - -if @note.errors.any?  
5 - .alert-message.block-message.error  
6 - - @note.errors.full_messages.each do |msg|  
7 - %div= msg  
8 -  
9 - = f.hidden_field :noteable_id  
10 - = f.hidden_field :commit_id  
11 - = f.hidden_field :noteable_type  
12 - = f.text_area :note, size: 255, class: 'note-text js-gfm-input'  
13 - #preview-note.preview_note.hide  
14 - .hint  
15 - .right Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.  
16 - .clearfix  
17 -  
18 - .row.note_advanced_opts  
19 - .span3  
20 - = f.submit 'Add Comment', class: "btn success submit_note grouped", id: "submit_note"  
21 - = link_to 'Preview', preview_project_notes_path(@project), class: 'btn grouped', id: 'preview-link'  
22 - .span4.notify_opts  
23 - %h6.left Notify via email:  
24 - = label_tag :notify do  
25 - = check_box_tag :notify, 1, @note.noteable_type != "Commit"  
26 - %span Project team  
27 -  
28 - - if @note.notify_only_author?(current_user)  
29 - = label_tag :notify_author do  
30 - = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit"  
31 - %span Commit author  
32 - .span5.attachments  
33 - %h6.left Attachment:  
34 - %span.file_name File name...  
35 -  
36 - .input.input_file  
37 - %a.file_upload.btn.small Upload File  
38 - = f.file_field :attachment, class: "input-file"  
39 - %span.hint Any file less than 10 MB  
app/views/notes/_create_common_note.js.haml
@@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
1 -- if note.valid?  
2 - :plain  
3 - $(".note-form-holder .error").remove();  
4 - $('.note-form-holder textarea').val("");  
5 - $('.note-form-holder #preview-link').text('Preview');  
6 - $('.note-form-holder #preview-note').hide();  
7 - $('.note-form-holder').show();  
8 - NoteList.appendNewNote(#{note.id}, "#{escape_javascript(render "notes/note", note: note)}");  
9 -  
10 -- else  
11 - :plain  
12 - $(".note-form-holder").replaceWith("#{escape_javascript(render 'notes/common_form')}");  
13 - GitLab.GfmAutoComplete.setup();  
app/views/notes/_create_per_line_note.js.haml
@@ -1,19 +0,0 @@ @@ -1,19 +0,0 @@
1 -- if note.valid?  
2 - :plain  
3 - // hide and reset the form  
4 - $(".per_line_form").hide();  
5 - $('.line-note-form-holder textarea').val("");  
6 -  
7 - // find the reply button for this line  
8 - // (might not be there if this is the first note)  
9 - var trRpl = $("a.line_note_reply_link[data-line-code='#{note.line_code}']").closest("tr");  
10 - if (trRpl.size() == 0) {  
11 - // find the commented line ...  
12 - var trEl = $(".#{note.line_code}").parent();  
13 - // ... and insert the note and the reply button after it  
14 - trEl.after("#{escape_javascript(render "notes/per_line_reply_button", line_code: note.line_code)}");  
15 - trEl.after("#{escape_javascript(render "notes/per_line_note", note: note)}");  
16 - } else {  
17 - // instert new note before reply button  
18 - trRpl.before("#{escape_javascript(render "notes/per_line_note", note: note)}");  
19 - }  
app/views/notes/_diff_note_link.html.haml 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +- note = @project.notes.new(@comments_target.merge({ line_code: line_code }))
  2 += link_to "",
  3 + "javascript:;",
  4 + class: "add-diff-note js-add-diff-note-button",
  5 + data: { noteable_type: note.noteable_type,
  6 + noteable_id: note.noteable_id,
  7 + commit_id: note.commit_id,
  8 + line_code: note.line_code,
  9 + discussion_id: note.discussion_id },
  10 + title: "Add a comment to this line"
app/views/notes/_diff_notes_with_reply.html.haml 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +- note = notes.first # example note
  2 +%tr.notes_holder
  3 + %td.notes_line{ colspan: 2 }
  4 + %span.btn.disabled
  5 + %i.icon-comment
  6 + = notes.count
  7 + %td.notes_content
  8 + %ul.notes{ rel: note.discussion_id }
  9 + = render notes
  10 +
  11 + = render "notes/discussion_reply_button", note: note
app/views/notes/_discussion.html.haml 0 → 100644
@@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
  1 +- note = discussion_notes.first
  2 +.discussion.js-details-container.js-toggler-container.open{ class: note.discussion_id }
  3 + .discussion-header
  4 + .discussion-actions
  5 + = link_to "javascript:;", class: "js-details-target turn-on js-toggler-target" do
  6 + %i.icon-eye-close
  7 + Hide discussion
  8 + = link_to "javascript:;", class: "js-details-target turn-off js-toggler-target" do
  9 + %i.icon-eye-open
  10 + Show discussion
  11 + = image_tag gravatar_icon(note.author.email), class: "avatar s32"
  12 + %div
  13 + = link_to note.author_name, project_team_member_path(@project, @project.team_member_by_id(note.author)), class: "note-author"
  14 + - if note.for_merge_request?
  15 + - if note.diff
  16 + started a discussion on this merge request diff
  17 + = link_to_merge_request_diff_line_note(note)
  18 + - else
  19 + started
  20 + %strong
  21 + %i.icon-remove
  22 + outdated
  23 + discussion on this merge request diff
  24 + - elsif note.for_commit?
  25 + started a discussion on commit
  26 + #{link_to note.noteable.short_id, project_commit_path(@project, note.noteable)}
  27 + = link_to_commit_diff_line_note(note) if note.for_diff_line?
  28 + - else
  29 + %cite.cgray started a discussion
  30 + %div
  31 + - last_note = discussion_notes.last
  32 + last updated by
  33 + = link_to last_note.author_name, project_team_member_path(@project, @project.team_member_by_id(last_note.author)), class: "note-author"
  34 + %span.discussion-last-update
  35 + = time_ago_in_words(last_note.updated_at)
  36 + ago
  37 + .discussion-body
  38 + - if note.for_diff_line?
  39 + - if note.diff
  40 + .content
  41 + .diff_file= render "notes/discussion_diff", discussion_notes: discussion_notes, note: note
  42 + - else
  43 + = link_to 'show outdated discussion', '#', class: 'js-show-outdated-discussion'
  44 + %div.hide.outdated-discussion
  45 + .content
  46 + .notes{ rel: discussion_notes.first.discussion_id }
  47 + = render discussion_notes
  48 +
  49 +
  50 + - else
  51 + .content
  52 + .notes{ rel: discussion_notes.first.discussion_id }
  53 + = render discussion_notes
  54 + = render "notes/discussion_reply_button", note: discussion_notes.first
  55 +
  56 + -# will be shown when the other one is hidden
  57 + .discussion-hidden.content.hide
  58 + .note
  59 + %em Hidden discussion.
  60 + = link_to "javascript:;", class: "js-details-target js-toggler-target" do
  61 + %i.icon-eye-open
  62 + Show
  63 +
app/views/notes/_discussion_diff.html.haml 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +- diff = note.diff
  2 +.diff_file_header
  3 + - if diff.deleted_file
  4 + %span= diff.old_path
  5 + - else
  6 + %span= diff.new_path
  7 + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
  8 + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}"
  9 + %br/
  10 +.diff_file_content
  11 + %table
  12 + - each_diff_line(diff, note.diff_file_index) do |line, type, line_code, line_new, line_old|
  13 + %tr.line_holder{ id: line_code }
  14 + - if type == "match"
  15 + %td.old_line= "..."
  16 + %td.new_line= "..."
  17 + %td.line_content.matched= line
  18 + - else
  19 + %td.old_line= raw(type == "new" ? "&nbsp;" : line_old)
  20 + %td.new_line= raw(type == "old" ? "&nbsp;" : line_new)
  21 + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw "#{line} &nbsp;"
  22 +
  23 + - if line_code == note.line_code
  24 + = render "notes/diff_notes_with_reply", notes: discussion_notes
  25 + - break # cut off diff after notes
app/views/notes/_discussion_reply_button.html.haml 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 += link_to "javascript:;",
  2 + class: "btn reply-btn js-discussion-reply-button",
  3 + data: { noteable_type: note.noteable_type,
  4 + noteable_id: note.noteable_id,
  5 + commit_id: note.commit_id,
  6 + line_code: note.line_code,
  7 + discussion_id: note.discussion_id },
  8 + title: "Add a reply" do
  9 + %i.icon-comment
  10 + Reply
app/views/notes/_form.html.haml 0 → 100644
@@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
  1 += form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note js-new-note-form" } do |f|
  2 +
  3 + = note_target_fields
  4 + = f.hidden_field :commit_id
  5 + = f.hidden_field :line_code
  6 + = f.hidden_field :noteable_id
  7 + = f.hidden_field :noteable_type
  8 +
  9 + .note_text_and_preview.js-toggler-container
  10 + %a.js-note-preview-button.js-toggler-target.turn-off{ href: "javascript:;", title: "Preview", data: {url: preview_project_notes_path(@project)} }
  11 + %i.icon-eye-open
  12 + %a.js-note-edit-button.js-toggler-target.turn-off{ href: "javascript:;", title: "Edit" }
  13 + %i.icon-edit
  14 +
  15 + = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on'
  16 + .note_preview.js-note-preview.turn-off
  17 +
  18 + .buttons
  19 + = f.submit 'Add Comment', class: "btn comment-btn grouped js-comment-button"
  20 + %a.btn.grouped.js-close-discussion-note-form Cancel
  21 + .hint
  22 + .right Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
  23 + .clearfix
  24 +
  25 + .note_options
  26 + .attachment
  27 + %h6 Attachment:
  28 + .file_name.js-attachment-filename File name...
  29 + %a.choose-btn.btn.small.js-choose-note-attachment-button Choose File ...
  30 + .hint Any file up to 10 MB
  31 +
  32 + = f.file_field :attachment, class: "js-note-attachment-input"
  33 +
  34 + .notify_options
  35 + %h6 Notify via email:
  36 + = label_tag :notify do
  37 + = check_box_tag :notify, 1, !@note.for_commit?
  38 + Project team
  39 +
  40 + .js-notify-commit-author
  41 + = label_tag :notify_author do
  42 + = check_box_tag :notify_author, 1 , @note.for_commit?
  43 + Commit author
  44 + .clearfix
app/views/notes/_form_errors.html.haml 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +.error_message.js-errors
  2 + - note.errors.full_messages.each do |msg|
  3 + %div= msg
app/views/notes/_note.html.haml
1 -%li{id: dom_id(note), class: "note"}  
2 - = image_tag gravatar_icon(note.author.email), class: "avatar s32"  
3 - %div.note-author  
4 - %strong= note.author_name  
5 - = link_to "##{dom_id(note)}", name: dom_id(note) do  
6 - %cite.cgray  
7 - = time_ago_in_words(note.updated_at)  
8 - ago 1 +%li{ id: dom_id(note), class: dom_class(note), data: { discussion: note.discussion_id } }
  2 + .note-header
  3 + .note-actions
  4 + = link_to "##{dom_id(note)}", name: dom_id(note) do
  5 + %i.icon-link
  6 + Link here
  7 + &nbsp;
  8 + - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
  9 + = link_to project_note_path(@project, note), title: "Remove comment", method: :delete, confirm: 'Are you sure you want to remove comment?', remote: true, class: "danger js-note-delete" do
  10 + %i.icon-trash.cred
  11 + = image_tag gravatar_icon(note.author.email), class: "avatar s32"
  12 + = link_to note.author_name, project_team_member_path(@project, @project.team_member_by_id(note.author)), class: "note-author"
  13 + %span.note-last-update
  14 + = time_ago_in_words(note.updated_at)
  15 + ago
9 16
10 - - unless note_for_main_target?(note)  
11 - - if note.for_commit?  
12 - %span.cgray  
13 - on #{link_to note.noteable.short_id, project_commit_path(@project, note.noteable)}  
14 - = link_to_commit_diff_line_note(note) if note.for_diff_line? 17 + - if note.upvote?
  18 + %span.vote.upvote.label.label-success
  19 + %i.icon-thumbs-up
  20 + \+1
  21 + - if note.downvote?
  22 + %span.vote.downvote.label.label-error
  23 + %i.icon-thumbs-down
  24 + \-1
15 25
16 - -# only show vote if it's a note for the main target  
17 - - if note_for_main_target?(note)  
18 - - if note.upvote?  
19 - %span.vote.upvote.label.label-success  
20 - %i.icon-thumbs-up  
21 - \+1  
22 - - if note.downvote?  
23 - %span.vote.downvote.label.label-error  
24 - %i.icon-thumbs-down  
25 - \-1  
26 26
27 - -# remove button  
28 - - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)  
29 - = link_to [@project, note], confirm: 'Are you sure?', method: :delete, remote: true, class: "cred delete-note btn very_small" do  
30 - %i.icon-trash  
31 - Remove  
32 -  
33 - %div.note-title 27 + .note-body
34 = preserve do 28 = preserve do
35 = markdown(note.note) 29 = markdown(note.note)
36 - - if note.attachment.url  
37 - - if note.attachment.image?  
38 - = image_tag note.attachment.url, class: 'thumbnail span4'  
39 - .right  
40 - %div.file  
41 - = link_to note.attachment_identifier, note.attachment.url, target: "_blank" 30 + - if note.attachment.url
  31 + - if note.attachment.image?
  32 + = image_tag note.attachment.url, class: 'note-image-attach'
  33 + .attachment.right
  34 + = link_to note.attachment.url, target: "_blank" do
  35 + %i.icon-attachment
  36 + = note.attachment_identifier
42 .clear 37 .clear
app/views/notes/_notes.html.haml
1 -- @notes.each do |note|  
2 - - next unless note.author  
3 - = render "note", note: note  
4 - 1 +- if @discussions.present?
  2 + - @discussions.each do |discussion_notes|
  3 + - note = discussion_notes.first
  4 + - if note_for_main_target?(note)
  5 + = render discussion_notes
  6 + - else
  7 + = render 'discussion', discussion_notes: discussion_notes
  8 +- else
  9 + - @notes.each do |note|
  10 + - next unless note.author
  11 + = render 'note', note: note
app/views/notes/_notes_with_form.html.haml
1 -%ul#notes-list  
2 -%ul#new-notes-list  
3 -.notes-status 1 +%ul#notes-list.notes
  2 +.js-notes-busy
4 3
  4 +.js-main-target-form
5 - if can? current_user, :write_note, @project 5 - if can? current_user, :write_note, @project
6 - = render "notes/common_form" 6 + = render "notes/form"
7 7
8 :javascript 8 :javascript
9 $(function(){ 9 $(function(){
10 - NoteList.init("#{tid}", "#{tt}", "#{project_notes_path(@project)}"); 10 + NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}");
11 }); 11 });
app/views/notes/_per_line_form.html.haml
@@ -1,40 +0,0 @@ @@ -1,40 +0,0 @@
1 -%table{style: "display:none;"}  
2 - %tr.per_line_form  
3 - %td{colspan: 3 }  
4 - .line-note-form-holder  
5 - = form_for [@project, @note], remote: "true", multipart: true do |f|  
6 - %h3.page_title Leave a note  
7 - %div.span10  
8 - -if @note.errors.any?  
9 - .alert-message.block-message.error  
10 - - @note.errors.full_messages.each do |msg|  
11 - %div= msg  
12 -  
13 - = f.hidden_field :noteable_id  
14 - = f.hidden_field :commit_id  
15 - = f.hidden_field :noteable_type  
16 - = f.hidden_field :line_code  
17 - = f.text_area :note, size: 255, class: 'line-note-text js-gfm-input'  
18 - .note_actions  
19 - .buttons  
20 - = f.submit 'Add note', class: "btn save-btn submit_note submit_inline_note", id: "submit_note"  
21 - = link_to "Cancel", "#", class: "btn hide-button"  
22 - .options  
23 - %h6.left Notify via email:  
24 - .labels  
25 - = label_tag :notify do  
26 - = check_box_tag :notify, 1, @note.noteable_type != "Commit"  
27 - %span Project team  
28 -  
29 - - if @note.notify_only_author?(current_user)  
30 - = label_tag :notify_author do  
31 - = check_box_tag :notify_author, 1 , @note.noteable_type == "Commit"  
32 - %span Commit author  
33 -  
34 -:javascript  
35 - $(function(){  
36 - $(".per_line_form .hide-button").bind("click", function(){  
37 - $('.per_line_form').hide();  
38 - return false;  
39 - });  
40 - });  
app/views/notes/_per_line_note.html.haml
@@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
1 -%tr.line_notes_row  
2 - %td{colspan: 3}  
3 - %ul  
4 - = render "notes/note", note: note  
5 -  
app/views/notes/_per_line_note_link.html.haml
@@ -1 +0,0 @@ @@ -1 +0,0 @@
1 -= link_to "", "#", class: "line_note_link", data: { line_code: line_code }, title: "Add note for this line"  
app/views/notes/_per_line_notes_with_reply.html.haml
@@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
1 -- notes.each do |note|  
2 - = render "notes/per_line_note", note: note  
3 -= render "notes/per_line_reply_button", line_code: notes.first.line_code  
app/views/notes/_per_line_reply_button.html.haml
@@ -1,4 +0,0 @@ @@ -1,4 +0,0 @@
1 -%tr.line_notes_row.reply  
2 - %td{colspan: 3}  
3 - %i.icon-comment  
4 - = link_to "Reply", "#", class: "line_note_reply_link", data: { line_code: line_code }, title: "Add note for this line"  
app/views/notes/_reversed_notes_with_form.html.haml
  1 +.js-main-target-form
1 - if can? current_user, :write_note, @project 2 - if can? current_user, :write_note, @project
2 - = render "notes/common_form" 3 + = render "notes/form"
3 4
4 -%ul.reversed#new-notes-list  
5 -%ul.reversed#notes-list  
6 -.notes-status 5 +%ul#new-notes-list.reversed.notes
  6 +%ul#notes-list.reversed.notes
  7 +.notes-busy.js-notes-busy
7 8
8 :javascript 9 :javascript
9 $(function(){ 10 $(function(){
10 - NoteList.init("#{tid}", "#{tt}", "#{project_notes_path(@project)}"); 11 + NoteList.init("#{@target_id}", "#{@target_type}", "#{project_notes_path(@project)}");
11 }); 12 });
app/views/notes/create.js.haml
1 -- if @note.line_code  
2 - = render "create_per_line_note", note: @note  
3 -- else  
4 - = render "create_common_note", note: @note 1 +- if @note.valid?
  2 + var noteHtml = "#{escape_javascript(render "notes/note", note: @note)}";
  3 +
  4 + - if note_for_main_target?(@note)
  5 + - if @note.for_wall?
  6 + NoteList.appendNewWallNote(#{@note.id}, noteHtml);
  7 + - else
  8 + NoteList.appendNewNote(#{@note.id}, noteHtml);
  9 + - else
  10 + :plain
  11 + var firstDiscussionNoteHtml = "#{escape_javascript(render "notes/diff_notes_with_reply", notes: [@note])}";
  12 + NoteList.appendNewDiscussionNote("#{@note.discussion_id}",
  13 + firstDiscussionNoteHtml,
  14 + noteHtml);
5 15
6 --# Enable submit button  
7 -:plain  
8 - $("#submit_note").removeAttr("disabled"); 16 +- else
  17 + var errorsHtml = "#{escape_javascript(render 'notes/form_errors', note: @note)}";
  18 + - if note_for_main_target?(@note)
  19 + NoteList.errorsOnForm(errorsHtml);
  20 + - else
  21 + NoteList.errorsOnForm(errorsHtml, "#{@note.discussion_id}");
9 \ No newline at end of file 22 \ No newline at end of file
app/views/notes/index.js.haml
1 - unless @notes.blank? 1 - unless @notes.blank?
  2 + var notesHtml = "#{escape_javascript(render 'notes/notes')}";
  3 + - new_note_ids = @notes.map(&:id)
2 - if loading_more_notes? 4 - if loading_more_notes?
3 - :plain  
4 - NoteList.appendMoreNotes(#{@notes.last.id}, "#{escape_javascript(render 'notes/notes')}"); 5 + NoteList.appendMoreNotes(#{new_note_ids}, notesHtml);
5 6
6 - elsif loading_new_notes? 7 - elsif loading_new_notes?
7 - :plain  
8 - NoteList.replaceNewNotes("#{escape_javascript(render 'notes/notes')}"); 8 + NoteList.replaceNewNotes(#{new_note_ids}, notesHtml);
9 9
10 - else 10 - else
11 - :plain  
12 - NoteList.setContent(#{@notes.first.id}, #{@notes.last.id}, "#{escape_javascript(render 'notes/notes')}"); 11 + NoteList.setContent(#{new_note_ids}, notesHtml);
13 12
14 - else 13 - else
15 - if loading_more_notes? 14 - if loading_more_notes?
16 - :plain  
17 - NoteList.finishedLoadingMore(); 15 + NoteList.finishedLoadingMore();
app/views/projects/wall.html.haml
1 %div.wall_page 1 %div.wall_page
2 - = render "notes/reversed_notes_with_form", tid: nil, tt: "wall" 2 + = render "notes/reversed_notes_with_form"
app/views/snippets/show.html.haml
@@ -8,4 +8,4 @@ @@ -8,4 +8,4 @@
8 8
9 %br 9 %br
10 %div= render 'blob' 10 %div= render 'blob'
11 -%div#notes= render "notes/notes_with_form", tid: @snippet.id, tt: "snippet" 11 +%div#notes= render "notes/notes_with_form"
features/project/commits/commit_comments.feature
1 -Feature: Project Comment commit 1 +Feature: Comments on commits
2 Background: 2 Background:
3 Given I sign in as a user 3 Given I sign in as a user
4 And I own project "Shop" 4 And I own project "Shop"
5 - Given I visit project commit page 5 + And I visit project commit page
6 6
7 @javascript 7 @javascript
8 - Scenario: I comment commit 8 + Scenario: I can comment on a commit
9 Given I leave a comment like "XML attached" 9 Given I leave a comment like "XML attached"
10 - Then I should see comment "XML attached" 10 + Then I should see a comment saying "XML attached"
  11 +
  12 + @javascript
  13 + Scenario: I can't cancel the main form
  14 + Then I should not see the cancel comment button
  15 +
  16 + @javascript
  17 + Scenario: I can't preview without text
  18 + Given I haven't written any comment text
  19 + Then I should not see the comment preview button
  20 +
  21 + @javascript
  22 + Scenario: I can preview with text
  23 + Given I write a comment like "Nice"
  24 + Then I should see the comment preview button
  25 +
  26 + @javascript
  27 + Scenario: I preview a comment
  28 + Given I preview a comment text like "Bug fixed :smile:"
  29 + Then I should see the comment preview
  30 + And I should not see the comment text field
  31 +
  32 + @javascript
  33 + Scenario: I can edit after preview
  34 + Given I preview a comment text like "Bug fixed :smile:"
  35 + Then I should see the comment edit button
  36 +
  37 + @javascript
  38 + Scenario: I have a reset form after posting from preview
  39 + Given I preview a comment text like "Bug fixed :smile:"
  40 + And I submit the comment
  41 + Then I should see an empty comment text field
  42 + And I should not see the comment preview
  43 +
  44 + @javascript
  45 + Scenario: I can delete a comment
  46 + Given I leave a comment like "XML attached"
  47 + And I delete a comment
  48 + Then I should not see a comment saying "XML attached"
features/project/commits/commit_diff_comments.feature 0 → 100644
@@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
  1 +Feature: Comments on commit diffs
  2 + Background:
  3 + Given I sign in as a user
  4 + And I own project "Shop"
  5 + And I visit project commit page
  6 +
  7 + @javascript
  8 + Scenario: I can access add diff comment buttons
  9 + Then I should see add a diff comment button
  10 +
  11 + @javascript
  12 + Scenario: I can comment on a commit diff
  13 + Given I leave a diff comment like "Typo, please fix"
  14 + Then I should see a diff comment saying "Typo, please fix"
  15 +
  16 + @javascript
  17 + Scenario: I get a temporary form for the first comment on a diff line
  18 + Given I open a diff comment form
  19 + Then I should see a temporary diff comment form
  20 +
  21 + @javascript
  22 + Scenario: I have a cancel button on the diff form
  23 + Given I open a diff comment form
  24 + Then I should see the cancel comment button
  25 +
  26 + @javascript
  27 + Scenario: I can cancel a diff form
  28 + Given I open a diff comment form
  29 + And I cancel the diff comment
  30 + Then I should not see the diff comment form
  31 +
  32 + @javascript
  33 + Scenario: I can't open a second form for a diff line
  34 + Given I open a diff comment form
  35 + And I open a diff comment form
  36 + Then I should only see one diff form
  37 +
  38 + @javascript
  39 + Scenario: I can have multiple forms
  40 + Given I open a diff comment form
  41 + And I write a diff comment like ":-1: I don't like this"
  42 + And I open another diff comment form
  43 + Then I should see a diff comment form with ":-1: I don't like this"
  44 + And I should see an empty diff comment form
  45 +
  46 + @javascript
  47 + Scenario: I can preview multiple forms separately
  48 + Given I preview a diff comment text like "Should fix it :smile:"
  49 + And I preview another diff comment text like "DRY this up"
  50 + Then I should see two separate previews
  51 +
  52 + @javascript
  53 + Scenario: I have a reply button in discussions
  54 + Given I leave a diff comment like "Typo, please fix"
  55 + Then I should see a discussion reply button
  56 +
  57 + @javascript
  58 + Scenario: I can't preview without text
  59 + Given I open a diff comment form
  60 + And I haven't written any diff comment text
  61 + Then I should not see the diff comment preview button
  62 +
  63 + @javascript
  64 + Scenario: I can preview with text
  65 + Given I open a diff comment form
  66 + And I write a diff comment like ":-1: I don't like this"
  67 + Then I should see the diff comment preview button
  68 +
  69 + @javascript
  70 + Scenario: I preview a diff comment
  71 + Given I preview a diff comment text like "Should fix it :smile:"
  72 + Then I should see the diff comment preview
  73 + And I should not see the diff comment text field
  74 +
  75 + @javascript
  76 + Scenario: I can edit after preview
  77 + Given I preview a diff comment text like "Should fix it :smile:"
  78 + Then I should see the diff comment edit button
  79 +
  80 + @javascript
  81 + Scenario: The form gets removed after posting
  82 + Given I preview a diff comment text like "Should fix it :smile:"
  83 + And I submit the diff comment
  84 + Then I should not see the diff comment form
  85 + And I should see a discussion reply button
  86 +
  87 + @javascript
  88 + Scenario: I can delete a discussion comment
  89 + Given I leave a diff comment like "Typo, please fix"
  90 + And I delete a diff comment
  91 + Then I should not see a diff comment saying "Typo, please fix"
features/project/merge_requests.feature
@@ -35,8 +35,34 @@ Feature: Project Merge Requests @@ -35,8 +35,34 @@ Feature: Project Merge Requests
35 Then I should see merge request "Wiki Feature" 35 Then I should see merge request "Wiki Feature"
36 36
37 @javascript 37 @javascript
38 - Scenario: I comment merge request 38 + Scenario: I comment on a merge request
39 Given I visit merge request page "Bug NS-04" 39 Given I visit merge request page "Bug NS-04"
40 And I leave a comment like "XML attached" 40 And I leave a comment like "XML attached"
41 Then I should see comment "XML attached" 41 Then I should see comment "XML attached"
42 42
  43 + @javascript
  44 + Scenario: I comment on a merge request diff
  45 + Given project "Shop" have "Bug NS-05" open merge request with diffs inside
  46 + And I visit merge request page "Bug NS-05"
  47 + And I switch to the diff tab
  48 + And I leave a comment like "Line is wrong" on line 185 of the first file
  49 + And I switch to the merge request's comments tab
  50 + Then I should see a discussion has started on line 185
  51 +
  52 + @javascript
  53 + Scenario: I comment on a line of a commit in merge request
  54 + Given project "Shop" have "Bug NS-05" open merge request with diffs inside
  55 + And I visit merge request page "Bug NS-05"
  56 + And I click on the first commit in the merge request
  57 + And I leave a comment like "Line is wrong" on line 185 of the first file
  58 + And I switch to the merge request's comments tab
  59 + Then I should see a discussion has started on commit bcf03b5de6c:L185
  60 +
  61 + @javascript
  62 + Scenario: I comment on a commit in merge request
  63 + Given project "Shop" have "Bug NS-05" open merge request with diffs inside
  64 + And I visit merge request page "Bug NS-05"
  65 + And I click on the first commit in the merge request
  66 + And I leave a comment on the diff page
  67 + And I switch to the merge request's comments tab
  68 + Then I should see a discussion has started on commit bcf03b5de6c
features/steps/project/comments_on_commit_diffs.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +class CommentsOnCommitDiffs < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedDiffNote
  4 + include SharedPaths
  5 + include SharedProject
  6 +end
features/steps/project/comments_on_commits.rb 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +class CommentsOnCommits < Spinach::FeatureSteps
  2 + include SharedAuthentication
  3 + include SharedNote
  4 + include SharedPaths
  5 + include SharedProject
  6 +end
features/steps/project/project_comment_commit.rb
@@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
1 -class ProjectCommentCommit < Spinach::FeatureSteps  
2 - include SharedAuthentication  
3 - include SharedProject  
4 - include SharedNote  
5 - include SharedPaths  
6 -end  
features/steps/project/project_merge_requests.rb
@@ -4,50 +4,55 @@ class ProjectMergeRequests &lt; Spinach::FeatureSteps @@ -4,50 +4,55 @@ class ProjectMergeRequests &lt; Spinach::FeatureSteps
4 include SharedNote 4 include SharedNote
5 include SharedPaths 5 include SharedPaths
6 6
7 - Then 'I should see "Bug NS-04" in merge requests' do  
8 - page.should have_content "Bug NS-04" 7 + Given 'I click link "New Merge Request"' do
  8 + click_link "New Merge Request"
9 end 9 end
10 10
11 - And 'I should not see "Feature NS-03" in merge requests' do  
12 - page.should_not have_content "Feature NS-03" 11 + Given 'I click link "Bug NS-04"' do
  12 + click_link "Bug NS-04"
  13 + end
  14 +
  15 + Given 'I click link "All"' do
  16 + click_link "All"
13 end 17 end
14 18
15 Given 'I click link "Closed"' do 19 Given 'I click link "Closed"' do
16 click_link "Closed" 20 click_link "Closed"
17 end 21 end
18 22
19 - Then 'I should see "Feature NS-03" in merge requests' do  
20 - page.should have_content "Feature NS-03" 23 + Then 'I should see merge request "Wiki Feature"' do
  24 + page.should have_content "Wiki Feature"
21 end 25 end
22 26
23 - And 'I should not see "Bug NS-04" in merge requests' do  
24 - page.should_not have_content "Bug NS-04" 27 + Then 'I should see closed merge request "Bug NS-04"' do
  28 + mr = MergeRequest.find_by_title("Bug NS-04")
  29 + mr.closed.should be_true
  30 + page.should have_content "Closed by"
25 end 31 end
26 32
27 - Given 'I click link "All"' do  
28 - click_link "All" 33 + Then 'I should see merge request "Bug NS-04"' do
  34 + page.should have_content "Bug NS-04"
29 end 35 end
30 36
31 - Given 'I click link "Bug NS-04"' do  
32 - click_link "Bug NS-04" 37 + Then 'I should see "Bug NS-04" in merge requests' do
  38 + page.should have_content "Bug NS-04"
33 end 39 end
34 40
35 - Then 'I should see merge request "Bug NS-04"' do  
36 - page.should have_content "Bug NS-04" 41 + Then 'I should see "Feature NS-03" in merge requests' do
  42 + page.should have_content "Feature NS-03"
37 end 43 end
38 44
39 - And 'I click link "Close"' do  
40 - click_link "Close" 45 + And 'I should not see "Feature NS-03" in merge requests' do
  46 + page.should_not have_content "Feature NS-03"
41 end 47 end
42 48
43 - Then 'I should see closed merge request "Bug NS-04"' do  
44 - mr = MergeRequest.find_by_title("Bug NS-04")  
45 - mr.closed.should be_true  
46 - page.should have_content "Closed by" 49 +
  50 + And 'I should not see "Bug NS-04" in merge requests' do
  51 + page.should_not have_content "Bug NS-04"
47 end 52 end
48 53
49 - Given 'I click link "New Merge Request"' do  
50 - click_link "New Merge Request" 54 + And 'I click link "Close"' do
  55 + click_link "Close"
51 end 56 end
52 57
53 And 'I submit new merge request "Wiki Feature"' do 58 And 'I submit new merge request "Wiki Feature"' do
@@ -57,24 +62,91 @@ class ProjectMergeRequests &lt; Spinach::FeatureSteps @@ -57,24 +62,91 @@ class ProjectMergeRequests &lt; Spinach::FeatureSteps
57 click_button "Submit merge request" 62 click_button "Submit merge request"
58 end 63 end
59 64
60 - Then 'I should see merge request "Wiki Feature"' do  
61 - page.should have_content "Wiki Feature"  
62 - end  
63 -  
64 And 'project "Shop" have "Bug NS-04" open merge request' do 65 And 'project "Shop" have "Bug NS-04" open merge request' do
65 project = Project.find_by_name("Shop") 66 project = Project.find_by_name("Shop")
66 create(:merge_request, 67 create(:merge_request,
67 - :title => "Bug NS-04",  
68 - :project => project,  
69 - :author => project.users.first) 68 + title: "Bug NS-04",
  69 + project: project,
  70 + author: project.users.first)
  71 + end
  72 +
  73 + And 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
  74 + project = Project.find_by_name("Shop")
  75 + create(:merge_request_with_diffs,
  76 + title: "Bug NS-05",
  77 + project: project,
  78 + author: project.users.first)
70 end 79 end
71 80
72 And 'project "Shop" have "Feature NS-03" closed merge request' do 81 And 'project "Shop" have "Feature NS-03" closed merge request' do
73 project = Project.find_by_name("Shop") 82 project = Project.find_by_name("Shop")
74 create(:merge_request, 83 create(:merge_request,
75 - :title => "Feature NS-03",  
76 - :project => project,  
77 - :author => project.users.first,  
78 - :closed => true) 84 + title: "Feature NS-03",
  85 + project: project,
  86 + author: project.users.first,
  87 + closed: true)
  88 + end
  89 +
  90 + And 'I switch to the diff tab' do
  91 + mr = MergeRequest.find_by_title("Bug NS-05")
  92 + visit diffs_project_merge_request_path(mr.project, mr)
  93 + end
  94 +
  95 + And 'I switch to the merge request\'s comments tab' do
  96 + mr = MergeRequest.find_by_title("Bug NS-05")
  97 + visit project_merge_request_path(mr.project, mr)
  98 + end
  99 +
  100 + And 'I click on the first commit in the merge request' do
  101 + mr = MergeRequest.find_by_title("Bug NS-05")
  102 + click_link mr.commits.first.short_id(8)
  103 + end
  104 +
  105 + And 'I leave a comment on the diff page' do
  106 + within(:xpath, "//div[@class='note-form-holder']") do
  107 + fill_in "note_note", with: "One comment to rule them all"
  108 + click_button "Add Comment"
  109 + end
  110 + end
  111 +
  112 + And 'I leave a comment like "Line is wrong" on line 185 of the first file' do
  113 + save_and_open_page
  114 + within(:xpath, "//div[@class='diff_file'][1]") do
  115 + click_link "add-diff-line-note-0_185_185"
  116 + end
  117 +
  118 + within(:xpath, "//div[@class='line-note-form-holder']") do
  119 + fill_in "note_note", with: "Line is wrong"
  120 + click_button "Add Comment"
  121 + end
  122 + end
  123 +
  124 + Then 'I should see a discussion has started on line 185' do
  125 + mr = MergeRequest.find_by_title("Bug NS-05")
  126 + first_commit = mr.commits.first
  127 + first_diff = mr.diffs.first
  128 + page.should have_content "#{current_user.name} started a discussion on this merge request diff"
  129 + page.should have_content "#{first_diff.b_path}:L185"
  130 + page.should have_content "Line is wrong"
  131 + end
  132 +
  133 + Then 'I should see a discussion has started on commit bcf03b5de6c:L185' do
  134 + mr = MergeRequest.find_by_title("Bug NS-05")
  135 + first_commit = mr.commits.first
  136 + first_diff = mr.diffs.first
  137 + page.should have_content "#{current_user.name} started a discussion on commit"
  138 + page.should have_content first_commit.short_id(8)
  139 + page.should have_content "#{first_diff.b_path}:L185"
  140 + page.should have_content "Line is wrong"
  141 + end
  142 +
  143 + Then 'I should see a discussion has started on commit bcf03b5de6c' do
  144 + mr = MergeRequest.find_by_title("Bug NS-05")
  145 + first_commit = mr.st_commits.first
  146 + first_diff = mr.diffs.first
  147 + page.should have_content "#{current_user.name} started a discussion on commit"
  148 + page.should have_content first_commit.short_id(8)
  149 + page.should have_content "One comment to rule them all"
  150 + page.should_not have_content "#{first_diff.b_path}:L185"
79 end 151 end
80 end 152 end
features/steps/shared/diff_note.rb 0 → 100644
@@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
  1 +module SharedDiffNote
  2 + include Spinach::DSL
  3 +
  4 + Given 'I cancel the diff comment' do
  5 + within(".diff_file") do
  6 + find(".js-close-discussion-note-form").trigger("click")
  7 + end
  8 + end
  9 +
  10 + Given 'I delete a diff comment' do
  11 + within(".diff_file") do
  12 + first(".js-note-delete").trigger("click")
  13 + end
  14 + end
  15 +
  16 + Given 'I haven\'t written any diff comment text' do
  17 + within(".diff_file") do
  18 + fill_in "note[note]", with: ""
  19 + end
  20 + end
  21 +
  22 + Given 'I leave a diff comment like "Typo, please fix"' do
  23 + find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click")
  24 + within(".diff_file") do
  25 + fill_in "note[note]", with: "Typo, please fix"
  26 + #click_button("Add Comment")
  27 + find(".js-comment-button").trigger("click")
  28 + end
  29 + end
  30 +
  31 + Given 'I preview a diff comment text like "Should fix it :smile:"' do
  32 + find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click")
  33 + within(".diff_file") do
  34 + fill_in "note[note]", with: "Should fix it :smile:"
  35 + find(".js-note-preview-button").trigger("click")
  36 + end
  37 + end
  38 +
  39 + Given 'I preview another diff comment text like "DRY this up"' do
  40 + find("#586fb7c4e1add2d4d24e27566ed7064680098646_57_41.line_holder .js-add-diff-note-button").trigger("click")
  41 + within(".diff_file") do
  42 + fill_in "note[note]", with: "DRY this up"
  43 + find(".js-note-preview-button").trigger("click")
  44 + end
  45 + end
  46 +
  47 + Given 'I open a diff comment form' do
  48 + find("#586fb7c4e1add2d4d24e27566ed7064680098646_29_14.line_holder .js-add-diff-note-button").trigger("click")
  49 + end
  50 +
  51 + Given 'I open another diff comment form' do
  52 + find("#586fb7c4e1add2d4d24e27566ed7064680098646_57_41.line_holder .js-add-diff-note-button").trigger("click")
  53 + end
  54 +
  55 + Given 'I write a diff comment like ":-1: I don\'t like this"' do
  56 + within(".diff_file") do
  57 + fill_in "note[note]", with: ":-1: I don\'t like this"
  58 + end
  59 + end
  60 +
  61 + Given 'I submit the diff comment' do
  62 + within(".diff_file") do
  63 + click_button("Add Comment")
  64 + end
  65 + end
  66 +
  67 +
  68 +
  69 + Then 'I should not see the diff comment form' do
  70 + within(".diff_file") do
  71 + page.should_not have_css("form.new_note")
  72 + end
  73 + end
  74 +
  75 + Then 'I should not see the diff comment preview button' do
  76 + within(".diff_file") do
  77 + page.should have_css(".js-note-preview-button", visible: false)
  78 + end
  79 + end
  80 +
  81 + Then 'I should not see the diff comment text field' do
  82 + within(".diff_file") do
  83 + page.should have_css(".js-note-text", visible: false)
  84 + end
  85 + end
  86 +
  87 + Then 'I should only see one diff form' do
  88 + within(".diff_file") do
  89 + page.should have_css("form.new_note", count: 1)
  90 + end
  91 + end
  92 +
  93 + Then 'I should see a diff comment form with ":-1: I don\'t like this"' do
  94 + within(".diff_file") do
  95 + page.should have_field("note[note]", with: ":-1: I don\'t like this")
  96 + end
  97 + end
  98 +
  99 + Then 'I should see a diff comment saying "Typo, please fix"' do
  100 + within(".diff_file .note") do
  101 + page.should have_content("Typo, please fix")
  102 + end
  103 + end
  104 +
  105 + Then 'I should see a discussion reply button' do
  106 + within(".diff_file") do
  107 + page.should have_link("Reply")
  108 + end
  109 + end
  110 +
  111 + Then 'I should see a temporary diff comment form' do
  112 + within(".diff_file") do
  113 + page.should have_css(".js-temp-notes-holder form.new_note")
  114 + end
  115 + end
  116 +
  117 + Then 'I should see add a diff comment button' do
  118 + page.should have_css(".js-add-diff-note-button", visible: false)
  119 + end
  120 +
  121 + Then 'I should see an empty diff comment form' do
  122 + within(".diff_file") do
  123 + page.should have_field("note[note]", with: "")
  124 + end
  125 + end
  126 +
  127 + Then 'I should see the cancel comment button' do
  128 + within(".diff_file form") do
  129 + page.should have_css(".js-close-discussion-note-form", text: "Cancel")
  130 + end
  131 + end
  132 +
  133 + Then 'I should see the diff comment preview' do
  134 + within(".diff_file form") do
  135 + page.should have_css(".js-note-preview", visible: false)
  136 + end
  137 + end
  138 +
  139 + Then 'I should see the diff comment edit button' do
  140 + within(".diff_file") do
  141 + page.should have_css(".js-note-edit-button", visible: true)
  142 + end
  143 + end
  144 +
  145 + Then 'I should see the diff comment preview button' do
  146 + within(".diff_file") do
  147 + page.should have_css(".js-note-preview-button", visible: true)
  148 + end
  149 + end
  150 +
  151 + Then 'I should see two separate previews' do
  152 + within(".diff_file") do
  153 + page.should have_css(".js-note-preview", visible: true, count: 2)
  154 + page.should have_content("Should fix it")
  155 + page.should have_content("DRY this up")
  156 + end
  157 + end
  158 +end
features/steps/shared/note.rb
1 module SharedNote 1 module SharedNote
2 include Spinach::DSL 2 include Spinach::DSL
3 3
  4 + Given 'I delete a comment' do
  5 + first(".js-note-delete").trigger("click")
  6 + end
  7 +
  8 + Given 'I haven\'t written any comment text' do
  9 + within(".js-main-target-form") do
  10 + fill_in "note[note]", with: ""
  11 + end
  12 + end
  13 +
4 Given 'I leave a comment like "XML attached"' do 14 Given 'I leave a comment like "XML attached"' do
5 - fill_in "note_note", :with => "XML attached"  
6 - click_button "Add Comment" 15 + within(".js-main-target-form") do
  16 + fill_in "note[note]", with: "XML attached"
  17 + click_button "Add Comment"
  18 + end
7 end 19 end
8 20
9 - Then 'I should see comment "XML attached"' do  
10 - page.should have_content "XML attached" 21 + Given 'I preview a comment text like "Bug fixed :smile:"' do
  22 + within(".js-main-target-form") do
  23 + fill_in "note[note]", with: "Bug fixed :smile:"
  24 + find(".js-note-preview-button").trigger("click")
  25 + end
11 end 26 end
12 27
  28 + Given 'I submit the comment' do
  29 + within(".js-main-target-form") do
  30 + click_button "Add Comment"
  31 + end
  32 + end
  33 +
  34 + Given 'I write a comment like "Nice"' do
  35 + within(".js-main-target-form") do
  36 + fill_in "note[note]", with: "Nice"
  37 + end
  38 + end
  39 +
  40 +
  41 +
  42 + Then 'I should not see a comment saying "XML attached"' do
  43 + page.should_not have_css(".note")
  44 + end
  45 +
  46 + Then 'I should not see the cancel comment button' do
  47 + within(".js-main-target-form") do
  48 + should_not have_link("Cancel")
  49 + end
  50 + end
  51 +
  52 + Then 'I should not see the comment preview' do
  53 + within(".js-main-target-form") do
  54 + page.should have_css(".js-note-preview", visible: false)
  55 + end
  56 + end
  57 +
  58 + Then 'I should not see the comment preview button' do
  59 + within(".js-main-target-form") do
  60 + page.should have_css(".js-note-preview-button", visible: false)
  61 + end
  62 + end
  63 +
  64 + Then 'I should not see the comment text field' do
  65 + within(".js-main-target-form") do
  66 + page.should have_css(".js-note-text", visible: false)
  67 + end
  68 + end
  69 +
  70 + Then 'I should see a comment saying "XML attached"' do
  71 + within(".note") do
  72 + page.should have_content("XML attached")
  73 + end
  74 + end
  75 +
  76 + Then 'I should see an empty comment text field' do
  77 + within(".js-main-target-form") do
  78 + page.should have_field("note[note]", with: "")
  79 + end
  80 + end
  81 +
  82 + Then 'I should see the comment edit button' do
  83 + within(".js-main-target-form") do
  84 + page.should have_css(".js-note-edit-button", visible: true)
  85 + end
  86 + end
  87 +
  88 + Then 'I should see the comment preview' do
  89 + within(".js-main-target-form") do
  90 + page.should have_css(".js-note-preview", visible: true)
  91 + end
  92 + end
  93 +
  94 + Then 'I should see the comment preview button' do
  95 + within(".js-main-target-form") do
  96 + page.should have_css(".js-note-preview-button", visible: true)
  97 + end
  98 + end
  99 +
  100 +
  101 +
  102 + # Wall
  103 +
13 Given 'I write new comment "my special test message"' do 104 Given 'I write new comment "my special test message"' do
14 - fill_in "note_note", :with => "my special test message"  
15 - click_button "Add Comment" 105 + within(".js-main-target-form") do
  106 + fill_in "note[note]", with: "my special test message"
  107 + click_button "Add Comment"
  108 + end
16 end 109 end
17 110
18 Then 'I should see project wall note "my special test message"' do 111 Then 'I should see project wall note "my special test message"' do
features/steps/shared/paths.rb
@@ -224,6 +224,11 @@ module SharedPaths @@ -224,6 +224,11 @@ module SharedPaths
224 visit project_merge_request_path(mr.project, mr) 224 visit project_merge_request_path(mr.project, mr)
225 end 225 end
226 226
  227 + Given 'I visit merge request page "Bug NS-05"' do
  228 + mr = MergeRequest.find_by_title("Bug NS-05")
  229 + visit project_merge_request_path(mr.project, mr)
  230 + end
  231 +
227 And 'I visit project "Shop" merge requests page' do 232 And 'I visit project "Shop" merge requests page' do
228 visit project_merge_requests_path(Project.find_by_name("Shop")) 233 visit project_merge_requests_path(Project.find_by_name("Shop"))
229 end 234 end
lib/gitlab/markdown.rb
@@ -25,22 +25,6 @@ module Gitlab @@ -25,22 +25,6 @@ module Gitlab
25 # >> gfm(":trollface:") 25 # >> gfm(":trollface:")
26 # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" /> 26 # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
27 module Markdown 27 module Markdown
28 - REFERENCE_PATTERN = %r{  
29 - (?<prefix>\W)? # Prefix  
30 - ( # Reference  
31 - @(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User name  
32 - |\#(?<issue>\d+) # Issue ID  
33 - |!(?<merge_request>\d+) # MR ID  
34 - |\$(?<snippet>\d+) # Snippet ID  
35 - |(?<commit>[\h]{6,40}) # Commit ID  
36 - )  
37 - (?<suffix>\W)? # Suffix  
38 - }x.freeze  
39 -  
40 - TYPES = [:user, :issue, :merge_request, :snippet, :commit].freeze  
41 -  
42 - EMOJI_PATTERN = %r{(:(\S+):)}.freeze  
43 -  
44 attr_reader :html_options 28 attr_reader :html_options
45 29
46 # Public: Parse the provided text with GitLab-Flavored Markdown 30 # Public: Parse the provided text with GitLab-Flavored Markdown
@@ -96,6 +80,20 @@ module Gitlab @@ -96,6 +80,20 @@ module Gitlab
96 text 80 text
97 end 81 end
98 82
  83 + REFERENCE_PATTERN = %r{
  84 + (?<prefix>\W)? # Prefix
  85 + ( # Reference
  86 + @(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User name
  87 + |\#(?<issue>\d+) # Issue ID
  88 + |!(?<merge_request>\d+) # MR ID
  89 + |\$(?<snippet>\d+) # Snippet ID
  90 + |(?<commit>[\h]{6,40}) # Commit ID
  91 + )
  92 + (?<suffix>\W)? # Suffix
  93 + }x.freeze
  94 +
  95 + TYPES = [:user, :issue, :merge_request, :snippet, :commit].freeze
  96 +
99 def parse_references(text) 97 def parse_references(text)
100 # parse reference links 98 # parse reference links
101 text.gsub!(REFERENCE_PATTERN) do |match| 99 text.gsub!(REFERENCE_PATTERN) do |match|
@@ -115,11 +113,13 @@ module Gitlab @@ -115,11 +113,13 @@ module Gitlab
115 end 113 end
116 end 114 end
117 115
  116 + EMOJI_PATTERN = %r{(:(\S+):)}.freeze
  117 +
118 def parse_emoji(text) 118 def parse_emoji(text)
119 # parse emoji 119 # parse emoji
120 text.gsub!(EMOJI_PATTERN) do |match| 120 text.gsub!(EMOJI_PATTERN) do |match|
121 if valid_emoji?($2) 121 if valid_emoji?($2)
122 - image_tag("emoji/#{$2}.png", size: "20x20", class: 'emoji', title: $1, alt: $1) 122 + image_tag("emoji/#{$2}.png", class: 'emoji', title: $1, alt: $1, size: "20x20")
123 else 123 else
124 match 124 match
125 end 125 end
spec/factories.rb
@@ -73,8 +73,8 @@ FactoryGirl.define do @@ -73,8 +73,8 @@ FactoryGirl.define do
73 73
74 # pick 3 commits "at random" (from bcf03b5d~3 to bcf03b5d) 74 # pick 3 commits "at random" (from bcf03b5d~3 to bcf03b5d)
75 trait :with_diffs do 75 trait :with_diffs do
76 - target_branch "bcf03b5d~3"  
77 - source_branch "bcf03b5d" 76 + target_branch "master" # pretend bcf03b5d~3
  77 + source_branch "stable" # pretend bcf03b5d
78 st_commits do 78 st_commits do
79 [Commit.new(project.repo.commit('bcf03b5d')), 79 [Commit.new(project.repo.commit('bcf03b5d')),
80 Commit.new(project.repo.commit('bcf03b5d~1')), 80 Commit.new(project.repo.commit('bcf03b5d~1')),
@@ -92,6 +92,32 @@ FactoryGirl.define do @@ -92,6 +92,32 @@ FactoryGirl.define do
92 factory :note do 92 factory :note do
93 project 93 project
94 note "Note" 94 note "Note"
  95 + author
  96 +
  97 + factory :note_on_commit, traits: [:on_commit]
  98 + factory :note_on_commit_diff, traits: [:on_commit, :on_diff]
  99 + factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
  100 + factory :note_on_merge_request, traits: [:on_merge_request]
  101 + factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff]
  102 +
  103 + trait :on_commit do
  104 + commit_id "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a"
  105 + noteable_type "Commit"
  106 + end
  107 +
  108 + trait :on_diff do
  109 + line_code "0_184_184"
  110 + end
  111 +
  112 + trait :on_merge_request do
  113 + noteable_id 1
  114 + noteable_type "MergeRequest"
  115 + end
  116 +
  117 + trait :on_issue do
  118 + noteable_id 1
  119 + noteable_type "Issue"
  120 + end
95 end 121 end
96 122
97 factory :event do 123 factory :event do
spec/lib/votes_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe Issue do 3 describe Issue do
4 - let(:issue) { create(:issue) } 4 + it { should include_module(Votes) }
  5 +end
  6 +
  7 +describe MergeRequest do
  8 + let(:merge_request) { FactoryGirl.create(:merge_request_with_diffs) }
  9 +
  10 + it { should include_module(Votes) }
5 11
6 describe "#upvotes" do 12 describe "#upvotes" do
7 it "with no notes has a 0/0 score" do 13 it "with no notes has a 0/0 score" do
8 - issue.upvotes.should == 0 14 + merge_request.upvotes.should == 0
9 end 15 end
10 16
11 it "should recognize non-+1 notes" do 17 it "should recognize non-+1 notes" do
12 - issue.notes << create(:note, note: "No +1 here")  
13 - issue.should have(1).note  
14 - issue.notes.first.upvote?.should be_false  
15 - issue.upvotes.should == 0 18 + merge_request.notes << create(:note, note: "No +1 here")
  19 + merge_request.should have(1).note
  20 + merge_request.notes.first.upvote?.should be_false
  21 + merge_request.upvotes.should == 0
16 end 22 end
17 23
18 it "should recognize a single +1 note" do 24 it "should recognize a single +1 note" do
19 - issue.notes << create(:note, note: "+1 This is awesome")  
20 - issue.upvotes.should == 1 25 + merge_request.notes << create(:note, note: "+1 This is awesome")
  26 + merge_request.upvotes.should == 1
21 end 27 end
22 28
23 it "should recognize multiple +1 notes" do 29 it "should recognize multiple +1 notes" do
24 - issue.notes << create(:note, note: "+1 This is awesome")  
25 - issue.notes << create(:note, note: "+1 I want this")  
26 - issue.upvotes.should == 2 30 + merge_request.notes << create(:note, note: "+1 This is awesome")
  31 + merge_request.notes << create(:note, note: "+1 I want this")
  32 + merge_request.upvotes.should == 2
27 end 33 end
28 end 34 end
29 35
30 describe "#downvotes" do 36 describe "#downvotes" do
31 it "with no notes has a 0/0 score" do 37 it "with no notes has a 0/0 score" do
32 - issue.downvotes.should == 0 38 + merge_request.downvotes.should == 0
33 end 39 end
34 40
35 it "should recognize non--1 notes" do 41 it "should recognize non--1 notes" do
36 - issue.notes << create(:note, note: "Almost got a -1")  
37 - issue.should have(1).note  
38 - issue.notes.first.downvote?.should be_false  
39 - issue.downvotes.should == 0 42 + merge_request.notes << create(:note, note: "Almost got a -1")
  43 + merge_request.should have(1).note
  44 + merge_request.notes.first.downvote?.should be_false
  45 + merge_request.downvotes.should == 0
40 end 46 end
41 47
42 it "should recognize a single -1 note" do 48 it "should recognize a single -1 note" do
43 - issue.notes << create(:note, note: "-1 This is bad")  
44 - issue.downvotes.should == 1 49 + merge_request.notes << create(:note, note: "-1 This is bad")
  50 + merge_request.downvotes.should == 1
45 end 51 end
46 52
47 it "should recognize multiple -1 notes" do 53 it "should recognize multiple -1 notes" do
48 - issue.notes << create(:note, note: "-1 This is bad")  
49 - issue.notes << create(:note, note: "-1 Away with this")  
50 - issue.downvotes.should == 2 54 + merge_request.notes << create(:note, note: "-1 This is bad")
  55 + merge_request.notes << create(:note, note: "-1 Away with this")
  56 + merge_request.downvotes.should == 2
51 end 57 end
52 end 58 end
53 59
54 describe "#votes_count" do 60 describe "#votes_count" do
55 it "with no notes has a 0/0 score" do 61 it "with no notes has a 0/0 score" do
56 - issue.votes_count.should == 0 62 + merge_request.votes_count.should == 0
57 end 63 end
58 64
59 it "should recognize non notes" do 65 it "should recognize non notes" do
60 - issue.notes << create(:note, note: "No +1 here")  
61 - issue.should have(1).note  
62 - issue.votes_count.should == 0 66 + merge_request.notes << create(:note, note: "No +1 here")
  67 + merge_request.should have(1).note
  68 + merge_request.votes_count.should == 0
63 end 69 end
64 70
65 it "should recognize a single +1 note" do 71 it "should recognize a single +1 note" do
66 - issue.notes << create(:note, note: "+1 This is awesome")  
67 - issue.votes_count.should == 1 72 + merge_request.notes << create(:note, note: "+1 This is awesome")
  73 + merge_request.votes_count.should == 1
68 end 74 end
69 75
70 it "should recognize a single -1 note" do 76 it "should recognize a single -1 note" do
71 - issue.notes << create(:note, note: "-1 This is bad")  
72 - issue.votes_count.should == 1 77 + merge_request.notes << create(:note, note: "-1 This is bad")
  78 + merge_request.votes_count.should == 1
73 end 79 end
74 80
75 it "should recognize multiple notes" do 81 it "should recognize multiple notes" do
76 - issue.notes << create(:note, note: "+1 This is awesome")  
77 - issue.notes << create(:note, note: "-1 This is bad")  
78 - issue.notes << create(:note, note: "+1 I want this")  
79 - issue.votes_count.should == 3 82 + merge_request.notes << create(:note, note: "+1 This is awesome")
  83 + merge_request.notes << create(:note, note: "-1 This is bad")
  84 + merge_request.notes << create(:note, note: "+1 I want this")
  85 + merge_request.votes_count.should == 3
80 end 86 end
81 end 87 end
82 88
83 describe "#upvotes_in_percent" do 89 describe "#upvotes_in_percent" do
84 it "with no notes has a 0% score" do 90 it "with no notes has a 0% score" do
85 - issue.upvotes_in_percent.should == 0 91 + merge_request.upvotes_in_percent.should == 0
86 end 92 end
87 93
88 it "should count a single 1 note as 100%" do 94 it "should count a single 1 note as 100%" do
89 - issue.notes << create(:note, note: "+1 This is awesome")  
90 - issue.upvotes_in_percent.should == 100 95 + merge_request.notes << create(:note, note: "+1 This is awesome")
  96 + merge_request.upvotes_in_percent.should == 100
91 end 97 end
92 98
93 it "should count multiple +1 notes as 100%" do 99 it "should count multiple +1 notes as 100%" do
94 - issue.notes << create(:note, note: "+1 This is awesome")  
95 - issue.notes << create(:note, note: "+1 I want this")  
96 - issue.upvotes_in_percent.should == 100 100 + merge_request.notes << create(:note, note: "+1 This is awesome")
  101 + merge_request.notes << create(:note, note: "+1 I want this")
  102 + merge_request.upvotes_in_percent.should == 100
97 end 103 end
98 104
99 it "should count fractions for multiple +1 and -1 notes correctly" do 105 it "should count fractions for multiple +1 and -1 notes correctly" do
100 - issue.notes << create(:note, note: "+1 This is awesome")  
101 - issue.notes << create(:note, note: "+1 I want this")  
102 - issue.notes << create(:note, note: "-1 This is bad")  
103 - issue.notes << create(:note, note: "+1 me too")  
104 - issue.upvotes_in_percent.should == 75 106 + merge_request.notes << create(:note, note: "+1 This is awesome")
  107 + merge_request.notes << create(:note, note: "+1 I want this")
  108 + merge_request.notes << create(:note, note: "-1 This is bad")
  109 + merge_request.notes << create(:note, note: "+1 me too")
  110 + merge_request.upvotes_in_percent.should == 75
105 end 111 end
106 end 112 end
107 113
108 describe "#downvotes_in_percent" do 114 describe "#downvotes_in_percent" do
109 it "with no notes has a 0% score" do 115 it "with no notes has a 0% score" do
110 - issue.downvotes_in_percent.should == 0 116 + merge_request.downvotes_in_percent.should == 0
111 end 117 end
112 118
113 it "should count a single -1 note as 100%" do 119 it "should count a single -1 note as 100%" do
114 - issue.notes << create(:note, note: "-1 This is bad")  
115 - issue.downvotes_in_percent.should == 100 120 + merge_request.notes << create(:note, note: "-1 This is bad")
  121 + merge_request.downvotes_in_percent.should == 100
116 end 122 end
117 123
118 it "should count multiple -1 notes as 100%" do 124 it "should count multiple -1 notes as 100%" do
119 - issue.notes << create(:note, note: "-1 This is bad")  
120 - issue.notes << create(:note, note: "-1 Away with this")  
121 - issue.downvotes_in_percent.should == 100 125 + merge_request.notes << create(:note, note: "-1 This is bad")
  126 + merge_request.notes << create(:note, note: "-1 Away with this")
  127 + merge_request.downvotes_in_percent.should == 100
122 end 128 end
123 129
124 it "should count fractions for multiple +1 and -1 notes correctly" do 130 it "should count fractions for multiple +1 and -1 notes correctly" do
125 - issue.notes << create(:note, note: "+1 This is awesome")  
126 - issue.notes << create(:note, note: "+1 I want this")  
127 - issue.notes << create(:note, note: "-1 This is bad")  
128 - issue.notes << create(:note, note: "+1 me too")  
129 - issue.downvotes_in_percent.should == 25 131 + merge_request.notes << create(:note, note: "+1 This is awesome")
  132 + merge_request.notes << create(:note, note: "+1 I want this")
  133 + merge_request.notes << create(:note, note: "-1 This is bad")
  134 + merge_request.notes << create(:note, note: "+1 me too")
  135 + merge_request.downvotes_in_percent.should == 25
130 end 136 end
131 end 137 end
132 end 138 end
spec/models/note_spec.rb
@@ -38,34 +38,34 @@ describe Note do @@ -38,34 +38,34 @@ describe Note do
38 let(:project) { create(:project) } 38 let(:project) { create(:project) }
39 39
40 it "recognizes a neutral note" do 40 it "recognizes a neutral note" do
41 - note = create(:note, note: "This is not a +1 note") 41 + note = create(:votable_note, note: "This is not a +1 note")
42 note.should_not be_upvote 42 note.should_not be_upvote
43 note.should_not be_downvote 43 note.should_not be_downvote
44 end 44 end
45 45
46 it "recognizes a neutral emoji note" do 46 it "recognizes a neutral emoji note" do
47 - note = build(:note, note: "I would :+1: this, but I don't want to") 47 + note = build(:votable_note, note: "I would :+1: this, but I don't want to")
48 note.should_not be_upvote 48 note.should_not be_upvote
49 note.should_not be_downvote 49 note.should_not be_downvote
50 end 50 end
51 51
52 it "recognizes a +1 note" do 52 it "recognizes a +1 note" do
53 - note = create(:note, note: "+1 for this") 53 + note = create(:votable_note, note: "+1 for this")
54 note.should be_upvote 54 note.should be_upvote
55 end 55 end
56 56
57 it "recognizes a +1 emoji as a vote" do 57 it "recognizes a +1 emoji as a vote" do
58 - note = build(:note, note: ":+1: for this") 58 + note = build(:votable_note, note: ":+1: for this")
59 note.should be_upvote 59 note.should be_upvote
60 end 60 end
61 61
62 it "recognizes a -1 note" do 62 it "recognizes a -1 note" do
63 - note = create(:note, note: "-1 for this") 63 + note = create(:votable_note, note: "-1 for this")
64 note.should be_downvote 64 note.should be_downvote
65 end 65 end
66 66
67 it "recognizes a -1 emoji as a vote" do 67 it "recognizes a -1 emoji as a vote" do
68 - note = build(:note, note: ":-1: for this") 68 + note = build(:votable_note, note: ":-1: for this")
69 note.should be_downvote 69 note.should be_downvote
70 end 70 end
71 end 71 end
@@ -74,43 +74,72 @@ describe Note do @@ -74,43 +74,72 @@ describe Note do
74 let(:commit) { project.repository.commit } 74 let(:commit) { project.repository.commit }
75 75
76 describe "Commit notes" do 76 describe "Commit notes" do
77 - before do  
78 - @note = create(:note,  
79 - commit_id: commit.id,  
80 - noteable_type: "Commit")  
81 - end 77 + let!(:note) { create(:note_on_commit, note: "+1 from me") }
  78 + let!(:commit) { note.noteable }
82 79
83 it "should be accessible through #noteable" do 80 it "should be accessible through #noteable" do
84 - @note.commit_id.should == commit.id  
85 - @note.noteable.should be_a(Commit)  
86 - @note.noteable.should == commit 81 + note.commit_id.should == commit.id
  82 + note.noteable.should be_a(Commit)
  83 + note.noteable.should == commit
87 end 84 end
88 85
89 it "should save a valid note" do 86 it "should save a valid note" do
90 - @note.commit_id.should == commit.id  
91 - @note.noteable == commit 87 + note.commit_id.should == commit.id
  88 + note.noteable == commit
92 end 89 end
93 90
94 it "should be recognized by #for_commit?" do 91 it "should be recognized by #for_commit?" do
95 - @note.should be_for_commit 92 + note.should be_for_commit
96 end 93 end
97 - end  
98 94
99 - describe "Pre-line commit notes" do  
100 - before do  
101 - @note = create(:note,  
102 - commit_id: commit.id,  
103 - noteable_type: "Commit",  
104 - line_code: "0_16_1") 95 + it "should not be votable" do
  96 + note.should_not be_votable
105 end 97 end
  98 + end
  99 +
  100 + describe "Commit diff line notes" do
  101 + let!(:note) { create(:note_on_commit_line, note: "+1 from me") }
  102 + let!(:commit) { note.noteable }
106 103
107 it "should save a valid note" do 104 it "should save a valid note" do
108 - @note.commit_id.should == commit.id  
109 - @note.noteable.id.should == commit.id 105 + note.commit_id.should == commit.id
  106 + note.noteable.id.should == commit.id
110 end 107 end
111 108
112 it "should be recognized by #for_diff_line?" do 109 it "should be recognized by #for_diff_line?" do
113 - @note.should be_for_diff_line 110 + note.should be_for_diff_line
  111 + end
  112 +
  113 + it "should be recognized by #for_commit_diff_line?" do
  114 + note.should be_for_commit_diff_line
  115 + end
  116 +
  117 + it "should not be votable" do
  118 + note.should_not be_votable
  119 + end
  120 + end
  121 +
  122 + describe "Issue notes" do
  123 + let!(:note) { create(:note_on_issue, note: "+1 from me") }
  124 +
  125 + it "should not be votable" do
  126 + note.should be_votable
  127 + end
  128 + end
  129 +
  130 + describe "Merge request notes" do
  131 + let!(:note) { create(:note_on_merge_request, note: "+1 from me") }
  132 +
  133 + it "should not be votable" do
  134 + note.should be_votable
  135 + end
  136 + end
  137 +
  138 + describe "Merge request diff line notes" do
  139 + let!(:note) { create(:note_on_merge_request_line, note: "+1 from me") }
  140 +
  141 + it "should not be votable" do
  142 + note.should_not be_votable
114 end 143 end
115 end 144 end
116 145
spec/requests/notes_on_merge_requests_spec.rb 0 → 100644
@@ -0,0 +1,233 @@ @@ -0,0 +1,233 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "On a merge request", js: true do
  4 + let!(:project) { create(:project) }
  5 + let!(:merge_request) { create(:merge_request, project: project) }
  6 +
  7 + before do
  8 + login_as :user
  9 + project.team << [@user, :master]
  10 +
  11 + visit project_merge_request_path(project, merge_request)
  12 + end
  13 +
  14 + subject { page }
  15 +
  16 + describe "the note form" do
  17 + # main target form creation
  18 + it { should have_css(".js-main-target-form", visible: true, count: 1) }
  19 +
  20 + # button initalization
  21 + it { within(".js-main-target-form") { should have_button("Add Comment") } }
  22 + it { within(".js-main-target-form") { should_not have_link("Cancel") } }
  23 +
  24 + # notifiactions
  25 + it { within(".js-main-target-form") { should have_checked_field("Project team") } }
  26 + it { within(".js-main-target-form") { should_not have_checked_field("Commit author") } }
  27 + it { within(".js-main-target-form") { should_not have_unchecked_field("Commit author") } }
  28 +
  29 + describe "without text" do
  30 + it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
  31 + end
  32 +
  33 + describe "with text" do
  34 + before do
  35 + within(".js-main-target-form") do
  36 + fill_in "note[note]", with: "This is awesome"
  37 + end
  38 + end
  39 +
  40 + it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } }
  41 +
  42 + it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } }
  43 + end
  44 +
  45 + describe "with preview" do
  46 + before do
  47 + within(".js-main-target-form") do
  48 + fill_in "note[note]", with: "This is awesome"
  49 + find(".js-note-preview-button").trigger("click")
  50 + end
  51 + end
  52 +
  53 + it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } }
  54 +
  55 + it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
  56 + it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } }
  57 + end
  58 + end
  59 +
  60 + describe "when posting a note" do
  61 + before do
  62 + within(".js-main-target-form") do
  63 + fill_in "note[note]", with: "This is awsome!"
  64 + find(".js-note-preview-button").trigger("click")
  65 + click_button "Add Comment"
  66 + end
  67 + end
  68 +
  69 + # note added
  70 + it { within(".js-main-target-form") { should have_content("This is awsome!") } }
  71 +
  72 + # reset form
  73 + it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } }
  74 +
  75 + # return from preview
  76 + it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } }
  77 + it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } }
  78 +
  79 +
  80 + it "should be removable" do
  81 + find(".js-note-delete").trigger("click")
  82 +
  83 + should_not have_css(".note")
  84 + end
  85 + end
  86 +end
  87 +
  88 +
  89 +
  90 +describe "On a merge request diff", js: true, focus: true do
  91 + let!(:project) { create(:project) }
  92 + let!(:merge_request) { create(:merge_request_with_diffs, project: project) }
  93 +
  94 + before do
  95 + login_as :user
  96 + project.team << [@user, :master]
  97 +
  98 + visit diffs_project_merge_request_path(project, merge_request)
  99 +
  100 + click_link("Diff")
  101 + end
  102 +
  103 + subject { page }
  104 +
  105 + describe "when adding a note" do
  106 + before do
  107 + find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click")
  108 + end
  109 +
  110 + describe "the notes holder" do
  111 + it { should have_css("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") }
  112 +
  113 + it { within(".js-temp-notes-holder") { should have_css(".new_note") } }
  114 + end
  115 +
  116 + describe "the note form" do
  117 + # set up hidden fields correctly
  118 + it { within(".js-temp-notes-holder") { find("#note_noteable_type").value.should == "MergeRequest" } }
  119 + it { within(".js-temp-notes-holder") { find("#note_noteable_id").value.should == "" } }
  120 + it { within(".js-temp-notes-holder") { find("#note_commit_id").value.should == "bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a" } }
  121 + it { within(".js-temp-notes-holder") { find("#note_line_code").value.should == "4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185" } }
  122 +
  123 + # buttons
  124 + it { should have_button("Add Comment") }
  125 + it { should have_css(".js-close-discussion-note-form", text: "Cancel") }
  126 +
  127 + # notification options
  128 + it { should have_unchecked_field("Project team") }
  129 + it { should have_checked_field("Commit author") }
  130 +
  131 + it "shouldn't add a second form for same row" do
  132 + find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click")
  133 +
  134 + should have_css("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder form", count: 1)
  135 + end
  136 +
  137 + it "should be removed when canceled" do
  138 + find(".js-close-discussion-note-form").trigger("click")
  139 +
  140 + should have_no_css(".js-temp-notes-holder")
  141 + end
  142 + end
  143 + end
  144 +
  145 + describe "with muliple note forms" do
  146 + before do
  147 + find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click")
  148 + find("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder .js-add-diff-note-button").trigger("click")
  149 + end
  150 +
  151 + # has two line forms
  152 + it { should have_css(".js-temp-notes-holder", count: 2) }
  153 +
  154 + describe "previewing them separately" do
  155 + before do
  156 + # add two separate texts and trigger previews on both
  157 + within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") do
  158 + fill_in "note[note]", with: "One comment on line 185"
  159 + find(".js-note-preview-button").trigger("click")
  160 + end
  161 + within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") do
  162 + fill_in "note[note]", with: "Another comment on line 17"
  163 + find(".js-note-preview-button").trigger("click")
  164 + end
  165 + end
  166 +
  167 + # check if previews were rendered separately
  168 + it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder + .js-temp-notes-holder") { should have_css(".js-note-preview", text: "One comment on line 185") } }
  169 + it { within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") { should have_css(".js-note-preview", text: "Another comment on line 17") } }
  170 + end
  171 +
  172 + describe "posting a note" do
  173 + before do
  174 + within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") do
  175 + fill_in "note[note]", with: "Another comment on line 17"
  176 + click_button("Add Comment")
  177 + end
  178 + end
  179 +
  180 + # removed form after submit
  181 + it { should have_no_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .js-temp-notes-holder") }
  182 +
  183 + # added discussion
  184 + it { should have_content("Another comment on line 17") }
  185 + it { should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder") }
  186 + it { should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder .note", count: 1) }
  187 + it { should have_link("Reply") }
  188 +
  189 + it "should remove last note of a discussion" do
  190 + within("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + .notes_holder") do
  191 + find(".js-note-delete").trigger("click")
  192 + end
  193 +
  194 + # removed whole discussion
  195 + should_not have_css(".note_holder")
  196 + should have_css("#342e16cbbd482ac2047dc679b2749d248cc1428f_18_17.line_holder + #342e16cbbd482ac2047dc679b2749d248cc1428f_18_18.line_holder")
  197 + end
  198 + end
  199 + end
  200 +
  201 + describe "when replying to a note" do
  202 + before do
  203 + # create first note
  204 + find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder .js-add-diff-note-button").trigger("click")
  205 + within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .js-temp-notes-holder") do
  206 + fill_in "note[note]", with: "One comment on line 184"
  207 + click_button("Add Comment")
  208 + end
  209 + # create second note
  210 + within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") do
  211 + find(".js-discussion-reply-button").trigger("click")
  212 + fill_in "note[note]", with: "An additional comment in reply"
  213 + click_button("Add Comment")
  214 + end
  215 + end
  216 +
  217 + # inserted note
  218 + it { should have_content("An additional comment in reply") }
  219 + it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_css(".note", count: 2) } }
  220 +
  221 + # removed form after reply
  222 + it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_no_css("form") } }
  223 + it { within("#4735dfc552ad7bf15ca468adc3cad9d05b624490_184_184.line_holder + .notes_holder") { should have_link("Reply") } }
  224 + end
  225 +end
  226 +
  227 +
  228 +
  229 +describe "On merge request discussion", js: true do
  230 + describe "with merge request diff note"
  231 + describe "with commit note"
  232 + describe "with commit diff note"
  233 +end
spec/requests/notes_on_wall_spec.rb 0 → 100644
@@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "On the project wall", js: true do
  4 + let!(:project) { create(:project) }
  5 + let!(:commit) { project.commit("bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a") }
  6 +
  7 + before do
  8 + login_as :user
  9 + project.team << [@user, :master]
  10 + visit wall_project_path(project)
  11 + end
  12 +
  13 + subject { page }
  14 +
  15 + describe "the note form" do
  16 + # main target form creation
  17 + it { should have_css(".js-main-target-form", visible: true, count: 1) }
  18 +
  19 + # button initalization
  20 + it { within(".js-main-target-form") { should have_button("Add Comment") } }
  21 + it { within(".js-main-target-form") { should_not have_link("Cancel") } }
  22 +
  23 + # notifiactions
  24 + it { within(".js-main-target-form") { should have_checked_field("Project team") } }
  25 + it { within(".js-main-target-form") { should_not have_checked_field("Commit author") } }
  26 + it { within(".js-main-target-form") { should_not have_unchecked_field("Commit author") } }
  27 +
  28 + describe "without text" do
  29 + it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
  30 + end
  31 +
  32 + describe "with text" do
  33 + before do
  34 + within(".js-main-target-form") do
  35 + fill_in "note[note]", with: "This is awesome"
  36 + end
  37 + end
  38 +
  39 + it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } }
  40 +
  41 + it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } }
  42 + end
  43 +
  44 + describe "with preview" do
  45 + before do
  46 + within(".js-main-target-form") do
  47 + fill_in "note[note]", with: "This is awesome"
  48 + find(".js-note-preview-button").trigger("click")
  49 + end
  50 + end
  51 +
  52 + it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } }
  53 +
  54 + it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }
  55 + it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } }
  56 + end
  57 + end
  58 +
  59 + describe "when posting a note" do
  60 + before do
  61 + within(".js-main-target-form") do
  62 + fill_in "note[note]", with: "This is awsome!"
  63 + find(".js-note-preview-button").trigger("click")
  64 + click_button "Add Comment"
  65 + end
  66 + end
  67 +
  68 + # note added
  69 + it { within(".js-main-target-form") { should have_content("This is awsome!") } }
  70 +
  71 + # reset form
  72 + it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } }
  73 +
  74 + # return from preview
  75 + it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } }
  76 + it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } }
  77 +
  78 +
  79 + it "should be removable" do
  80 + find(".js-note-delete").trigger("click")
  81 +
  82 + should_not have_css(".note")
  83 + end
  84 + end
  85 +end