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