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