Commit 6ddacaf9bfe02378e610ce277cd901097dcf6fdd
Exists in
master
and in
4 other branches
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
691 Bytes
app/assets/javascripts/behaviors/details_behavior.coffee
0 → 100644
app/assets/javascripts/behaviors/toggler_behavior.coffee
0 → 100644
app/assets/javascripts/notes.js
... | ... | @@ -9,72 +9,315 @@ var NoteList = { |
9 | 9 | loading_more_disabled: false, |
10 | 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 | 329 | /** |
87 | 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 | 343 | * Called in response to getContent(). |
102 | 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 | 353 | // init infinite scrolling |
111 | - this.initLoadMore(); | |
354 | + NoteList.initLoadMore(); | |
112 | 355 | |
113 | 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 | 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 | 366 | * Set up refreshing only new notes after all notes have been loaded. |
125 | 367 | */ |
... | ... | @@ -128,65 +370,57 @@ var NoteList = { |
128 | 370 | /** |
129 | 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 | 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 | 403 | * Called in response to getMore(). |
164 | 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 | 415 | * Called in response to getMore(). |
176 | 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 | 428 | * |
195 | 429 | * New notes are all notes that are created after the site has been loaded. |
196 | 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 | 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 | 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 | 458 | * Called in response to getNew(). |
223 | 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 | 545 | * Recalculates the votes and updates them (if they are displayed at all). |
... | ... | @@ -249,67 +549,24 @@ var NoteList = { |
249 | 549 | * Might produce inaccurate results when not all notes have been loaded and a |
250 | 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
... | ... | @@ -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
app/assets/stylesheets/sections/commits.scss
... | ... | @@ -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 | 130 | padding: 0px 5px; |
129 | 131 | border-right: 1px solid #ccc; |
130 | 132 | text-align: right; |
... | ... | @@ -134,6 +136,7 @@ |
134 | 136 | moz-user-select: none; |
135 | 137 | -khtml-user-select: none; |
136 | 138 | user-select: none; |
139 | + | |
137 | 140 | a { |
138 | 141 | float: left; |
139 | 142 | width: 35px; | ... | ... |
app/assets/stylesheets/sections/notes.scss
1 | 1 | /** |
2 | 2 | * Notes |
3 | - * | |
4 | 3 | */ |
5 | -#notes-list, | |
6 | -#new-notes-list { | |
4 | +ul.notes { | |
7 | 5 | display: block; |
8 | 6 | list-style: none; |
9 | 7 | margin: 0px; |
10 | 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 | 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 | 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 | 3 | def execute |
4 | 4 | note = project.notes.new(params[:note]) |
5 | 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 | 8 | note.save |
9 | 9 | note |
10 | 10 | end | ... | ... |
app/contexts/notes/load_context.rb
... | ... | @@ -9,11 +9,11 @@ module Notes |
9 | 9 | |
10 | 10 | @notes = case target_type |
11 | 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 | 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 | 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 | 17 | when "snippet" |
18 | 18 | project.snippets.find(target_id).notes.fresh |
19 | 19 | when "wall" | ... | ... |
app/controllers/commit_controller.rb
... | ... | @@ -13,11 +13,17 @@ class CommitController < ProjectResourceController |
13 | 13 | @commit = result[:commit] |
14 | 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 | 28 | respond_to do |format| |
23 | 29 | format.html do | ... | ... |
app/controllers/issues_controller.rb
app/controllers/merge_requests_controller.rb
... | ... | @@ -18,6 +18,9 @@ class MergeRequestsController < ProjectResourceController |
18 | 18 | end |
19 | 19 | |
20 | 20 | def show |
21 | + @target_type = :merge_request | |
22 | + @target_id = @merge_request.id | |
23 | + | |
21 | 24 | respond_to do |format| |
22 | 25 | format.html |
23 | 26 | format.js |
... | ... | @@ -31,7 +34,9 @@ class MergeRequestsController < ProjectResourceController |
31 | 34 | @diffs = @merge_request.diffs |
32 | 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 | 40 | @line_notes = @merge_request.notes.where("line_code is not null") |
36 | 41 | end |
37 | 42 | ... | ... |
app/controllers/notes_controller.rb
... | ... | @@ -6,10 +6,12 @@ class NotesController < ProjectResourceController |
6 | 6 | respond_to :js |
7 | 7 | |
8 | 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 | 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 | 15 | end |
14 | 16 | |
15 | 17 | respond_with(@notes) |
... | ... | @@ -17,6 +19,8 @@ class NotesController < ProjectResourceController |
17 | 19 | |
18 | 20 | def create |
19 | 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 | 25 | respond_to do |format| |
22 | 26 | format.html {redirect_to :back} |
... | ... | @@ -40,7 +44,34 @@ class NotesController < ProjectResourceController |
40 | 44 | |
41 | 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 | 76 | end |
46 | 77 | end | ... | ... |
app/controllers/projects_controller.rb
... | ... | @@ -80,7 +80,10 @@ class ProjectsController < ProjectResourceController |
80 | 80 | |
81 | 81 | def wall |
82 | 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 | 88 | respond_to do |format| |
86 | 89 | format.html | ... | ... |
app/controllers/snippets_controller.rb
app/helpers/commits_helper.rb
... | ... | @@ -9,11 +9,13 @@ module CommitsHelper |
9 | 9 | end |
10 | 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 | 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 | 19 | line_old = 1 |
18 | 20 | line_new = 1 |
19 | 21 | type = nil |
... | ... | @@ -39,7 +41,7 @@ module CommitsHelper |
39 | 41 | next |
40 | 42 | else |
41 | 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 | 45 | yield(full_line, type, line_code, line_new, line_old) |
44 | 46 | end |
45 | 47 | ... | ... |
app/helpers/notes_helper.rb
1 | 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 | 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 | 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 | 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 | 31 | end |
24 | 32 | end | ... | ... |
app/mailers/notify.rb
... | ... | @@ -63,12 +63,12 @@ class Notify < ActionMailer::Base |
63 | 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 | 67 | @note = Note.find(note_id) |
68 | 68 | @commit = @note.noteable |
69 | 69 | @commit = CommitDecorator.decorate(@commit) |
70 | 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 | 72 | end |
73 | 73 | |
74 | 74 | def note_issue_email(recipient_id, note_id) | ... | ... |
app/models/concerns/issuable.rb
... | ... | @@ -69,29 +69,33 @@ module Issuable |
69 | 69 | closed_changed? && !closed |
70 | 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 | 79 | end |
76 | 80 | |
77 | - def upvotes_in_percent | |
81 | + def downvotes_in_percent | |
78 | 82 | if votes_count.zero? |
79 | 83 | 0 |
80 | 84 | else |
81 | - 100.0 / votes_count * upvotes | |
85 | + 100.0 - upvotes_in_percent | |
82 | 86 | end |
83 | 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 | 92 | end |
89 | 93 | |
90 | - def downvotes_in_percent | |
94 | + def upvotes_in_percent | |
91 | 95 | if votes_count.zero? |
92 | 96 | 0 |
93 | 97 | else |
94 | - 100.0 - upvotes_in_percent | |
98 | + 100.0 / votes_count * upvotes | |
95 | 99 | end |
96 | 100 | end |
97 | 101 | ... | ... |
app/models/note.rb
... | ... | @@ -19,7 +19,6 @@ require 'carrierwave/orm/activerecord' |
19 | 19 | require 'file_size_validator' |
20 | 20 | |
21 | 21 | class Note < ActiveRecord::Base |
22 | - | |
23 | 22 | attr_accessible :note, :noteable, :noteable_id, :noteable_type, :project_id, |
24 | 23 | :attachment, :line_code, :commit_id |
25 | 24 | |
... | ... | @@ -34,6 +33,7 @@ class Note < ActiveRecord::Base |
34 | 33 | delegate :name, :email, to: :author, prefix: true |
35 | 34 | |
36 | 35 | validates :note, :project, presence: true |
36 | + validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true | |
37 | 37 | validates :attachment, file_size: { maximum: 10.megabytes.to_i } |
38 | 38 | |
39 | 39 | validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } |
... | ... | @@ -60,12 +60,74 @@ class Note < ActiveRecord::Base |
60 | 60 | }, without_protection: true) |
61 | 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 | 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 | 131 | end |
70 | 132 | |
71 | 133 | # override to return commits, which are not active record |
... | ... | @@ -81,50 +143,24 @@ class Note < ActiveRecord::Base |
81 | 143 | nil |
82 | 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 | 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 | 152 | end |
117 | 153 | |
118 | 154 | # Returns true if this is an upvote note, |
119 | 155 | # otherwise false is returned |
120 | 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 | 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 | 164 | end |
129 | 165 | |
130 | 166 | def noteable_type_name | ... | ... |
app/observers/note_observer.rb
... | ... | @@ -11,7 +11,7 @@ class NoteObserver < ActiveRecord::Observer |
11 | 11 | notify_team(note) |
12 | 12 | elsif note.notify_author |
13 | 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 | 15 | else |
16 | 16 | # Otherwise ignore it |
17 | 17 | nil | ... | ... |
app/views/commit/show.html.haml
... | ... | @@ -7,12 +7,10 @@ |
7 | 7 | %span.cred #{@commit.stats.deletions} deletions |
8 | 8 | |
9 | 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 | 12 | :javascript |
14 | 13 | $(function(){ |
15 | - PerLineNotes.init(); | |
16 | 14 | var w, h; |
17 | 15 | $('.diff_file').each(function(){ |
18 | 16 | $('.image.diff_removed img', this).on('load', $.proxy(function(event){ | ... | ... |
app/views/commits/_diffs.html.haml
... | ... | @@ -38,10 +38,10 @@ |
38 | 38 | |
39 | 39 | %br/ |
40 | 40 | .diff_file_content |
41 | - -# Skipp all non non-supported blobs | |
41 | + -# Skip all non-supported blobs | |
42 | 42 | - next unless file.respond_to?('text?') |
43 | 43 | - if file.text? |
44 | - = render "commits/text_file", diff: diff, index: i | |
44 | + = render "commits/text_diff", diff: diff, index: i | |
45 | 45 | - elsif file.image? |
46 | 46 | - old_file = (@commit.prev_commit.tree / diff.old_path) if !@commit.prev_commit.nil? |
47 | 47 | - if diff.renamed_file || diff.new_file || diff.deleted_file | ... | ... |
... | ... | @@ -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" ? " " : 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" ? " " : 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 | -- 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" ? " " : 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" ? " " : 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
app/views/merge_requests/_show.html.haml
... | ... | @@ -12,20 +12,18 @@ |
12 | 12 | %li.notes-tab{data: {action: 'notes'}} |
13 | 13 | = link_to project_merge_request_path(@project, @merge_request) do |
14 | 14 | %i.icon-comment |
15 | - Comments | |
15 | + Discussion | |
16 | 16 | %li.diffs-tab{data: {action: 'diffs'}} |
17 | 17 | = link_to diffs_project_merge_request_path(@project, @merge_request) do |
18 | 18 | %i.icon-list-alt |
19 | 19 | Diff |
20 | 20 | |
21 | 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 | 23 | .diffs.tab-content |
24 | 24 | = render "merge_requests/show/diffs" if @diffs |
25 | 25 | .status |
26 | 26 | |
27 | - = render "notes/per_line_form" | |
28 | - | |
29 | 27 | :javascript |
30 | 28 | var merge_request; |
31 | 29 | $(function(){ | ... | ... |
app/views/merge_requests/diffs.html.haml
app/views/merge_requests/diffs.js.haml
app/views/merge_requests/show.js.haml
app/views/notes/_common_form.html.haml
... | ... | @@ -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 | -- 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 | -- 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 | - } |
... | ... | @@ -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" | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | + | ... | ... |
... | ... | @@ -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" ? " " : line_old) | |
20 | + %td.new_line= raw(type == "old" ? " " : line_new) | |
21 | + %td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw "#{line} " | |
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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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/_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 | + | |
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 | 28 | = preserve do |
35 | 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 | 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 | 5 | - if can? current_user, :write_note, @project |
6 | - = render "notes/common_form" | |
6 | + = render "notes/form" | |
7 | 7 | |
8 | 8 | :javascript |
9 | 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 | -%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
app/views/notes/_per_line_note_link.html.haml
... | ... | @@ -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
app/views/notes/_per_line_reply_button.html.haml
app/views/notes/_reversed_notes_with_form.html.haml
1 | +.js-main-target-form | |
1 | 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 | 9 | :javascript |
9 | 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 | 22 | \ No newline at end of file | ... | ... |
app/views/notes/index.js.haml
1 | 1 | - unless @notes.blank? |
2 | + var notesHtml = "#{escape_javascript(render 'notes/notes')}"; | |
3 | + - new_note_ids = @notes.map(&:id) | |
2 | 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 | 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 | 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 | 13 | - else |
15 | 14 | - if loading_more_notes? |
16 | - :plain | |
17 | - NoteList.finishedLoadingMore(); | |
15 | + NoteList.finishedLoadingMore(); | ... | ... |
app/views/projects/wall.html.haml
app/views/snippets/show.html.haml
features/project/commits/commit_comments.feature
1 | -Feature: Project Comment commit | |
1 | +Feature: Comments on commits | |
2 | 2 | Background: |
3 | 3 | Given I sign in as a user |
4 | 4 | And I own project "Shop" |
5 | - Given I visit project commit page | |
5 | + And I visit project commit page | |
6 | 6 | |
7 | 7 | @javascript |
8 | - Scenario: I comment commit | |
8 | + Scenario: I can comment on a commit | |
9 | 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" | ... | ... |
... | ... | @@ -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 | 35 | Then I should see merge request "Wiki Feature" |
36 | 36 | |
37 | 37 | @javascript |
38 | - Scenario: I comment merge request | |
38 | + Scenario: I comment on a merge request | |
39 | 39 | Given I visit merge request page "Bug NS-04" |
40 | 40 | And I leave a comment like "XML attached" |
41 | 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/project_comment_commit.rb
features/steps/project/project_merge_requests.rb
... | ... | @@ -4,50 +4,55 @@ class ProjectMergeRequests < Spinach::FeatureSteps |
4 | 4 | include SharedNote |
5 | 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 | 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 | 17 | end |
14 | 18 | |
15 | 19 | Given 'I click link "Closed"' do |
16 | 20 | click_link "Closed" |
17 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 56 | end |
52 | 57 | |
53 | 58 | And 'I submit new merge request "Wiki Feature"' do |
... | ... | @@ -57,24 +62,91 @@ class ProjectMergeRequests < Spinach::FeatureSteps |
57 | 62 | click_button "Submit merge request" |
58 | 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 | 65 | And 'project "Shop" have "Bug NS-04" open merge request' do |
65 | 66 | project = Project.find_by_name("Shop") |
66 | 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 | 79 | end |
71 | 80 | |
72 | 81 | And 'project "Shop" have "Feature NS-03" closed merge request' do |
73 | 82 | project = Project.find_by_name("Shop") |
74 | 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 | 151 | end |
80 | 152 | end | ... | ... |
... | ... | @@ -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 | 1 | module SharedNote |
2 | 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 | 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 | 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 | 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 | 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 | 109 | end |
17 | 110 | |
18 | 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 | 224 | visit project_merge_request_path(mr.project, mr) |
225 | 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 | 232 | And 'I visit project "Shop" merge requests page' do |
228 | 233 | visit project_merge_requests_path(Project.find_by_name("Shop")) |
229 | 234 | end | ... | ... |
lib/gitlab/markdown.rb
... | ... | @@ -25,22 +25,6 @@ module Gitlab |
25 | 25 | # >> gfm(":trollface:") |
26 | 26 | # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" /> |
27 | 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 | 28 | attr_reader :html_options |
45 | 29 | |
46 | 30 | # Public: Parse the provided text with GitLab-Flavored Markdown |
... | ... | @@ -96,6 +80,20 @@ module Gitlab |
96 | 80 | text |
97 | 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 | 97 | def parse_references(text) |
100 | 98 | # parse reference links |
101 | 99 | text.gsub!(REFERENCE_PATTERN) do |match| |
... | ... | @@ -115,11 +113,13 @@ module Gitlab |
115 | 113 | end |
116 | 114 | end |
117 | 115 | |
116 | + EMOJI_PATTERN = %r{(:(\S+):)}.freeze | |
117 | + | |
118 | 118 | def parse_emoji(text) |
119 | 119 | # parse emoji |
120 | 120 | text.gsub!(EMOJI_PATTERN) do |match| |
121 | 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 | 123 | else |
124 | 124 | match |
125 | 125 | end | ... | ... |
spec/factories.rb
... | ... | @@ -73,8 +73,8 @@ FactoryGirl.define do |
73 | 73 | |
74 | 74 | # pick 3 commits "at random" (from bcf03b5d~3 to bcf03b5d) |
75 | 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 | 78 | st_commits do |
79 | 79 | [Commit.new(project.repo.commit('bcf03b5d')), |
80 | 80 | Commit.new(project.repo.commit('bcf03b5d~1')), |
... | ... | @@ -92,6 +92,32 @@ FactoryGirl.define do |
92 | 92 | factory :note do |
93 | 93 | project |
94 | 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 | 121 | end |
96 | 122 | |
97 | 123 | factory :event do | ... | ... |
spec/lib/votes_spec.rb
1 | 1 | require 'spec_helper' |
2 | 2 | |
3 | 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 | 12 | describe "#upvotes" do |
7 | 13 | it "with no notes has a 0/0 score" do |
8 | - issue.upvotes.should == 0 | |
14 | + merge_request.upvotes.should == 0 | |
9 | 15 | end |
10 | 16 | |
11 | 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 | 22 | end |
17 | 23 | |
18 | 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 | 27 | end |
22 | 28 | |
23 | 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 | 33 | end |
28 | 34 | end |
29 | 35 | |
30 | 36 | describe "#downvotes" do |
31 | 37 | it "with no notes has a 0/0 score" do |
32 | - issue.downvotes.should == 0 | |
38 | + merge_request.downvotes.should == 0 | |
33 | 39 | end |
34 | 40 | |
35 | 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 | 46 | end |
41 | 47 | |
42 | 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 | 51 | end |
46 | 52 | |
47 | 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 | 57 | end |
52 | 58 | end |
53 | 59 | |
54 | 60 | describe "#votes_count" do |
55 | 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 | 63 | end |
58 | 64 | |
59 | 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 | 69 | end |
64 | 70 | |
65 | 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 | 74 | end |
69 | 75 | |
70 | 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 | 79 | end |
74 | 80 | |
75 | 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 | 86 | end |
81 | 87 | end |
82 | 88 | |
83 | 89 | describe "#upvotes_in_percent" do |
84 | 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 | 92 | end |
87 | 93 | |
88 | 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 | 97 | end |
92 | 98 | |
93 | 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 | 103 | end |
98 | 104 | |
99 | 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 | 111 | end |
106 | 112 | end |
107 | 113 | |
108 | 114 | describe "#downvotes_in_percent" do |
109 | 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 | 117 | end |
112 | 118 | |
113 | 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 | 122 | end |
117 | 123 | |
118 | 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 | 128 | end |
123 | 129 | |
124 | 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 | 136 | end |
131 | 137 | end |
132 | 138 | end | ... | ... |
spec/models/note_spec.rb
... | ... | @@ -38,34 +38,34 @@ describe Note do |
38 | 38 | let(:project) { create(:project) } |
39 | 39 | |
40 | 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 | 42 | note.should_not be_upvote |
43 | 43 | note.should_not be_downvote |
44 | 44 | end |
45 | 45 | |
46 | 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 | 48 | note.should_not be_upvote |
49 | 49 | note.should_not be_downvote |
50 | 50 | end |
51 | 51 | |
52 | 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 | 54 | note.should be_upvote |
55 | 55 | end |
56 | 56 | |
57 | 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 | 59 | note.should be_upvote |
60 | 60 | end |
61 | 61 | |
62 | 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 | 64 | note.should be_downvote |
65 | 65 | end |
66 | 66 | |
67 | 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 | 69 | note.should be_downvote |
70 | 70 | end |
71 | 71 | end |
... | ... | @@ -74,43 +74,72 @@ describe Note do |
74 | 74 | let(:commit) { project.repository.commit } |
75 | 75 | |
76 | 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 | 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 | 84 | end |
88 | 85 | |
89 | 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 | 89 | end |
93 | 90 | |
94 | 91 | it "should be recognized by #for_commit?" do |
95 | - @note.should be_for_commit | |
92 | + note.should be_for_commit | |
96 | 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 | 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 | 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 | 107 | end |
111 | 108 | |
112 | 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 | 143 | end |
115 | 144 | end |
116 | 145 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |