debian-mirror-gitlab/app/assets/javascripts/notes/components/noteable_discussion.vue

514 lines
16 KiB
Vue
Raw Normal View History

2018-03-17 18:26:18 +05:30
<script>
2019-02-15 15:39:39 +05:30
import _ from 'underscore';
2018-05-09 12:01:36 +05:30
import { mapActions, mapGetters } from 'vuex';
2019-02-15 15:39:39 +05:30
import { GlTooltipDirective } from '@gitlab/ui';
2018-11-08 19:23:39 +05:30
import { truncateSha } from '~/lib/utils/text_utility';
2019-02-15 15:39:39 +05:30
import { s__, __, sprintf } from '~/locale';
2019-07-07 11:18:12 +05:30
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
2018-12-13 13:39:08 +05:30
import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
2019-07-07 11:18:12 +05:30
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
2019-02-15 15:39:39 +05:30
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
2018-05-09 12:01:36 +05:30
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
2019-03-02 22:35:43 +05:30
import resolveDiscussionButton from './discussion_resolve_button.vue';
2018-12-13 13:39:08 +05:30
import toggleRepliesWidget from './toggle_replies_widget.vue';
2018-05-09 12:01:36 +05:30
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
2018-11-18 11:00:15 +05:30
import discussionNavigation from '../mixins/discussion_navigation';
2019-03-02 22:35:43 +05:30
import ReplyPlaceholder from './discussion_reply_placeholder.vue';
2019-07-07 11:18:12 +05:30
import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
2019-03-02 22:35:43 +05:30
import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
2019-07-07 11:18:12 +05:30
import eventHub from '../event_hub';
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
export default {
2018-11-08 19:23:39 +05:30
name: 'NoteableDiscussion',
2018-05-09 12:01:36 +05:30
components: {
2018-12-13 13:39:08 +05:30
icon,
2018-05-09 12:01:36 +05:30
noteableNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
noteEditedText,
noteForm,
2019-03-02 22:35:43 +05:30
resolveDiscussionButton,
jumpToNextDiscussionButton,
2018-12-13 13:39:08 +05:30
toggleRepliesWidget,
2019-03-02 22:35:43 +05:30
ReplyPlaceholder,
2018-05-09 12:01:36 +05:30
placeholderNote,
placeholderSystemNote,
2019-07-07 11:18:12 +05:30
ResolveWithIssueButton,
2018-11-08 19:23:39 +05:30
systemNote,
2019-07-07 11:18:12 +05:30
DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'),
2019-02-15 15:39:39 +05:30
TimelineEntryItem,
2018-05-09 12:01:36 +05:30
},
directives: {
2019-02-15 15:39:39 +05:30
GlTooltip: GlTooltipDirective,
2018-05-09 12:01:36 +05:30
},
2019-07-07 11:18:12 +05:30
mixins: [noteable, resolvable, discussionNavigation, diffLineNoteFormMixin],
2018-05-09 12:01:36 +05:30
props: {
2018-11-08 19:23:39 +05:30
discussion: {
2018-05-09 12:01:36 +05:30
type: Object,
required: true,
},
2019-02-15 15:39:39 +05:30
line: {
type: Object,
required: false,
default: null,
},
2018-11-08 19:23:39 +05:30
renderDiffFile: {
type: Boolean,
required: false,
default: true,
},
alwaysExpanded: {
type: Boolean,
required: false,
default: false,
},
2018-11-18 11:00:15 +05:30
discussionsByDiffOrder: {
type: Boolean,
required: false,
default: false,
},
2019-02-15 15:39:39 +05:30
helpPagePath: {
type: String,
required: false,
default: '',
},
2018-05-09 12:01:36 +05:30
},
data() {
return {
isReplying: false,
isResolving: false,
resolveAsThread: true,
};
},
computed: {
...mapGetters([
2019-07-07 11:18:12 +05:30
'convertedDisscussionIds',
2018-05-09 12:01:36 +05:30
'getNoteableData',
2018-11-18 11:00:15 +05:30
'nextUnresolvedDiscussionId',
2019-02-15 15:39:39 +05:30
'unresolvedDiscussionsCount',
'hasUnresolvedDiscussions',
'showJumpToNextDiscussion',
2018-05-09 12:01:36 +05:30
]),
author() {
2019-07-07 11:18:12 +05:30
return this.firstNote.author;
},
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
2018-05-09 12:01:36 +05:30
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
},
newNotePath() {
return this.getNoteableData.create_note_path;
},
2018-12-13 13:39:08 +05:30
hasReplies() {
return this.discussion.notes.length > 1;
},
2019-07-07 11:18:12 +05:30
firstNote() {
2018-12-13 13:39:08 +05:30
return this.discussion.notes.slice(0, 1)[0];
},
replies() {
return this.discussion.notes.slice(1);
},
2018-05-09 12:01:36 +05:30
lastUpdatedBy() {
2018-11-08 19:23:39 +05:30
const { notes } = this.discussion;
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (notes.length > 1) {
return notes[notes.length - 1].author;
}
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
return null;
},
lastUpdatedAt() {
2018-11-08 19:23:39 +05:30
const { notes } = this.discussion;
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (notes.length > 1) {
return notes[notes.length - 1].created_at;
}
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
return null;
},
2018-11-08 19:23:39 +05:30
resolvedText() {
2019-02-15 15:39:39 +05:30
return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
2019-01-03 12:48:30 +05:30
},
2019-02-15 15:39:39 +05:30
shouldShowJumpToNextDiscussion() {
return this.showJumpToNextDiscussion(
this.discussion.id,
this.discussionsByDiffOrder ? 'diff' : 'discussion',
2018-11-20 20:47:30 +05:30
);
2018-11-18 11:00:15 +05:30
},
2018-11-08 19:23:39 +05:30
shouldRenderDiffs() {
2019-02-15 15:39:39 +05:30
return this.discussion.diff_discussion && this.renderDiffFile;
2018-05-09 12:01:36 +05:30
},
2018-12-13 13:39:08 +05:30
shouldGroupReplies() {
2019-02-15 15:39:39 +05:30
return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
2018-12-13 13:39:08 +05:30
},
2018-05-09 12:01:36 +05:30
wrapperComponent() {
2018-11-08 19:23:39 +05:30
return this.shouldRenderDiffs ? diffWithNote : 'div';
},
wrapperComponentProps() {
if (this.shouldRenderDiffs) {
2019-02-15 15:39:39 +05:30
return { discussion: this.discussion };
2018-11-08 19:23:39 +05:30
}
return {};
2018-05-09 12:01:36 +05:30
},
2018-12-13 13:39:08 +05:30
componentClassName() {
if (this.shouldRenderDiffs) {
if (!this.lastUpdatedAt && !this.discussion.resolved) {
return 'unresolved';
}
}
return '';
},
2019-07-07 11:18:12 +05:30
isExpanded() {
return this.discussion.expanded || this.alwaysExpanded;
},
shouldHideDiscussionBody() {
return this.shouldRenderDiffs && !this.isExpanded;
2018-12-13 13:39:08 +05:30
},
2019-02-15 15:39:39 +05:30
actionText() {
const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
const linkEnd = '</a>';
2018-12-13 13:39:08 +05:30
2019-02-15 15:39:39 +05:30
let { commit_id: commitId } = this.discussion;
if (commitId) {
commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
}
const {
for_commit: isForCommit,
diff_discussion: isDiffDiscussion,
active: isActive,
} = this.discussion;
let text = s__('MergeRequests|started a discussion');
if (isForCommit) {
text = s__(
'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}',
);
} else if (isDiffDiscussion && commitId) {
text = isActive
? s__('MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}')
: s__(
'MergeRequests|started a discussion on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}',
);
} else if (isDiffDiscussion) {
text = isActive
? s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}')
: s__(
'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
);
}
return sprintf(text, { commitId, linkStart, linkEnd }, false);
},
diffLine() {
2019-03-02 22:35:43 +05:30
if (this.line) {
return this.line;
}
2019-02-15 15:39:39 +05:30
if (this.discussion.diff_discussion && this.discussion.truncated_diff_lines) {
return this.discussion.truncated_diff_lines.slice(-1)[0];
}
2019-03-02 22:35:43 +05:30
return null;
},
commit() {
if (!this.discussion.for_commit) {
return null;
}
return {
id: this.discussion.commit_id,
url: this.discussion.discussion_path,
};
2018-12-13 13:39:08 +05:30
},
2019-07-07 11:18:12 +05:30
resolveWithIssuePath() {
return !this.discussionResolved && this.discussion.resolve_with_issue_path;
2019-05-30 16:15:17 +05:30
},
2019-05-18 00:54:41 +05:30
},
2019-07-07 11:18:12 +05:30
created() {
eventHub.$on('startReplying', this.onStartReplying);
},
beforeDestroy() {
eventHub.$off('startReplying', this.onStartReplying);
},
2018-05-09 12:01:36 +05:30
methods: {
...mapActions([
'saveNote',
'toggleDiscussion',
'removePlaceholderNotes',
'toggleResolveNote',
2018-11-08 19:23:39 +05:30
'expandDiscussion',
2019-07-07 11:18:12 +05:30
'removeConvertedDiscussion',
2018-05-09 12:01:36 +05:30
]),
2018-11-08 19:23:39 +05:30
truncateSha,
2018-05-09 12:01:36 +05:30
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
return placeholderSystemNote;
2018-03-17 18:26:18 +05:30
}
2018-12-05 23:21:45 +05:30
2018-05-09 12:01:36 +05:30
return placeholderNote;
2018-03-17 18:26:18 +05:30
}
2018-05-09 12:01:36 +05:30
2018-11-08 19:23:39 +05:30
if (note.system) {
return systemNote;
}
2018-05-09 12:01:36 +05:30
return noteableNote;
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
componentData(note) {
2018-12-05 23:21:45 +05:30
return note.isPlaceholderNote ? note.notes[0] : note;
2018-05-09 12:01:36 +05:30
},
toggleDiscussionHandler() {
2018-11-08 19:23:39 +05:30
this.toggleDiscussion({ discussionId: this.discussion.id });
2018-05-09 12:01:36 +05:30
},
showReplyForm() {
this.isReplying = true;
},
2018-11-18 11:00:15 +05:30
cancelReplyForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
2018-05-09 12:01:36 +05:30
// eslint-disable-next-line no-alert
2018-11-18 11:00:15 +05:30
if (!window.confirm(msg)) {
2018-05-09 12:01:36 +05:30
return;
2018-03-17 18:26:18 +05:30
}
2018-05-09 12:01:36 +05:30
}
2018-03-17 18:26:18 +05:30
2019-07-07 11:18:12 +05:30
if (this.convertedDisscussionIds.includes(this.discussion.id)) {
this.removeConvertedDiscussion(this.discussion.id);
}
2018-05-09 12:01:36 +05:30
this.isReplying = false;
2019-07-07 11:18:12 +05:30
clearDraft(this.autosaveKey);
2018-05-09 12:01:36 +05:30
},
saveReply(noteText, form, callback) {
2018-11-08 19:23:39 +05:30
const postData = {
in_reply_to_discussion_id: this.discussion.reply_id,
target_type: this.getNoteableData.targetType,
note: { note: noteText },
};
2019-07-07 11:18:12 +05:30
if (this.convertedDisscussionIds.includes(this.discussion.id)) {
postData.return_discussion = true;
}
2018-11-08 19:23:39 +05:30
if (this.discussion.for_commit) {
postData.note_project_id = this.discussion.project_id;
}
2018-05-09 12:01:36 +05:30
const replyData = {
endpoint: this.newNotePath,
flashContainer: this.$el,
2018-11-08 19:23:39 +05:30
data: postData,
2018-05-09 12:01:36 +05:30
};
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
this.isReplying = false;
2018-05-09 12:01:36 +05:30
this.saveNote(replyData)
.then(() => {
2019-07-07 11:18:12 +05:30
clearDraft(this.autosaveKey);
2018-05-09 12:01:36 +05:30
callback();
})
.catch(err => {
this.removePlaceholderNotes();
this.isReplying = true;
this.$nextTick(() => {
const msg = `Your comment could not be submitted!
2018-03-17 18:26:18 +05:30
Please check your network connection and try again.`;
2018-05-09 12:01:36 +05:30
Flash(msg, 'alert', this.$el);
this.$refs.noteForm.note = noteText;
callback(err);
2018-03-17 18:26:18 +05:30
});
2018-05-09 12:01:36 +05:30
});
},
2018-11-08 19:23:39 +05:30
jumpToNextDiscussion() {
2018-11-20 20:47:30 +05:30
const nextId = this.nextUnresolvedDiscussionId(
this.discussion.id,
this.discussionsByDiffOrder,
);
2018-03-27 19:54:05 +05:30
2018-11-18 11:00:15 +05:30
this.jumpToDiscussion(nextId);
2018-03-17 18:26:18 +05:30
},
2018-11-20 20:47:30 +05:30
deleteNoteHandler(note) {
this.$emit('noteDeleted', this.discussion, note);
},
2019-07-07 11:18:12 +05:30
onStartReplying(discussionId) {
if (this.discussion.id === discussionId) {
this.showReplyForm();
}
},
2018-05-09 12:01:36 +05:30
},
};
2018-03-17 18:26:18 +05:30
</script>
<template>
2019-02-15 15:39:39 +05:30
<timeline-entry-item class="note note-discussion" :class="componentClassName">
<div class="timeline-content">
<div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
<div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
<div v-once class="timeline-icon">
<user-avatar-link
v-if="author"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40"
2018-03-17 18:26:18 +05:30
/>
</div>
2019-07-07 11:18:12 +05:30
<div class="timeline-content">
<note-header
:author="author"
:created-at="firstNote.created_at"
:note-id="firstNote.id"
:include-toggle="true"
:expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
>
<span v-html="actionText"></span>
</note-header>
<note-edited-text
v-if="discussion.resolved"
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline"
/>
<note-edited-text
v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
action-text="Last updated"
class-name="discussion-headline-light js-discussion-headline"
/>
</div>
2019-02-15 15:39:39 +05:30
</div>
2019-07-07 11:18:12 +05:30
<div v-if="!shouldHideDiscussionBody" class="discussion-body">
2019-02-15 15:39:39 +05:30
<component
:is="wrapperComponent"
v-bind="wrapperComponentProps"
class="card discussion-wrapper"
>
<div class="discussion-notes">
<ul class="notes">
<template v-if="shouldGroupReplies">
<component
2019-07-07 11:18:12 +05:30
:is="componentName(firstNote)"
:note="componentData(firstNote)"
2019-02-15 15:39:39 +05:30
:line="line"
2019-03-02 22:35:43 +05:30
:commit="commit"
2019-02-15 15:39:39 +05:30
:help-page-path="helpPagePath"
2019-03-02 22:35:43 +05:30
:show-reply-button="canReply"
2019-02-15 15:39:39 +05:30
@handleDeleteNote="deleteNoteHandler"
2019-07-07 11:18:12 +05:30
@startReplying="showReplyForm"
2019-02-15 15:39:39 +05:30
>
<note-edited-text
v-if="discussion.resolved"
slot="discussion-resolved-text"
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
2019-01-03 12:48:30 +05:30
/>
2019-02-15 15:39:39 +05:30
<slot slot="avatar-badge" name="avatar-badge"></slot>
</component>
<toggle-replies-widget
v-if="hasReplies"
2019-07-07 11:18:12 +05:30
:collapsed="!isExpanded"
2019-02-15 15:39:39 +05:30
:replies="replies"
2019-07-07 11:18:12 +05:30
@toggle="toggleDiscussionHandler"
2019-02-15 15:39:39 +05:30
/>
2019-07-07 11:18:12 +05:30
<template v-if="isExpanded">
2018-12-13 13:39:08 +05:30
<component
:is="componentName(note)"
2019-02-15 15:39:39 +05:30
v-for="note in replies"
2018-12-13 13:39:08 +05:30
:key="note.id"
:note="componentData(note)"
2019-02-15 15:39:39 +05:30
:help-page-path="helpPagePath"
:line="line"
2018-12-13 13:39:08 +05:30
@handleDeleteNote="deleteNoteHandler"
2019-02-15 15:39:39 +05:30
/>
2019-01-03 12:48:30 +05:30
</template>
2019-02-15 15:39:39 +05:30
</template>
<template v-else>
<component
:is="componentName(note)"
v-for="(note, index) in discussion.notes"
:key="note.id"
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="diffLine"
@handleDeleteNote="deleteNoteHandler"
>
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
</component>
</template>
</ul>
<div
2019-07-07 11:18:12 +05:30
v-if="isExpanded || !hasReplies"
2019-02-15 15:39:39 +05:30
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
>
<template v-if="!isReplying && canReply">
<div class="discussion-with-resolve-btn">
2019-03-02 22:35:43 +05:30
<reply-placeholder class="qa-discussion-reply" @onClick="showReplyForm" />
<resolve-discussion-button
v-if="discussion.resolvable"
:is-resolving="isResolving"
:button-title="resolveButtonTitle"
@onClick="resolveHandler"
/>
2019-02-15 15:39:39 +05:30
<div
v-if="discussion.resolvable"
class="btn-group discussion-actions ml-sm-2"
role="group"
>
2019-07-07 11:18:12 +05:30
<resolve-with-issue-button
v-if="resolveWithIssuePath"
:url="resolveWithIssuePath"
/>
2019-03-02 22:35:43 +05:30
<jump-to-next-discussion-button
v-if="shouldShowJumpToNextDiscussion"
@onClick="jumpToNextDiscussion"
/>
2018-03-27 19:54:05 +05:30
</div>
2019-02-15 15:39:39 +05:30
</div>
</template>
<note-form
v-if="isReplying"
ref="noteForm"
:discussion="discussion"
:is-editing="false"
:line="diffLine"
save-button-title="Comment"
2019-07-07 11:18:12 +05:30
:autosave-key="autosaveKey"
@handleFormUpdateAddToReview="addReplyToReview"
2019-02-15 15:39:39 +05:30
@handleFormUpdate="saveReply"
@cancelForm="cancelReplyForm"
/>
<note-signed-out-widget v-if="!canReply" />
2018-03-17 18:26:18 +05:30
</div>
2019-02-15 15:39:39 +05:30
</div>
</component>
2018-03-17 18:26:18 +05:30
</div>
</div>
</div>
2019-02-15 15:39:39 +05:30
</timeline-entry-item>
2018-03-17 18:26:18 +05:30
</template>