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