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