Commit 8205fc6c authored by Phil Hughes's avatar Phil Hughes

Merge branch 'leipert-remove-unused-diff-notes-code' into 'master'

Remove unused code related to discussions

See merge request gitlab-org/gitlab!43412
parents a6309aba 02ca0ed3
......@@ -216,7 +216,6 @@ linters:
- "app/views/projects/mattermosts/_team_selection.html.haml"
- "app/views/projects/mattermosts/new.html.haml"
- "app/views/projects/merge_requests/_commits.html.haml"
- "app/views/projects/merge_requests/_discussion.html.haml"
- "app/views/projects/merge_requests/_how_to_merge.html.haml"
- "app/views/projects/merge_requests/_mr_title.html.haml"
- "app/views/projects/merge_requests/conflicts/_commit_stats.html.haml"
......
/* global CommentsStore */
import $ from 'jquery';
import Vue from 'vue';
import { __ } from '~/locale';
const CommentAndResolveBtn = Vue.extend({
props: {
discussionId: {
type: String,
required: true,
},
},
data() {
return {
textareaIsEmpty: true,
discussion: {},
};
},
computed: {
showButton() {
if (this.discussion) {
return this.discussion.isResolvable();
}
return false;
},
isDiscussionResolved() {
return this.discussion.isResolved();
},
buttonText() {
if (this.textareaIsEmpty) {
return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
}
return this.isDiscussionResolved
? __('Comment & unresolve thread')
: __('Comment & resolve thread');
},
},
created() {
if (this.discussionId) {
this.discussion = CommentsStore.state[this.discussionId];
}
},
mounted() {
if (!this.discussionId) return;
const $textarea = $(
`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`,
);
this.textareaIsEmpty = $textarea.val() === '';
$textarea.on('input.comment-and-resolve-btn', () => {
this.textareaIsEmpty = $textarea.val() === '';
});
},
destroyed() {
if (!this.discussionId) return;
$(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off(
'input.comment-and-resolve-btn',
);
},
});
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
/* global CommentsStore */
import $ from 'jquery';
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
import Notes from '../../notes';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import { n__ } from '~/locale';
const DiffNoteAvatars = Vue.extend({
components: {
userAvatarImage,
},
props: {
discussionId: {
type: String,
required: true,
},
},
data() {
return {
isVisible: false,
lineType: '',
storeState: CommentsStore.state,
shownAvatars: 3,
collapseIcon,
};
},
computed: {
discussionClassName() {
return `js-diff-avatars-${this.discussionId}`;
},
notesSubset() {
let notes = [];
if (this.discussion) {
notes = Object.keys(this.discussion.notes)
.slice(0, this.shownAvatars)
.map(noteId => this.discussion.notes[noteId]);
}
return notes;
},
extraNotesTitle() {
if (this.discussion) {
const extra = this.discussion.notesCount() - this.shownAvatars;
return n__('%d more comment', '%d more comments', extra);
}
return '';
},
discussion() {
return this.storeState[this.discussionId];
},
notesCount() {
if (this.discussion) {
return this.discussion.notesCount();
}
return 0;
},
moreText() {
const plusSign = this.notesCount < 100 ? '+' : '';
return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
},
watch: {
storeState: {
handler() {
this.$nextTick(() => {
$('.has-tooltip', this.$el).tooltip('_fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
});
},
deep: true,
},
},
mounted() {
this.$nextTick(() => {
this.addNoCommentClass();
this.setDiscussionVisible();
this.lineType = $(this.$el)
.closest('.diff-line-num')
.hasClass('old_line')
? 'old'
: 'new';
});
$(document).on('toggle.comments', () => {
this.$nextTick(() => {
this.setDiscussionVisible();
});
});
},
beforeDestroy() {
this.addNoCommentClass();
$(document).off('toggle.comments');
},
methods: {
clickedAvatar(e) {
Notes.instance.onAddDiffNote(e);
// Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState();
this.$nextTick(() => {
this.setDiscussionVisible();
$('.has-tooltip', this.$el).tooltip('_fixTitle');
$('.has-tooltip', this.$el).tooltip('hide');
});
},
addNoCommentClass() {
const { notesCount } = this;
$(this.$el)
.closest('.js-avatar-container')
.toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container')
.toggleClass('no-comment-btn', notesCount > 0);
},
toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el)
.closest('.code')
.find('.notes_holder');
const $visibleNotesHolders = $notesHolders.filter(':visible');
const $toggleDiffCommentsBtn = $(this.$el)
.closest('.diff-file')
.find('.js-toggle-diff-comments');
$toggleDiffCommentsBtn.toggleClass(
'active',
$notesHolders.length === $visibleNotesHolders.length,
);
},
setDiscussionVisible() {
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(
':visible',
);
},
getTooltipText(note) {
return `${note.authorName}: ${note.noteTruncated}`;
},
},
template: `
<div class="diff-comment-avatar-holders"
:class="discussionClassName"
v-show="notesCount !== 0">
<div v-if="!isVisible">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image
v-for="note in notesSubset"
:key="note.id"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="clickedAvatar($event)"
:img-src="note.authorAvatar"
:tooltip-text="getTooltipText(note)"
:data-line-type="lineType"
:size="19"
data-html="true"
/>
<span v-if="notesCount > shownAvatars"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
data-container="body"
data-placement="top"
ref="extraComments"
role="button"
:data-line-type="lineType"
:title="extraNotesTitle"
@click="clickedAvatar($event)">{{ moreText }}</span>
</div>
<button class="diff-notes-collapse js-diff-comment-avatar"
type="button"
aria-label="Show comments"
:data-line-type="lineType"
@click="clickedAvatar($event)"
v-if="isVisible"
v-html="collapseIcon">
</button>
</div>
`,
});
Vue.component('diff-note-avatars', DiffNoteAvatars);
/* eslint-disable func-names, no-continue */
/* global CommentsStore */
import $ from 'jquery';
import 'vendor/jquery.scrollTo';
import Vue from 'vue';
import { __ } from '~/locale';
import DiscussionMixins from '../mixins/discussion';
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
discussionId: {
type: String,
required: true,
},
},
data() {
return {
discussions: CommentsStore.state,
discussion: {},
};
},
computed: {
buttonText() {
if (this.discussionId) {
return __('Jump to next unresolved thread');
}
return __('Jump to first unresolved thread');
},
allResolved() {
return this.unresolvedDiscussionCount === 0;
},
showButton() {
if (this.discussionId) {
if (this.unresolvedDiscussionCount > 1) {
return true;
}
return this.discussionId !== this.lastResolvedId;
}
return this.unresolvedDiscussionCount >= 1;
},
lastResolvedId() {
let lastId;
Object.keys(this.discussions).forEach(discussionId => {
const discussion = this.discussions[discussionId];
if (!discussion.isResolved()) {
lastId = discussion.id;
}
});
return lastId;
},
},
created() {
this.discussion = this.discussions[this.discussionId];
},
methods: {
jumpToNextUnresolvedDiscussion() {
let discussionsSelector;
let discussionIdsInScope;
let firstUnresolvedDiscussionId;
let nextUnresolvedDiscussionId;
let activeTab = window.mrTabs.currentAction;
let hasDiscussionsToJumpTo = true;
let jumpToFirstDiscussion = !this.discussionId;
const discussionIdsForElements = function(elements) {
return elements
.map(function() {
return $(this).attr('data-discussion-id');
})
.toArray();
};
const { discussions } = this;
if (activeTab === 'diffs') {
discussionsSelector = '.diffs .notes[data-discussion-id]';
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
let unresolvedDiscussionCount = 0;
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
if (discussion && !discussion.isResolved()) {
unresolvedDiscussionCount += 1;
}
}
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 (unresolvedDiscussionCount === 0) {
// If there are no unresolved discussions on the diffs tab at all,
// there are no discussions to jump to.
hasDiscussionsToJumpTo = false;
}
} else if (activeTab !== 'show') {
// 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 discussion there.
window.mrTabs.activateTab('show');
activeTab = 'show';
jumpToFirstDiscussion = true;
}
if (activeTab === 'show') {
discussionsSelector = '.discussion[data-discussion-id]';
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
}
let currentDiscussionFound = false;
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
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 === 'show') {
$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();
const $notesHolder = $target.closest('tr.notes_holder');
// Image diff discussions does not use notes_holder
// so we should keep original $target value in those cases
if ($notesHolder.length > 0) {
$target = $notesHolder;
}
$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 += 1) {
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: -150,
});
},
},
});
Vue.component('jump-to-discussion', JumpToDiscussion);
/* global CommentsStore */
import Vue from 'vue';
const NewIssueForDiscussion = Vue.extend({
props: {
discussionId: {
type: String,
required: true,
},
},
data() {
return {
discussions: CommentsStore.state,
};
},
computed: {
discussion() {
return this.discussions[this.discussionId];
},
showButton() {
if (this.discussion) return !this.discussion.isResolved();
return false;
},
},
});
Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
/* global CommentsStore */
/* global ResolveService */
import $ from 'jquery';
import Vue from 'vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
import { sprintf, __ } from '~/locale';
const ResolveBtn = Vue.extend({
props: {
noteId: {
type: Number,
required: true,
},
discussionId: {
type: String,
required: true,
},
resolved: {
type: Boolean,
required: true,
},
canResolve: {
type: Boolean,
required: true,
},
resolvedBy: {
type: String,
required: true,
},
authorName: {
type: String,
required: true,
},
authorAvatar: {
type: String,
required: true,
},
noteTruncated: {
type: String,
required: true,
},
},
data() {
return {
discussions: CommentsStore.state,
loading: false,
};
},
computed: {
discussion() {
return this.discussions[this.discussionId];
},
note() {
return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
buttonText() {
if (this.isResolved) {
return sprintf(__('Resolved by %{resolvedByName}'), {
resolvedByName: this.resolvedByName,
});
} else if (this.canResolve) {
return __('Mark as resolved');
}
return __('Unable to resolve');
},
isResolved() {
if (this.note) {
return this.note.resolved;
}
return false;
},
resolvedByName() {
return this.note.resolved_by;
},
},
watch: {
discussions: {
handler: 'updateTooltip',
deep: true,
},
},
mounted() {
$(this.$refs.button).tooltip({
container: 'body',
});
},
beforeDestroy() {
CommentsStore.delete(this.discussionId, this.noteId);
},
created() {
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
canResolve: this.canResolve,
resolved: this.resolved,
resolvedBy: this.resolvedBy,
authorName: this.authorName,
authorAvatar: this.authorAvatar,
noteTruncated: this.noteTruncated,
});
},
methods: {
updateTooltip() {
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
.tooltip('_fixTitle');
});
},
resolve() {
if (!this.canResolve) return;
let promise;
this.loading = true;
if (this.isResolved) {
promise = ResolveService.unresolve(this.noteId);
} else {
promise = ResolveService.resolve(this.noteId);
}
promise
.then(resp => resp.json())
.then(data => {
this.loading = false;
const resolvedBy = data ? data.resolved_by : null;
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
this.updateTooltip();
})
.catch(
() =>
new Flash(__('An error occurred when trying to resolve a comment. Please try again.')),
);
},
},
});
Vue.component('resolve-btn', ResolveBtn);
/* global CommentsStore */
import Vue from 'vue';
import DiscussionMixins from '../mixins/discussion';
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
props: {
loggedOut: {
type: Boolean,
required: true,
},
},
data() {
return {
discussions: CommentsStore.state,
};
},
computed: {
allResolved() {
return this.resolvedDiscussionCount === this.discussionCount;
},
resolvedCountText() {
return this.discussionCount === 1 ? 'discussion' : 'discussions';
},
},
});
/* eslint-disable func-names, new-cap */
import $ from 'jquery';
import Vue from 'vue';
import './models/discussion';
import './models/note';
import './stores/comments';
import './services/resolve';
import './mixins/discussion';
import './components/comment_resolve_btn';
import './components/jump_to_discussion';
import './components/resolve_btn';
import './components/resolve_count';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
export default () => {
const projectPathHolder =
document.querySelector('.merge-request') || document.querySelector('.commit-box');
const { projectPath } = projectPathHolder.dataset;
const COMPONENT_SELECTOR =
'resolve-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
$('diff-note-avatars').each(function() {
const tmp = Vue.extend({
template: $(this).get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
$(this).replaceWith(tmpApp.$el);
$(tmpApp.$el).one('remove.vue', () => {
tmpApp.$destroy();
tmpApp.$el.remove();
});
});
const $components = $(COMPONENT_SELECTOR).filter(function() {
return $(this).closest('resolve-count').length !== 1;
});
if ($components) {
$components.each(function() {
const $this = $(this);
const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({
template: $this.get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
if (noteId) {
gl.diffNoteApps[`note_${noteId}`] = tmpApp;
}
$this.replaceWith(tmpApp.$el);
});
}
};
gl.diffNotesCompileComponents();
$(window).trigger('resize.nav');
};
<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
/* eslint-disable guard-for-in, no-restricted-syntax, */
const DiscussionMixins = {
computed: {
discussionCount() {
return Object.keys(this.discussions).length;
},
resolvedDiscussionCount() {
let resolvedCount = 0;
for (const discussionId in this.discussions) {
const discussion = this.discussions[discussionId];
if (discussion.isResolved()) {
resolvedCount += 1;
}
}
return resolvedCount;
},
unresolvedDiscussionCount() {
let unresolvedCount = 0;
for (const discussionId in this.discussions) {
const discussion = this.discussions[discussionId];
if (!discussion.isResolved()) {
unresolvedCount += 1;
}
}
return unresolvedCount;
},
},
};
export default DiscussionMixins;
/* eslint-disable guard-for-in, no-restricted-syntax */
/* global NoteModel */
import $ from 'jquery';
import Vue from 'vue';
import { localTimeAgo } from '../../lib/utils/datetime_utility';
class DiscussionModel {
constructor(discussionId) {
this.id = discussionId;
this.notes = {};
this.loading = false;
this.canResolve = false;
}
createNote(noteObj) {
Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
}
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(resolvedBy) {
for (const noteId in this.notes) {
const note = this.notes[noteId];
if (!note.resolved) {
note.resolved = true;
note.resolved_by = resolvedBy;
}
}
}
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 discussionSelector = `.discussion[data-discussion-id="${this.id}"]`;
const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`);
if (data.discussion_headline_html) {
if ($discussionHeadline.length) {
$discussionHeadline.replaceWith(data.discussion_headline_html);
} else {
$(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
}
localTimeAgo($('.js-timeago', `${discussionSelector}`));
} 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;
}
}
window.DiscussionModel = DiscussionModel;
class NoteModel {
constructor(discussionId, noteObj) {
this.discussionId = discussionId;
this.id = noteObj.noteId;
this.canResolve = noteObj.canResolve;
this.resolved = noteObj.resolved;
this.resolved_by = noteObj.resolvedBy;
this.authorName = noteObj.authorName;
this.authorAvatar = noteObj.authorAvatar;
this.noteTruncated = noteObj.noteTruncated;
}
}
window.NoteModel = NoteModel;
/* global CommentsStore */
import Vue from 'vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
import { __ } from '~/locale';
window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
this.discussionResource = Vue.resource(
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
);
}
resolve(noteId) {
return this.noteResource.save({ noteId }, {});
}
unresolve(noteId) {
return this.noteResource.delete({ noteId }, {});
}
toggleResolveForDiscussion(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
const isResolved = discussion.isResolved();
let promise;
if (isResolved) {
promise = this.unResolveAll(mergeRequestId, discussionId);
} else {
promise = this.resolveAll(mergeRequestId, discussionId);
}
promise
.then(resp => resp.json())
.then(data => {
discussion.loading = false;
const resolvedBy = data ? data.resolved_by : null;
if (isResolved) {
discussion.unResolveAllNotes();
} else {
discussion.resolveAllNotes(resolvedBy);
}
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
})
.catch(
() =>
new Flash(__('An error occurred when trying to resolve a discussion. Please try again.')),
);
}
resolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
discussion.loading = true;
return this.discussionResource.save(
{
mergeRequestId,
discussionId,
},
{},
);
}
unResolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
discussion.loading = true;
return this.discussionResource.delete(
{
mergeRequestId,
discussionId,
},
{},
);
}
}
gl.DiffNotesResolveServiceClass = ResolveServiceClass;
/* eslint-disable no-restricted-syntax, guard-for-in */
/* global DiscussionModel */
import Vue from 'vue';
window.CommentsStore = {
state: {},
get(discussionId, noteId) {
return this.state[discussionId].getNote(noteId);
},
createDiscussion(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(noteObj) {
const discussion = this.createDiscussion(noteObj.discussionId);
discussion.createNote(noteObj);
},
update(discussionId, noteId, resolved, resolvedBy) {
const discussion = this.state[discussionId];
const note = discussion.getNote(noteId);
note.resolved = resolved;
note.resolved_by = resolvedBy;
},
delete(discussionId, noteId) {
const discussion = this.state[discussionId];
discussion.deleteNote(noteId);
if (discussion.notesCount() === 0) {
Vue.delete(this.state, discussionId);
}
},
unresolvedDiscussionIds() {
const ids = [];
for (const discussionId in this.state) {
const discussion = this.state[discussionId];
if (!discussion.isResolved()) {
ids.push(discussion.id);
}
}
return ids;
},
};
......@@ -396,10 +396,6 @@ export default class MergeRequestTabs {
initChangesDropdown(this.stickyTop);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
localTimeAgo($('.js-timeago', 'div#diffs'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
......
......@@ -479,11 +479,6 @@ export default class Notes {
row = form;
}
const lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
const diffAvatarContainer = row
.prevAll('.line_holder')
.first()
.find(`.js-avatar-container.${lineType}_line`);
// is this the first note of discussion?
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
if (!discussionContainer.length) {
......@@ -519,12 +514,6 @@ export default class Notes {
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
}
localTimeAgo($('.js-timeago'), false);
Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
......@@ -538,19 +527,6 @@ export default class Notes {
.get(0);
}
renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
let avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) {
avatarHolder = document.createElement('diff-note-avatars');
avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id);
diffAvatarContainer.append(avatarHolder);
gl.diffNotesCompileComponents();
}
}
/**
* Called in response the main target form has been successfully submitted.
*
......@@ -605,10 +581,6 @@ export default class Notes {
form.find('#note_type').val('');
form.find('#note_project_id').remove();
form.find('#in_reply_to_discussion_id').remove();
form
.find('.js-comment-resolve-button')
.closest('comment-and-resolve-btn')
.remove();
this.parentTimeline = form.parents('.timeline');
if (form.length) {
......@@ -714,10 +686,6 @@ export default class Notes {
$note_li.replaceWith($noteEntityEl);
this.setupNewNote($noteEntityEl);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
}
checkContentToAllowEditing($el) {
......@@ -844,12 +812,6 @@ export default class Notes {
const $notes = $note.closest('.discussion-notes');
const discussionId = $('.notes', $notes).data('discussionId');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) {
gl.diffNoteApps[noteElId].$destroy();
}
}
$note.remove();
// check if this is the last note for this line
......@@ -979,13 +941,6 @@ export default class Notes {
form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
const $commentBtn = form.find('comment-and-resolve-btn');
$commentBtn.attr(':discussion-id', `'${discussionID}'`);
gl.diffNotesCompileComponents();
}
form.find('.js-note-text').focus();
form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
}
......
......@@ -57,16 +57,10 @@ export default class SingleFileDiff {
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb);
......@@ -90,10 +84,6 @@ export default class SingleFileDiff {
}
this.collapsedContent.after(this.content);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
const $file = $(this.file);
FilesCommentButton.init($file);
......
- discussion = local_assigns.fetch(:discussion, nil)
- if current_user
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
.btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
%button.btn.gl-button.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
":title" => "buttonText",
":aria-label" => "buttonText",
data: { container: "body" } }
= custom_icon("next_discussion")
- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
.btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
= link_to custom_icon('icon_mr_issue'),
new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid),
title: 'Resolve all discussions in new issue',
aria: { label: 'Resolve all discussions in new issue' },
data: { container: 'body' },
class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip'
- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project)
%new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
"inline-template" => true }
.btn-group{ role: "group", "v-if" => "showButton" }
= link_to custom_icon('icon_mr_issue'),
new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id),
title: 'Resolve this thread in a new issue',
aria: { label: 'Resolve this thread in a new issue' },
data: { container: 'body' },
class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip'
......@@ -21,20 +21,6 @@
- if can_create_note?
%a.user-avatar-link.d-none.d-sm-block{ href: user_path(current_user) }
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
- if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
.discussion-with-resolve-btn
.btn-group.discussion-with-resolve-btn{ role: "group" }
.btn-group{ role: "group" }
= link_to_reply_discussion(discussion, line_type)
= render "discussions/resolve_all", discussion: discussion
.btn-group.discussion-actions
= render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
= render "discussions/jump_to_next", discussion: discussion
- else
.discussion-with-resolve-btn
= link_to_reply_discussion(discussion)
- elsif !current_user
......
......@@ -21,9 +21,6 @@
- else
= add_diff_note_button(line_code, diff_file.position(line), type)
%a{ href: "##{line_code}", data: { linenumber: link_text } }
- discussion = line_discussions.try(:first)
- if discussion && discussion.resolvable? && !plain
%diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
- if plain
......
......@@ -20,9 +20,6 @@
%td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } }
= add_diff_note_button(left_line_code, left_position, 'old')
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- discussion_left = discussions_left.try(:first)
- if discussion_left && discussion_left.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.rich_text)
- else
%td.old_line.diff-line-num.empty-cell
......@@ -41,9 +38,6 @@
%td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } }
= add_diff_note_button(right_line_code, right_position, 'new')
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- discussion_right = discussions_right.try(:first)
- if discussion_right && discussion_right.resolvable?
%diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.rich_text)
- else
%td.old_line.diff-line-num.empty-cell
......
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
= link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.reopenable?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
%comment-and-resolve-btn{ "inline-template" => true }
%button.btn.btn-nr.btn-default.gl-mr-3.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }}
#notes= render "shared/notes/notes_with_form", :autocomplete => true
<svg viewBox="0 0 20 19" ><path d="M15.21 7.783h-3.317c-.268 0-.472.218-.472.486v.953c0 .28.212.486.473.486h3.318v1.575c0 .36.233.452.52.23l3.06-2.37c.274-.213.286-.582 0-.804l-3.06-2.37c-.275-.213-.52-.12-.52.23v1.583zm.57-3.66c-1.558-1.22-3.783-1.98-6.254-1.98C4.816 2.143 1 4.91 1 8.333c0 1.964 1.256 3.715 3.216 4.846-.447 1.615-1.132 2.195-1.732 2.882-.142.174-.304.32-.256.56v.01c.047.213.218.368.41.368h.046c.37-.048.743-.116 1.085-.213 1.645-.425 3.13-1.22 4.377-2.34.447.048.913.077 1.38.077 2.092 0 4.01-.546 5.492-1.454-.416-.208-.798-.475-1.134-.792-1.227.63-2.743 1.008-4.36 1.008-.41 0-.828-.03-1.237-.078l-.543-.058-.41.368c-.78.696-1.655 1.248-2.616 1.654.248-.445.486-.977.667-1.664l.257-.928-.828-.484c-1.646-.948-2.598-2.32-2.598-3.763 0-2.69 3.35-4.952 7.308-4.952 1.893 0 3.647.518 4.962 1.353.393-.266.827-.473 1.29-.61z" /></svg>
......@@ -2675,12 +2675,6 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
msgid "An error occurred when trying to resolve a comment. Please try again."
msgstr ""
msgid "An error occurred when trying to resolve a discussion. Please try again."
msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
......@@ -14457,9 +14451,6 @@ msgstr ""
msgid "July"
msgstr ""
msgid "Jump to first unresolved thread"
msgstr ""
msgid "Jump to next unresolved thread"
msgstr ""
......@@ -21766,9 +21757,6 @@ msgstr ""
msgid "Resolved by %{name}"
msgstr ""
msgid "Resolved by %{resolvedByName}"
msgstr ""
msgid "Resolves IP addresses once and uses them to submit requests"
msgstr ""
......@@ -27251,9 +27239,6 @@ msgstr ""
msgid "Unable to load the merge request widget. Try reloading the page."
msgstr ""
msgid "Unable to resolve"
msgstr ""
msgid "Unable to save iteration. Please try again"
msgstr ""
......
/* global CommentsStore */
import '~/diff_notes/models/discussion';
import '~/diff_notes/models/note';
import '~/diff_notes/stores/comments';
function createDiscussion(noteId = 1, resolved = true) {
CommentsStore.create({
discussionId: 'a',
noteId,
canResolve: true,
resolved,
resolvedBy: 'test',
authorName: 'test',
authorAvatar: 'test',
noteTruncated: 'test...',
});
}
beforeEach(() => {
CommentsStore.state = {};
});
describe('New discussion', () => {
it('creates new discussion', () => {
expect(Object.keys(CommentsStore.state).length).toBe(0);
createDiscussion();
expect(Object.keys(CommentsStore.state).length).toBe(1);
});
it('creates new note in discussion', () => {
createDiscussion();
createDiscussion(2);
const discussion = CommentsStore.state.a;
expect(Object.keys(discussion.notes).length).toBe(2);
});
});
describe('Get note', () => {
beforeEach(() => {
createDiscussion();
});
it('gets note by ID', () => {
const note = CommentsStore.get('a', 1);
expect(note).toBeDefined();
expect(note.id).toBe(1);
});
});
describe('Delete discussion', () => {
beforeEach(() => {
createDiscussion();
});
it('deletes discussion by ID', () => {
CommentsStore.delete('a', 1);
expect(Object.keys(CommentsStore.state).length).toBe(0);
});
it('deletes discussion when no more notes', () => {
createDiscussion();
createDiscussion(2);
expect(Object.keys(CommentsStore.state).length).toBe(1);
expect(Object.keys(CommentsStore.state.a.notes).length).toBe(2);
CommentsStore.delete('a', 1);
CommentsStore.delete('a', 2);
expect(Object.keys(CommentsStore.state).length).toBe(0);
});
});
describe('Update note', () => {
beforeEach(() => {
createDiscussion();
});
it('updates note to be unresolved', () => {
CommentsStore.update('a', 1, false, 'test');
const note = CommentsStore.get('a', 1);
expect(note.resolved).toBe(false);
});
});
describe('Discussion resolved', () => {
beforeEach(() => {
createDiscussion();
});
it('is resolved with single note', () => {
const discussion = CommentsStore.state.a;
expect(discussion.isResolved()).toBe(true);
});
it('is unresolved with 2 notes', () => {
const discussion = CommentsStore.state.a;
createDiscussion(2, false);
expect(discussion.isResolved()).toBe(false);
});
it('is resolved with 2 notes', () => {
const discussion = CommentsStore.state.a;
createDiscussion(2);
expect(discussion.isResolved()).toBe(true);
});
it('resolve all notes', () => {
const discussion = CommentsStore.state.a;
createDiscussion(2, false);
discussion.resolveAllNotes();
expect(discussion.isResolved()).toBe(true);
});
it('unresolve all notes', () => {
const discussion = CommentsStore.state.a;
createDiscussion(2);
discussion.unResolveAllNotes();
expect(discussion.isResolved()).toBe(false);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment