diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index a0c859ab7f6c4e009b49f13ad689d0307e1750fd..3c30f9df058d82cf548dd8528a1836d04c6cad3a 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -10,40 +10,31 @@ }, computed: { allResolved: function () { - let allResolved = true; - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; - - for (const noteId in discussion) { - const note = discussion[noteId]; - - if (!note.resolved) { - allResolved = false; - } - } - } - - return allResolved; + const discussion = this.discussions[discussionId]; + return discussion.isResolved(); } }, methods: { jumpToNextUnresolvedDiscussion: function () { - let nextUnresolvedDiscussionId; + let nextUnresolvedDiscussionId, + firstUnresolvedDiscussionId; if (!this.discussionId) { + let i = 0; for (const discussionId in this.discussions) { const discussion = this.discussions[discussionId]; + const isResolved = discussion.isResolved(); - for (const noteId in discussion) { - const note = discussion[noteId]; + if (!firstUnresolvedDiscussionId && !isResolved) { + firstUnresolvedDiscussionId = discussionId; + } - if (!note.resolved) { - nextUnresolvedDiscussionId = discussionId; - break; - } + if (!isResolved) { + nextUnresolvedDiscussionId = discussionId; + break; } - if (nextUnresolvedDiscussionId) break; + i++; } } else { const discussionKeys = Object.keys(this.discussions), @@ -52,9 +43,16 @@ if (nextDiscussionId) { nextUnresolvedDiscussionId = nextDiscussionId; + } else { + firstUnresolvedDiscussionId = discussionKeys[0]; } } + if (firstUnresolvedDiscussionId) { + // Jump to first unresolved discussion + nextUnresolvedDiscussionId = firstUnresolvedDiscussionId; + } + if (nextUnresolvedDiscussionId) { $.scrollTo(`.discussion[data-discussion-id="${nextUnresolvedDiscussionId}"]`, { offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 index e077c3ba39532bc534269e6b5c999e8115fa184a..c8cf6b8ad3a4832ed5f4746b5bf537b763ebe37f 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 @@ -18,7 +18,16 @@ loading: false }; }, + watch: { + 'discussions': { + handler: 'updateTooltip', + deep: true + } + }, computed: { + note: function () { + return CommentsStore.get(this.discussionId, this.noteId); + }, buttonText: function () { if (this.isResolved) { return `Resolved by ${this.resolvedByName}`; @@ -26,8 +35,12 @@ return 'Mark as resolved'; } }, - isResolved: function () { return CommentsStore.get(this.discussionId, this.noteId).resolved; }, - resolvedByName: function () { return CommentsStore.get(this.discussionId, this.noteId).user; }, + isResolved: function () { + return this.note.resolved; + }, + resolvedByName: function () { + return this.note.user; + }, }, methods: { updateTooltip: function () { diff --git a/app/assets/javascripts/diff_notes/components/resolve_comment_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_comment_btn.js.es6 index 56514afa6ce6370031e16de7f859450ff519d23b..5832435dcfec213c2e8e3d5ed7774ed8880b1ba1 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_comment_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_comment_btn.js.es6 @@ -5,20 +5,10 @@ }, computed: { isDiscussionResolved: function () { - const notes = CommentsStore.notesForDiscussion(this.discussionId), - discussion = CommentsStore.state[this.discussionId]; - let allResolved = true; + const discussion = CommentsStore.state[this.discussionId], + notes = CommentsStore.notesForDiscussion(this.discussionId); - for (let i = 0; i < notes.length; i++) { - const noteId = notes[i]; - const note = discussion[noteId]; - - if (!note.resolved) { - allResolved = false; - } - } - - return allResolved; + return discussion.isResolved(); }, buttonText: function () { if (this.isDiscussionResolved) { diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 index b8df89491ad81f9d63ce59fba9fad39a194ea7fa..4d9e1dd8df82a618739f3bee27acd98066d52222 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 @@ -11,18 +11,9 @@ let resolvedCount = 0; for (const discussionId in this.discussions) { - const comments = this.discussions[discussionId]; - let resolved = true; + const discussion = this.discussions[discussionId]; - for (const noteId in comments) { - const commentResolved = comments[noteId].resolved; - - if (!commentResolved) { - resolved = false; - } - } - - if (resolved) { + if (discussion.isResolved()) { resolvedCount++; } } diff --git a/app/assets/javascripts/diff_notes/components/resolve_all_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 similarity index 72% rename from app/assets/javascripts/diff_notes/components/resolve_all_btn.js.es6 rename to app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 index 543ed8625f38148db8e37416499a45fe62843132..47a317a07c48d095208cc9381e9a3474d64a15ac 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_all_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 @@ -1,5 +1,5 @@ ((w) => { - w.ResolveAllBtn = Vue.extend({ + w.ResolveDiscussionBtn = Vue.extend({ mixins: [ ButtonMixins ], @@ -17,15 +17,7 @@ }, computed: { allResolved: function () { - let isResolved = true; - for (const noteId in this.discussions[this.discussionId]) { - const resolved = this.discussions[this.discussionId][noteId].resolved; - - if (!resolved) { - isResolved = false; - } - } - return isResolved; + return this.discussions[this.discussionId].isResolved(); }, buttonText: function () { if (this.allResolved) { diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index b856c7180342d427a57e8175a1fa95c39f38d76e..33b7648f0b66a626551830486d90c6d43cdc9994 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,5 +1,6 @@ //= require vue //= require vue-resource +//= require_directory ./models //= require_directory ./stores //= require_directory ./services //= require_directory ./mixins @@ -10,8 +11,8 @@ $(() => { el: '#diff-notes-app', components: { 'resolve-btn': ResolveBtn, - 'resolve-all-btn': ResolveAllBtn, - 'resolve-comment-btn': ResolveCommentBtn, + 'resolve-discussion-btn': ResolveDiscussionBtn, + 'resolve-comment-btn': ResolveCommentBtn } }); @@ -21,4 +22,13 @@ $(() => { 'resolve-count': ResolveCount } }); + + window.compileVueComponentsForDiffNotes = function () { + const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion'); + if ($components.length) { + $components.each(function () { + DiffNotesApp.$compile($(this).get(0)) + }); + } + } }); diff --git a/app/assets/javascripts/diff_notes/models/disucssion.js.es6 b/app/assets/javascripts/diff_notes/models/disucssion.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..bbc940214dd8eaecc0385f7fef59823d3ec5da54 --- /dev/null +++ b/app/assets/javascripts/diff_notes/models/disucssion.js.es6 @@ -0,0 +1,51 @@ +class DiscussionModel { + constructor (discussionId) { + this.discussionId = discussionId; + this.notes = {}; + } + + createNote (noteId, resolved, user) { + Vue.set(this.notes, noteId, new NoteModel(this.discussionId, noteId, resolved, user)); + } + + deleteNote (noteId) { + Vue.delete(this.notes, noteId); + } + + getNote (noteId) { + return this.notes[noteId]; + } + + isResolved () { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (!note.resolved) { + return false; + } + } + return true; + } + + resolveAllNotes (user) { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (!note.resolved) { + note.resolved = true; + note.user = user; + } + } + } + + unResolveAllNotes (user) { + for (const noteId in this.notes) { + const note = this.notes[noteId]; + + if (note.resolved) { + note.resolved = false; + note.user = null; + } + } + } +} diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js new file mode 100644 index 0000000000000000000000000000000000000000..2772991ce8bf710fe59d69e95466d6989c7204cd --- /dev/null +++ b/app/assets/javascripts/diff_notes/models/note.js @@ -0,0 +1,8 @@ +class NoteModel { + constructor (discussionId, noteId, resolved, user) { + this.discussionId = discussionId; + this.noteId = noteId; + this.resolved = resolved; + this.user = user; + } +} diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index 50392e76fbec80e1104d8c4d927638d016a13596..0be938448b2c6a330bf4d0b469a6d20ce810d493 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -24,17 +24,7 @@ } toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) { - const noteIds = CommentsStore.notesForDiscussion(discussionId); - let isResolved = true; - - for (let i = 0; i < noteIds.length; i++) { - const noteId = noteIds[i]; - const resolved = CommentsStore.state[discussionId][noteId].resolved; - - if (!resolved) { - isResolved = false; - } - } + const isResolved = CommentsStore.state[discussionId].isResolved(); if (isResolved) { return this.unResolveAll(namespace, mergeRequestId, discussionId); @@ -55,9 +45,11 @@ }, {}).then((response) => { const data = response.data; const user = data ? data.resolved_by : null; + const discussion = CommentsStore.state[discussionId]; + discussion.resolveAllNotes(user); + CommentsStore.loading[discussionId] = false; - CommentsStore.updateCommentsForDiscussion(discussionId, true, user); this.updateUpdatedHtml(discussionId, data); }); @@ -74,9 +66,10 @@ discussionId }, {}).then((response) => { const data = response.data; - CommentsStore.loading[discussionId] = false; + const discussion = CommentsStore.state[discussionId]; + discussion.unResolveAllNotes(); - CommentsStore.updateCommentsForDiscussion(discussionId, false); + CommentsStore.loading[discussionId] = false; this.updateUpdatedHtml(discussionId, data); }); diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 index 6e2040144a660acfbf5445c1876f113545507e3d..fbb71ca30af963d058e880eff6775687eb9699d2 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6 @@ -3,57 +3,32 @@ state: {}, loading: {}, get: function (discussionId, noteId) { - return this.state[discussionId][noteId]; + return this.state[discussionId].getNote(noteId); }, create: function (discussionId, noteId, resolved, user) { + let discussion = this.state[discussionId]; if (!this.state[discussionId]) { - Vue.set(this.state, discussionId, {}); + discussion = new DiscussionModel(discussionId); + Vue.set(this.state, discussionId, discussion); Vue.set(this.loading, discussionId, false); } - Vue.set(this.state[discussionId], noteId, { resolved, user}); + discussion.createNote(noteId, resolved, user); }, update: function (discussionId, noteId, resolved, user) { - this.state[discussionId][noteId].resolved = resolved; - this.state[discussionId][noteId].user = user; + const discussion = this.state[discussionId]; + const note = discussion.getNote(noteId); + note.resolved = resolved; + note.user = user; }, delete: function (discussionId, noteId) { - Vue.delete(this.state[discussionId], noteId); + const discussion = this.state[discussionId]; + discussion.deleteNote(noteId); - if (Object.keys(this.state[discussionId]).length === 0) { + if (Object.keys(discussion.notes).length === 0) { Vue.delete(this.state, discussionId); Vue.delete(this.loading, discussionId); } - }, - updateCommentsForDiscussion: function (discussionId, resolve, user) { - const noteIds = CommentsStore.resolvedNotesForDiscussion(discussionId, resolve); - - for (let i = 0; i < noteIds.length; i++) { - const noteId = noteIds[i]; - CommentsStore.update(discussionId, noteId, resolve, user); - } - }, - notesForDiscussion: function (discussionId) { - let ids = []; - - for (const noteId in CommentsStore.state[discussionId]) { - ids.push(noteId); - } - - return ids; - }, - resolvedNotesForDiscussion: function (discussionId, resolve) { - let ids = []; - - for (const noteId in CommentsStore.state[discussionId]) { - const resolved = CommentsStore.state[discussionId][noteId].resolved; - - if (resolved !== resolve) { - ids.push(noteId); - } - } - - return ids; } }; })(window); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 2343ace3b68e001b5822aaa724ab39e40d25acbf..8574a4e337056a01010b9564976a447b42afb92b 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -120,10 +120,8 @@ return function(data) { $('#diffs').html(data.html); - if ($('resolve-btn, resolve-all-btn, jump-to-discussion').length && (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null)) { - $('resolve-btn, resolve-all-btn, jump-to-discussion').each(function () { - DiffNotesApp.$compile($(this).get(0)) - }); + if (compileVueComponentsForDiffNotes) { + compileVueComponentsForDiffNotes(); } gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index aeaba5ae03e7e73a62674d16dd87d19934106a63..0ffd3e0882b7d51e657834e1c2805fba1a687106 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -300,10 +300,8 @@ discussionContainer.append(note_html); } - if ($('resolve-btn, resolve-all-btn, jump-to-discussion').length && (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null)) { - $('resolve-btn, resolve-all-btn, jump-to-discussion').each(function () { - DiffNotesApp.$compile($(this).get(0)) - }); + if (compileVueComponentsForDiffNotes) { + compileVueComponentsForDiffNotes(); } gl.utils.localTimeAgo($('.js-timeago', note_html), false); @@ -402,7 +400,7 @@ var namespacePath = $form.attr('data-namespace-path'), projectPath = $form.attr('data-project-path') discussionId = $form.attr('data-discussion-id'), - mergeRequestId = $('input[name="noteable_iid"]', $form).val(), + mergeRequestId = $form.attr('data-noteable-iid'), namespace = namespacePath + '/' + projectPath; if (ResolveService != null) { @@ -429,20 +427,10 @@ $html.find('.js-task-list-container').taskList('enable'); $note_li = $('.note-row-' + note.id); - if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) { - ref = DiffNotesApp.$refs['' + note.id + '']; - - if (ref) { - ref.$destroy(true); - } - } - $note_li.replaceWith($html); - if ($('resolve-btn, resolve-all-btn, jump-to-discussion').length && (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null)) { - $('resolve-btn, resolve-all-btn, jump-to-discussion').each(function () { - DiffNotesApp.$compile($(this).get(0)) - }); + if (compileVueComponentsForDiffNotes) { + compileVueComponentsForDiffNotes(); } }; @@ -589,7 +577,7 @@ */ Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { - var canResolve = dataHolder.attr('data-resolvable'); + var canResolve = dataHolder.attr('data-can-resolve'); form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId"))); form.attr("data-line-code", dataHolder.data("lineCode")); form.find("#note_type").val(dataHolder.data("noteType")); @@ -604,7 +592,7 @@ if (canResolve === 'false') { form.find('resolve-comment-btn').remove(); - } else if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) { + } else if (DiffNotesApp) { var $commentBtn = form.find('resolve-comment-btn'); $commentBtn .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'"); diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 9d9b0dba57f1a4b7771f57a8b0ebfee05853cfdd..3761855efc7c57a809a657a91e4dcefd157ef62e 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -79,7 +79,7 @@ module NotesHelper def link_to_reply_discussion(discussion, line_type = nil) return unless current_user - data = discussion.reply_attributes.merge(line_type: line_type, resolvable: discussion.can_resolve?(current_user)) + data = discussion.reply_attributes.merge(line_type: line_type, can_resolve: discussion.can_resolve?(current_user)) button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', data: data, title: 'Add a reply' diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml index 9a75a5738289f53f680ef48661ec6337f9e2e8ce..9eef367716fdc501d1110bd0c251e3dafde9b966 100644 --- a/app/views/discussions/_resolve_all.html.haml +++ b/app/views/discussions/_resolve_all.html.haml @@ -1,5 +1,5 @@ - if discussion.can_resolve?(current_user) - %resolve-all-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'", + %resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'", ":project-path" => "'#{discussion.project.path}'", ":discussion-id" => "'#{discussion.id}'", ":merge-request-id" => "#{discussion.noteable.iid}", diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 032aeec70fd61b3593a052c9796ac2635f265a21..b915d0133f914703be42de86898d7e71e0ee2fc9 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,11 +1,10 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) = f.hidden_field :commit_id = f.hidden_field :line_code = f.hidden_field :noteable_id - = hidden_field_tag :noteable_iid, @note.noteable.try(:iid) = f.hidden_field :noteable_type = f.hidden_field :type = f.hidden_field :position diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 69b770b62b7d567e664193485f18cd8419b8245a..c708bd463878355238fe5abd8ef7fbbe0a6b622f 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -22,7 +22,7 @@ - if access %span.note-role.hidden-xs= access - - if note.resolvable? + - if (note.resolvable? && can?(current_user, :resolve_note, note)) || (note.resolved? && !can?(current_user, :resolve_note, note)) %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'", ":project-path" => "'#{note.project.path}'", ":discussion-id" => "'#{note.discussion_id}'", @@ -36,7 +36,7 @@ .note-action-button = icon("spin spinner", "v-show" => "loading") %button.line-resolve-btn{ type: "button", - class: ("is-disabled" if !can?(current_user, :resolve_note, note)), + class: ("is-disabled" unless can?(current_user, :resolve_note, note)), ":class" => "{ 'is-active': isResolved }", ":aria-label" => "buttonText", "@click" => "resolve",