Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/application.js9
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es649
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6188
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es6107
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es618
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es660
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es635
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es635
-rw-r--r--app/assets/javascripts/diff_notes/mixins/namespace.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es687
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es688
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es653
-rw-r--r--app/assets/javascripts/merge_request.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/notes.js95
-rw-r--r--app/assets/javascripts/single_file_diff.js16
-rw-r--r--app/assets/stylesheets/behaviors.scss6
-rw-r--r--app/assets/stylesheets/pages/note_form.scss26
-rw-r--r--app/assets/stylesheets/pages/notes.scss77
20 files changed, 953 insertions, 22 deletions
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index f6e8e770527..a122fa2d637 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -225,10 +225,13 @@
});
$body.on("click", ".js-toggle-diff-comments", function(e) {
var $this = $(this);
- var showComments = $this.hasClass('active');
-
$this.toggleClass('active');
- $this.closest(".diff-file").find(".notes_holder").toggle(showComments);
+ var notesHolders = $this.closest('.diff-file').find('.notes_holder');
+ if ($this.hasClass('active')) {
+ notesHolders.show();
+ } else {
+ notesHolders.hide();
+ }
return e.preventDefault();
});
$document.off("click", '.js-confirm-danger');
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
new file mode 100644
index 00000000000..48bc7d77805
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -0,0 +1,49 @@
+((w) => {
+ w.CommentAndResolveBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ textareaIsEmpty: Boolean
+ },
+ computed: {
+ discussion: function () {
+ return CommentsStore.state[this.discussionId];
+ },
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ return this.discussion.isResolved();
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ if (this.textareaIsEmpty) {
+ return "Unresolve discussion";
+ } else {
+ return "Comment & unresolve discussion";
+ }
+ } else {
+ if (this.textareaIsEmpty) {
+ return "Resolve discussion";
+ } else {
+ return "Comment & resolve discussion";
+ }
+ }
+ }
+ },
+ ready: function () {
+ const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
+ this.textareaIsEmpty = $textarea.val() === '';
+
+ $textarea.on('input.comment-and-resolve-btn', () => {
+ this.textareaIsEmpty = $textarea.val() === '';
+ });
+ },
+ destroyed: function () {
+ $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
+ }
+ });
+})(window);
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
new file mode 100644
index 00000000000..ad80d1118df
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -0,0 +1,188 @@
+(() => {
+ JumpToDiscussion = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ discussionId: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ };
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ allResolved: function () {
+ return this.unresolvedDiscussionCount === 0;
+ },
+ showButton: function () {
+ if (this.discussionId) {
+ if (this.unresolvedDiscussionCount > 1) {
+ return true;
+ } else {
+ return this.discussionId !== this.lastResolvedId;
+ }
+ } else {
+ return this.unresolvedDiscussionCount >= 1;
+ }
+ },
+ lastResolvedId: function () {
+ let lastId;
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (!discussion.isResolved()) {
+ lastId = discussion.id;
+ }
+ }
+ return lastId;
+ }
+ },
+ methods: {
+ jumpToNextUnresolvedDiscussion: function () {
+ let discussionsSelector,
+ discussionIdsInScope,
+ firstUnresolvedDiscussionId,
+ nextUnresolvedDiscussionId,
+ activeTab = window.mrTabs.currentAction,
+ hasDiscussionsToJumpTo = true,
+ jumpToFirstDiscussion = !this.discussionId;
+
+ const discussionIdsForElements = function(elements) {
+ return elements.map(function() {
+ return $(this).attr('data-discussion-id');
+ }).toArray();
+ };
+
+ const discussions = this.discussions;
+
+ if (activeTab === 'diffs') {
+ discussionsSelector = '.diffs .notes[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+
+ let unresolvedDiscussionCount = 0;
+
+ for (let i = 0; i < discussionIdsInScope.length; i++) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+ if (discussion && !discussion.isResolved()) {
+ unresolvedDiscussionCount++;
+ }
+ }
+
+ if (this.discussionId && !this.discussion.isResolved()) {
+ // If this is the last unresolved discussion on the diffs tab,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 1) {
+ hasDiscussionsToJumpTo = false;
+ }
+ } else {
+ // If there are no unresolved discussions on the diffs tab at all,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 0) {
+ hasDiscussionsToJumpTo = false;
+ }
+ }
+ } else if (activeTab !== 'notes') {
+ // If we are on the commits or builds tabs,
+ // there are no discussions to jump to.
+ hasDiscussionsToJumpTo = false;
+ }
+
+ if (!hasDiscussionsToJumpTo) {
+ // If there are no discussions to jump to on the current page,
+ // switch to the notes tab and jump to the first disucssion there.
+ window.mrTabs.activateTab('notes');
+ activeTab = 'notes';
+ jumpToFirstDiscussion = true;
+ }
+
+ if (activeTab === 'notes') {
+ discussionsSelector = '.discussion[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+ }
+
+ let currentDiscussionFound = false;
+ for (let i = 0; i < discussionIdsInScope.length; i++) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+
+ if (!discussion) {
+ // Discussions for comments on commits in this MR don't have a resolved status.
+ continue;
+ }
+
+ if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
+ firstUnresolvedDiscussionId = discussionId;
+
+ if (jumpToFirstDiscussion) {
+ break;
+ }
+ }
+
+ if (!jumpToFirstDiscussion) {
+ if (currentDiscussionFound) {
+ if (!discussion.isResolved()) {
+ nextUnresolvedDiscussionId = discussionId;
+ break;
+ }
+ else {
+ continue;
+ }
+ }
+
+ if (discussionId === this.discussionId) {
+ currentDiscussionFound = true;
+ }
+ }
+ }
+
+ nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
+
+ if (!nextUnresolvedDiscussionId) {
+ return;
+ }
+
+ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
+
+ if (activeTab === 'notes') {
+ $target = $target.closest('.note-discussion');
+
+ // If the next discussion is closed, toggle it open.
+ if ($target.find('.js-toggle-content').is(':hidden')) {
+ $target.find('.js-toggle-button i').trigger('click')
+ }
+ } else if (activeTab === 'diffs') {
+ // Resolved discussions are hidden in the diffs tab by default.
+ // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
+ // When jumping between unresolved discussions on the diffs tab, we show them.
+ $target.closest(".content").show();
+
+ $target = $target.closest("tr.notes_holder");
+ $target.show();
+
+ // If we are on the diffs tab, we don't scroll to the discussion itself, but to
+ // 4 diff lines above it: the line the discussion was in response to + 3 context
+ let prevEl;
+ for (let i = 0; i < 4; i++) {
+ prevEl = $target.prev();
+
+ // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
+ if (!prevEl.hasClass("line_holder")) {
+ break;
+ }
+
+ $target = prevEl;
+ }
+ }
+
+ $.scrollTo($target, {
+ offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ });
+ }
+ }
+ });
+
+ Vue.component('jump-to-discussion', JumpToDiscussion);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
new file mode 100644
index 00000000000..be6ebc77947
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -0,0 +1,107 @@
+((w) => {
+ w.ResolveBtn = Vue.extend({
+ mixins: [
+ ButtonMixins
+ ],
+ props: {
+ noteId: Number,
+ discussionId: String,
+ resolved: Boolean,
+ namespacePath: String,
+ projectPath: String,
+ canResolve: Boolean,
+ resolvedBy: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ loading: false
+ };
+ },
+ watch: {
+ 'discussions': {
+ handler: 'updateTooltip',
+ deep: true
+ }
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ note: function () {
+ if (this.discussion) {
+ return this.discussion.getNote(this.noteId);
+ } else {
+ return undefined;
+ }
+ },
+ buttonText: function () {
+ if (this.isResolved) {
+ return `Resolved by ${this.resolvedByName}`;
+ } else if (this.canResolve) {
+ return 'Mark as resolved';
+ } else {
+ return 'Unable to resolve';
+ }
+ },
+ isResolved: function () {
+ if (this.note) {
+ return this.note.resolved;
+ } else {
+ return false;
+ }
+ },
+ resolvedByName: function () {
+ return this.note.resolved_by;
+ },
+ },
+ methods: {
+ updateTooltip: function () {
+ $(this.$els.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ },
+ resolve: function () {
+ if (!this.canResolve) return;
+
+ let promise;
+ this.loading = true;
+
+ if (this.isResolved) {
+ promise = ResolveService
+ .unresolve(this.namespace, this.noteId);
+ } else {
+ promise = ResolveService
+ .resolve(this.namespace, this.noteId);
+ }
+
+ promise.then((response) => {
+ this.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ this.discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
+ }
+
+ this.$nextTick(this.updateTooltip);
+ });
+ }
+ },
+ compiled: function () {
+ $(this.$els.button).tooltip({
+ container: 'body'
+ });
+ },
+ beforeDestroy: function () {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created: function () {
+ CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
new file mode 100644
index 00000000000..9e383b14a3e
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -0,0 +1,18 @@
+((w) => {
+ w.ResolveCount = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ loggedOut: Boolean
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.resolvedDiscussionCount === this.discussionCount;
+ }
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
new file mode 100644
index 00000000000..e373b06b1eb
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -0,0 +1,60 @@
+((w) => {
+ w.ResolveDiscussionBtn = Vue.extend({
+ mixins: [
+ ButtonMixins
+ ],
+ props: {
+ discussionId: String,
+ mergeRequestId: Number,
+ namespacePath: String,
+ projectPath: String,
+ canResolve: Boolean,
+ },
+ data: function() {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ if (this.discussion) {
+ return this.discussion.isResolved();
+ } else {
+ return false;
+ }
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ return "Unresolve discussion";
+ } else {
+ return "Resolve discussion";
+ }
+ },
+ loading: function () {
+ if (this.discussion) {
+ return this.discussion.loading;
+ } else {
+ return false;
+ }
+ }
+ },
+ methods: {
+ resolve: function () {
+ ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
+ }
+ },
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
new file mode 100644
index 00000000000..22d9cf6c857
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -0,0 +1,35 @@
+//= require vue
+//= require vue-resource
+//= require_directory ./models
+//= require_directory ./stores
+//= require_directory ./services
+//= require_directory ./mixins
+//= require_directory ./components
+
+$(() => {
+ window.DiffNotesApp = new Vue({
+ el: '#diff-notes-app',
+ components: {
+ 'resolve-btn': ResolveBtn,
+ 'resolve-discussion-btn': ResolveDiscussionBtn,
+ 'comment-and-resolve-btn': CommentAndResolveBtn
+ },
+ methods: {
+ compileComponents: function () {
+ const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
+ if ($components.length) {
+ $components.each(function () {
+ DiffNotesApp.$compile($(this).get(0));
+ });
+ }
+ }
+ }
+ });
+
+ new Vue({
+ el: '#resolve-count-app',
+ components: {
+ 'resolve-count': ResolveCount
+ }
+ });
+});
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
new file mode 100644
index 00000000000..a05f885201d
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -0,0 +1,35 @@
+((w) => {
+ w.DiscussionMixins = {
+ computed: {
+ discussionCount: function () {
+ return Object.keys(this.discussions).length;
+ },
+ resolvedDiscussionCount: function () {
+ let resolvedCount = 0;
+
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (discussion.isResolved()) {
+ resolvedCount++;
+ }
+ }
+
+ return resolvedCount;
+ },
+ unresolvedDiscussionCount: function () {
+ let unresolvedCount = 0;
+
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (!discussion.isResolved()) {
+ unresolvedCount++;
+ }
+ }
+
+ return unresolvedCount;
+ }
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
new file mode 100644
index 00000000000..d278678085b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
@@ -0,0 +1,9 @@
+((w) => {
+ w.ButtonMixins = {
+ computed: {
+ namespace: function () {
+ return `${this.namespacePath}/${this.projectPath}`;
+ }
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
new file mode 100644
index 00000000000..488714e4870
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -0,0 +1,87 @@
+class DiscussionModel {
+ constructor (discussionId) {
+ this.id = discussionId;
+ this.notes = {};
+ this.loading = false;
+ this.canResolve = false;
+ }
+
+ createNote (noteId, canResolve, resolved, resolved_by) {
+ Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
+ }
+
+ deleteNote (noteId) {
+ Vue.delete(this.notes, noteId);
+ }
+
+ getNote (noteId) {
+ return this.notes[noteId];
+ }
+
+ notesCount() {
+ return Object.keys(this.notes).length;
+ }
+
+ isResolved () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ resolveAllNotes (resolved_by) {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ note.resolved = true;
+ note.resolved_by = resolved_by;
+ }
+ }
+ }
+
+ unResolveAllNotes () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.resolved) {
+ note.resolved = false;
+ note.resolved_by = null;
+ }
+ }
+ }
+
+ updateHeadline (data) {
+ const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`);
+
+ if (data.discussion_headline_html) {
+ if ($discussionHeadline.length) {
+ $discussionHeadline.replaceWith(data.discussion_headline_html);
+ } else {
+ $(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html);
+ }
+ } else {
+ $discussionHeadline.remove();
+ }
+ }
+
+ isResolvable () {
+ if (!this.canResolve) {
+ return false;
+ }
+
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.canResolve) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
new file mode 100644
index 00000000000..f2d2d389c38
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -0,0 +1,9 @@
+class NoteModel {
+ constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
+ this.discussionId = discussionId;
+ this.id = noteId;
+ this.canResolve = canResolve;
+ this.resolved = resolved;
+ this.resolved_by = resolved_by;
+ }
+}
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
new file mode 100644
index 00000000000..de771ff814b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -0,0 +1,88 @@
+((w) => {
+ class ResolveServiceClass {
+ constructor() {
+ this.noteResource = Vue.resource('notes{/noteId}/resolve');
+ this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
+ }
+
+ setCSRF() {
+ Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+
+ prepareRequest(namespace) {
+ this.setCSRF();
+ Vue.http.options.root = `/${namespace}`;
+ }
+
+ resolve(namespace, noteId) {
+ this.prepareRequest(namespace);
+
+ return this.noteResource.save({ noteId }, {});
+ }
+
+ unresolve(namespace, noteId) {
+ this.prepareRequest(namespace);
+
+ return this.noteResource.delete({ noteId }, {});
+ }
+
+ toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId],
+ isResolved = discussion.isResolved();
+ let promise;
+
+ if (isResolved) {
+ promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
+ } else {
+ promise = this.resolveAll(namespace, mergeRequestId, discussionId);
+ }
+
+ promise.then((response) => {
+ discussion.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ if (isResolved) {
+ discussion.unResolveAllNotes();
+ } else {
+ discussion.resolveAllNotes(resolved_by);
+ }
+
+ discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
+ }
+ })
+ }
+
+ resolveAll(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ this.prepareRequest(namespace);
+
+ discussion.loading = true;
+
+ return this.discussionResource.save({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+
+ unResolveAll(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ this.prepareRequest(namespace);
+
+ discussion.loading = true;
+
+ return this.discussionResource.delete({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+ }
+
+ w.ResolveService = new ResolveServiceClass();
+})(window);
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
new file mode 100644
index 00000000000..69522e1dac5
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -0,0 +1,53 @@
+((w) => {
+ w.CommentsStore = {
+ state: {},
+ get: function (discussionId, noteId) {
+ return this.state[discussionId].getNote(noteId);
+ },
+ createDiscussion: function (discussionId, canResolve) {
+ let discussion = this.state[discussionId];
+ if (!this.state[discussionId]) {
+ discussion = new DiscussionModel(discussionId);
+ Vue.set(this.state, discussionId, discussion);
+ }
+
+ if (canResolve !== undefined) {
+ discussion.canResolve = canResolve;
+ }
+
+ return discussion;
+ },
+ create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
+ const discussion = this.createDiscussion(discussionId);
+
+ discussion.createNote(noteId, canResolve, resolved, resolved_by);
+ },
+ update: function (discussionId, noteId, resolved, resolved_by) {
+ const discussion = this.state[discussionId];
+ const note = discussion.getNote(noteId);
+ note.resolved = resolved;
+ note.resolved_by = resolved_by;
+ },
+ delete: function (discussionId, noteId) {
+ const discussion = this.state[discussionId];
+ discussion.deleteNote(noteId);
+
+ if (discussion.notesCount() === 0) {
+ Vue.delete(this.state, discussionId);
+ }
+ },
+ unresolvedDiscussionIds: function () {
+ let ids = [];
+
+ for (const discussionId in this.state) {
+ const discussion = this.state[discussionId];
+
+ if (!discussion.isResolved()) {
+ ids.push(discussion.id);
+ }
+ }
+
+ return ids;
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 47e6dd1084d..56ebf84c4f6 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -34,7 +34,7 @@
MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') {
- return new MergeRequestTabs(this.opts);
+ window.mrTabs = new MergeRequestTabs(this.opts);
} else {
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 4e2273f5aa8..ad08209d61e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -89,6 +89,7 @@
if (action === 'show') {
action = 'notes';
}
+ this.currentAction = action;
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
if (action !== 'notes') {
new_state += "/" + action;
@@ -127,6 +128,11 @@
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 2484a07f363..d0d5cad813a 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -68,6 +68,7 @@
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
$(document).on("click", ".js-comment-button", this.updateCloseButton);
$(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+ $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
$(document).on("click", ".js-note-delete", this.removeNote);
$(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
$(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
@@ -100,6 +101,7 @@
$(document).off("click", ".js-note-target-close");
$(document).off("click", ".js-note-discard");
$(document).off("keydown", ".js-note-text");
+ $(document).off('click', '.js-comment-resolve-button');
$('.note .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.note .js-task-list-container');
};
@@ -304,6 +306,11 @@
} else {
discussionContainer.append(note_html);
}
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+
gl.utils.localTimeAgo($('.js-timeago', note_html), false);
return this.updateNotesCount(1);
};
@@ -350,6 +357,7 @@
form.find("#note_line_code").remove();
form.find("#note_position").remove();
form.find("#note_type").remove();
+ form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
return this.parentTimeline = form.parents('.timeline');
};
@@ -393,8 +401,22 @@
*/
Notes.prototype.addDiscussionNote = function(xhr, note, status) {
+ var $form = $(xhr.target);
+
+ if ($form.attr('data-resolve-all') != null) {
+ var namespacePath = $form.attr('data-namespace-path'),
+ projectPath = $form.attr('data-project-path')
+ discussionId = $form.attr('data-discussion-id'),
+ mergeRequestId = $form.attr('data-noteable-iid'),
+ namespace = namespacePath + '/' + projectPath;
+
+ if (ResolveService != null) {
+ ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
+ }
+ }
+
this.renderDiscussionNote(note);
- return this.removeDiscussionNoteForm($(xhr.target));
+ this.removeDiscussionNoteForm($form);
};
@@ -411,7 +433,12 @@
$html.syntaxHighlight();
$html.find('.js-task-list-container').taskList('enable');
$note_li = $('.note-row-' + note.id);
- return $note_li.replaceWith($html);
+
+ $note_li.replaceWith($html);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
};
@@ -492,6 +519,15 @@
var note, notes;
note = $(el);
notes = note.closest(".notes");
+
+ if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
+ ref = DiffNotesApp.$refs[noteId];
+
+ if (ref) {
+ ref.$destroy(true);
+ }
+ }
+
if (notes.find(".note").length === 1) {
notes.closest(".timeline-entry").remove();
notes.closest("tr").remove();
@@ -530,8 +566,10 @@
var form, replyLink;
form = this.formClone.clone();
replyLink = $(e.target).closest(".js-discussion-reply-button");
- replyLink.hide();
- replyLink.after(form);
+ replyLink
+ .closest('.discussion-reply-holder')
+ .hide()
+ .after(form);
return this.setupDiscussionNoteForm(replyLink, form);
};
@@ -556,9 +594,23 @@
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
+ form.find('.js-note-target-close').remove();
this.setupNoteForm(form);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ var $commentBtn = form.find('comment-and-resolve-btn');
+ $commentBtn
+ .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
+ DiffNotesApp.$compile($commentBtn.get(0));
+ }
+
form.find(".js-note-text").focus();
- return form.removeClass('js-main-target-form').addClass("discussion-form js-discussion-note-form");
+ form
+ .find('.js-comment-resolve-button')
+ .attr('data-discussion-id', dataHolder.data('discussionId'));
+ form
+ .removeClass('js-main-target-form')
+ .addClass("discussion-form js-discussion-note-form");
};
@@ -577,16 +629,19 @@
nextRow = row.next();
hasNotes = nextRow.is(".notes_holder");
addForm = false;
- targetContent = ".notes_content";
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>";
+ notesContentSelector = ".notes_content";
+ rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
if (this.isParallelView()) {
lineType = $link.data("lineType");
- targetContent += "." + lineType;
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>";
+ notesContentSelector += "." + lineType;
+ rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
}
+ notesContentSelector += " .content";
if (hasNotes) {
- notesContent = nextRow.find(targetContent);
+ nextRow.show();
+ notesContent = nextRow.find(notesContentSelector);
if (notesContent.length) {
+ notesContent.show();
replyButton = notesContent.find(".js-discussion-reply-button:visible");
if (replyButton.length) {
e.target = replyButton[0];
@@ -600,11 +655,13 @@
}
} else {
row.after(rowCssToAdd);
+ nextRow = row.next();
+ notesContent = nextRow.find(notesContentSelector);
addForm = true;
}
if (addForm) {
newForm = this.formClone.clone();
- newForm.appendTo(row.next().find(targetContent));
+ newForm.appendTo(notesContent);
return this.setupDiscussionNoteForm($link, newForm);
}
};
@@ -623,7 +680,9 @@
glForm = form.data('gl-form');
glForm.destroy();
form.find(".js-note-text").data("autosave").reset();
- form.prev(".js-discussion-reply-button").show();
+ form
+ .prev('.discussion-reply-holder')
+ .show();
if (row.is(".js-temp-notes-holder")) {
return row.remove();
} else {
@@ -732,6 +791,18 @@
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
};
+ Notes.prototype.resolveDiscussion = function () {
+ var $this = $(this),
+ discussionId = $this.attr('data-discussion-id');
+
+ $this
+ .closest('form')
+ .attr('data-discussion-id', discussionId)
+ .attr('data-resolve-all', 'true')
+ .attr('data-namespace-path', $this.attr('data-namespace-path'))
+ .attr('data-project-path', $this.attr('data-project-path'));
+ };
+
return Notes;
})();
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index b9ae497b0e5..156b9b8abec 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -35,10 +35,16 @@
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
- return this.collapsedContent.show();
+ this.collapsedContent.show();
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
} else if (this.content) {
this.collapsedContent.hide();
- return this.content.show();
+ this.content.show();
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
} else {
return this.getContentHTML();
}
@@ -57,7 +63,11 @@
_this.hasError = true;
_this.content = $(ERROR_HTML);
}
- return _this.collapsedContent.after(_this.content);
+ _this.collapsedContent.after(_this.content);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
};
})(this));
};
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index a6b9efc49c9..897bc49e7df 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -21,7 +21,7 @@
}
}
-
-[v-cloak] {
- display: none;
+// Hide element if Vue is still working on rendering it fully.
+[v-cloak="true"] {
+ display: none !important;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3784010348a..bd875b9823f 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -159,6 +159,32 @@
}
}
+.discussion-with-resolve-btn {
+ display: table;
+ width: 100%;
+ border-collapse: separate;
+ table-layout: auto;
+
+ .btn-group {
+ display: table-cell;
+ float: none;
+ width: 1%;
+
+ &:first-child {
+ width: 100%;
+ padding-right: 5px;
+ }
+
+ &:last-child {
+ padding-left: 5px;
+ }
+ }
+
+ .btn {
+ width: 100%;
+ }
+}
+
.discussion-notes-count {
font-size: 16px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a2b5437e503..08d1692c888 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -383,3 +383,80 @@ ul.notes {
color: $gl-link-color;
}
}
+
+.line-resolve-all-container {
+ .btn-group {
+ margin-top: -1px;
+ margin-left: -4px;
+ }
+
+ .discussion-next-btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
+
+.line-resolve-all {
+ display: inline-block;
+ padding: 5px 10px;
+ background-color: $background-color;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ &.has-next-btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .line-resolve-btn {
+ vertical-align: middle;
+ margin-right: 5px;
+ }
+}
+
+.line-resolve-text {
+ vertical-align: middle;
+}
+
+.line-resolve-btn {
+ display: inline-block;
+ position: relative;
+ top: 2px;
+ padding: 0;
+ background-color: transparent;
+ border: none;
+ outline: 0;
+
+ &.is-disabled {
+ cursor: default;
+ }
+
+ &:not(.is-disabled):hover,
+ &:not(.is-disabled):focus,
+ &.is-active {
+ color: $gl-text-green;
+
+ svg path {
+ fill: $gl-text-green;
+ }
+ }
+
+ svg {
+ position: relative;
+ color: $notes-action-color;
+
+ path {
+ fill: $notes-action-color;
+ }
+ }
+}
+
+.discussion-next-btn {
+ svg {
+ margin: 0;
+
+ path {
+ fill: $gray-darkest;
+ }
+ }
+}