329 lines
10 KiB
Vue
329 lines
10 KiB
Vue
<script>
|
|
import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
|
|
import { ApolloMutation } from 'vue-apollo';
|
|
import createFlash from '~/flash';
|
|
import { s__ } from '~/locale';
|
|
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
|
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
|
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
|
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
|
|
import { isLoggedIn } from '~/lib/utils/common_utils';
|
|
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
|
|
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
|
|
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
|
|
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
|
|
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
|
|
import allVersionsMixin from '../../mixins/all_versions';
|
|
import { hasErrors } from '../../utils/cache_update';
|
|
import { extractDesign } from '../../utils/design_management_utils';
|
|
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
|
|
import DesignNote from './design_note.vue';
|
|
import DesignNoteSignedOut from './design_note_signed_out.vue';
|
|
import DesignReplyForm from './design_reply_form.vue';
|
|
import ToggleRepliesWidget from './toggle_replies_widget.vue';
|
|
|
|
export default {
|
|
components: {
|
|
ApolloMutation,
|
|
DesignNote,
|
|
DesignNoteSignedOut,
|
|
ReplyPlaceholder,
|
|
DesignReplyForm,
|
|
GlIcon,
|
|
GlLoadingIcon,
|
|
GlLink,
|
|
ToggleRepliesWidget,
|
|
TimeAgoTooltip,
|
|
DesignNotePin,
|
|
},
|
|
directives: {
|
|
GlTooltip: GlTooltipDirective,
|
|
},
|
|
mixins: [allVersionsMixin],
|
|
props: {
|
|
discussion: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
noteableId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
designId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
markdownPreviewPath: {
|
|
type: String,
|
|
required: false,
|
|
default: '',
|
|
},
|
|
registerPath: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
signInPath: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
resolvedDiscussionsExpanded: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
discussionWithOpenForm: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
},
|
|
apollo: {
|
|
activeDiscussion: {
|
|
query: activeDiscussionQuery,
|
|
result({ data }) {
|
|
if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) {
|
|
return;
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
// We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists.
|
|
// We don't want scrollIntoView to be triggered from the discussion click itself.
|
|
if (this.$el && this.shouldScrollToDiscussion(data.activeDiscussion)) {
|
|
this.$el.scrollIntoView({
|
|
behavior: 'smooth',
|
|
inline: 'start',
|
|
});
|
|
}
|
|
});
|
|
},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
discussionComment: '',
|
|
isFormRendered: false,
|
|
activeDiscussion: {},
|
|
isResolving: false,
|
|
shouldChangeResolvedStatus: false,
|
|
areRepliesCollapsed: this.discussion.resolved,
|
|
isLoggedIn: isLoggedIn(),
|
|
};
|
|
},
|
|
computed: {
|
|
mutationPayload() {
|
|
return {
|
|
noteableId: this.noteableId,
|
|
body: this.discussionComment,
|
|
discussionId: this.discussion.id,
|
|
};
|
|
},
|
|
designVariables() {
|
|
return {
|
|
fullPath: this.projectPath,
|
|
iid: this.issueIid,
|
|
filenames: [this.$route.params.id],
|
|
atVersion: this.designsVersion,
|
|
};
|
|
},
|
|
isDiscussionActive() {
|
|
return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
|
|
},
|
|
resolveCheckboxText() {
|
|
return this.discussion.resolved
|
|
? s__('DesignManagement|Unresolve thread')
|
|
: s__('DesignManagement|Resolve thread');
|
|
},
|
|
firstNote() {
|
|
return this.discussion.notes[0];
|
|
},
|
|
discussionReplies() {
|
|
return this.discussion.notes.slice(1);
|
|
},
|
|
areRepliesShown() {
|
|
return !this.discussion.resolved || !this.areRepliesCollapsed;
|
|
},
|
|
resolveIconName() {
|
|
return this.discussion.resolved ? 'check-circle-filled' : 'check-circle';
|
|
},
|
|
isRepliesWidgetVisible() {
|
|
return this.discussion.resolved && this.discussionReplies.length > 0;
|
|
},
|
|
isReplyPlaceholderVisible() {
|
|
return this.areRepliesShown || !this.discussionReplies.length;
|
|
},
|
|
isFormVisible() {
|
|
return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id;
|
|
},
|
|
},
|
|
methods: {
|
|
onDone({ data: { createNote } }) {
|
|
if (hasErrors(createNote)) {
|
|
createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR });
|
|
}
|
|
this.discussionComment = '';
|
|
this.hideForm();
|
|
if (this.shouldChangeResolvedStatus) {
|
|
this.toggleResolvedStatus();
|
|
}
|
|
},
|
|
onCreateNoteError(err) {
|
|
this.$emit('create-note-error', err);
|
|
},
|
|
hideForm() {
|
|
this.isFormRendered = false;
|
|
this.discussionComment = '';
|
|
},
|
|
showForm() {
|
|
this.$emit('open-form', this.discussion.id);
|
|
this.isFormRendered = true;
|
|
},
|
|
toggleResolvedStatus() {
|
|
this.isResolving = true;
|
|
|
|
/**
|
|
* Get previous todo count
|
|
*/
|
|
const { defaultClient: client } = this.$apollo.provider.clients;
|
|
const sourceData = client.readQuery({
|
|
query: getDesignQuery,
|
|
variables: this.designVariables,
|
|
});
|
|
|
|
const design = extractDesign(sourceData);
|
|
const prevTodoCount = design.currentUserTodos?.nodes?.length || 0;
|
|
|
|
this.$apollo
|
|
.mutate({
|
|
mutation: toggleResolveDiscussionMutation,
|
|
variables: { id: this.discussion.id, resolve: !this.discussion.resolved },
|
|
})
|
|
.then(({ data }) => {
|
|
if (data.errors?.length > 0) {
|
|
this.$emit('resolve-discussion-error', data.errors[0]);
|
|
}
|
|
const newTodoCount =
|
|
data?.discussionToggleResolve?.discussion?.noteable?.currentUserTodos?.nodes?.length ||
|
|
0;
|
|
updateGlobalTodoCount(newTodoCount - prevTodoCount);
|
|
})
|
|
.catch((err) => {
|
|
this.$emit('resolve-discussion-error', err);
|
|
})
|
|
.finally(() => {
|
|
this.isResolving = false;
|
|
});
|
|
},
|
|
shouldScrollToDiscussion(activeDiscussion) {
|
|
const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
|
|
ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
|
|
ACTIVE_DISCUSSION_SOURCE_TYPES.url,
|
|
];
|
|
const { source } = activeDiscussion;
|
|
return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
|
|
},
|
|
},
|
|
createNoteMutation,
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="design-discussion-wrapper">
|
|
<design-note-pin :is-resolved="discussion.resolved" :label="discussion.index" />
|
|
<ul
|
|
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
|
|
data-qa-selector="design_discussion_content"
|
|
>
|
|
<design-note
|
|
:note="firstNote"
|
|
:markdown-preview-path="markdownPreviewPath"
|
|
:is-resolving="isResolving"
|
|
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
|
@error="$emit('update-note-error', $event)"
|
|
>
|
|
<template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
|
|
<button
|
|
v-gl-tooltip
|
|
:class="{ 'is-active': discussion.resolved }"
|
|
:title="resolveCheckboxText"
|
|
:aria-label="resolveCheckboxText"
|
|
class="line-resolve-btn note-action-button gl-mr-3"
|
|
data-testid="resolve-button"
|
|
@click.stop="toggleResolvedStatus"
|
|
>
|
|
<gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" />
|
|
<gl-loading-icon v-else size="sm" inline />
|
|
</button>
|
|
</template>
|
|
<template v-if="discussion.resolved" #resolved-status>
|
|
<p class="gl-text-gray-500 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
|
|
{{ __('Resolved by') }}
|
|
<gl-link
|
|
class="gl-text-gray-500 gl-text-decoration-none gl-font-sm link-inherit-color"
|
|
:href="discussion.resolvedBy.webUrl"
|
|
target="_blank"
|
|
>{{ discussion.resolvedBy.name }}</gl-link
|
|
>
|
|
<time-ago-tooltip :time="discussion.resolvedAt" tooltip-placement="bottom" />
|
|
</p>
|
|
</template>
|
|
</design-note>
|
|
<toggle-replies-widget
|
|
v-if="isRepliesWidgetVisible"
|
|
:collapsed="areRepliesCollapsed"
|
|
:replies="discussionReplies"
|
|
@toggle="areRepliesCollapsed = !areRepliesCollapsed"
|
|
/>
|
|
<design-note
|
|
v-for="note in discussionReplies"
|
|
v-show="areRepliesShown"
|
|
:key="note.id"
|
|
:note="note"
|
|
:markdown-preview-path="markdownPreviewPath"
|
|
:is-resolving="isResolving"
|
|
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
|
@error="$emit('update-note-error', $event)"
|
|
/>
|
|
<li
|
|
v-show="isReplyPlaceholderVisible"
|
|
class="reply-wrapper discussion-reply-holder"
|
|
:class="{ 'gl-bg-gray-10': !isLoggedIn }"
|
|
>
|
|
<template v-if="!isLoggedIn">
|
|
<design-note-signed-out :register-path="registerPath" :sign-in-path="signInPath" />
|
|
</template>
|
|
<template v-else>
|
|
<reply-placeholder
|
|
v-if="!isFormVisible"
|
|
class="qa-discussion-reply"
|
|
:placeholder-text="__('Reply…')"
|
|
@focus="showForm"
|
|
/>
|
|
<apollo-mutation
|
|
v-else
|
|
#default="{ mutate, loading }"
|
|
:mutation="$options.createNoteMutation"
|
|
:variables="{
|
|
input: mutationPayload,
|
|
}"
|
|
@done="onDone"
|
|
@error="onCreateNoteError"
|
|
>
|
|
<design-reply-form
|
|
v-model="discussionComment"
|
|
:is-saving="loading"
|
|
:markdown-preview-path="markdownPreviewPath"
|
|
@submit-form="mutate"
|
|
@cancel-form="hideForm"
|
|
>
|
|
<template v-if="discussion.resolvable" #resolve-checkbox>
|
|
<label data-testid="resolve-checkbox">
|
|
<input v-model="shouldChangeResolvedStatus" type="checkbox" />
|
|
{{ resolveCheckboxText }}
|
|
</label>
|
|
</template>
|
|
</design-reply-form>
|
|
</apollo-mutation>
|
|
</template>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|