From efb74875cfe1a1e2b3696f0129b819fc8e204876 Mon Sep 17 00:00:00 2001
From: Phil Hughes <me@iamphill.com>
Date: Fri, 29 Jul 2016 11:19:56 +0100
Subject: [PATCH] Moved most of the data handling into discussion & notes
 models Reduced some duplicated code with compiling components Fixed bug with
 resolve button tooltip not updating after resolving discussion

---
 .../components/jump_to_discussion.js.es6      | 42 ++++++++-------
 .../diff_notes/components/resolve_btn.js.es6  | 17 ++++++-
 .../components/resolve_comment_btn.js.es6     | 16 ++----
 .../components/resolve_count.js.es6           | 13 +----
 ...n.js.es6 => resolve_discussion_btn.js.es6} | 12 +----
 .../diff_notes/diff_notes_bundle.js.es6       | 14 ++++-
 .../diff_notes/models/disucssion.js.es6       | 51 +++++++++++++++++++
 .../javascripts/diff_notes/models/note.js     |  8 +++
 .../diff_notes/services/resolve.js.es6        | 21 +++-----
 .../diff_notes/stores/comments.js.es6         | 49 +++++-------------
 app/assets/javascripts/merge_request_tabs.js  |  6 +--
 app/assets/javascripts/notes.js               | 26 +++-------
 app/helpers/notes_helper.rb                   |  2 +-
 app/views/discussions/_resolve_all.html.haml  |  2 +-
 app/views/projects/notes/_form.html.haml      |  3 +-
 app/views/projects/notes/_note.html.haml      |  4 +-
 16 files changed, 146 insertions(+), 140 deletions(-)
 rename app/assets/javascripts/diff_notes/components/{resolve_all_btn.js.es6 => resolve_discussion_btn.js.es6} (72%)
 create mode 100644 app/assets/javascripts/diff_notes/models/disucssion.js.es6
 create mode 100644 app/assets/javascripts/diff_notes/models/note.js

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 a0c859ab7f6..3c30f9df058 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 e077c3ba395..c8cf6b8ad3a 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 56514afa6ce..5832435dcfe 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 b8df89491ad..4d9e1dd8df8 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 543ed8625f3..47a317a07c4 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 b856c718034..33b7648f0b6 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 00000000000..bbc940214dd
--- /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 00000000000..2772991ce8b
--- /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 50392e76fbe..0be938448b2 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 6e2040144a6..fbb71ca30af 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 2343ace3b68..8574a4e3370 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 aeaba5ae03e..0ffd3e0882b 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 9d9b0dba57f..3761855efc7 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 9a75a573828..9eef367716f 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 032aeec70fd..b915d0133f9 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 69b770b62b7..c708bd46387 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",
-- 
GitLab