diff options
Diffstat (limited to 'app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue')
-rw-r--r-- | app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue new file mode 100644 index 00000000000..6a20517eed7 --- /dev/null +++ b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue @@ -0,0 +1,297 @@ +<script> +import { ApolloMutation } from 'vue-apollo'; +import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import allVersionsMixin from '../../mixins/all_versions'; +import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql'; +import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql'; +import getDesignQuery from '../../graphql/queries/get_design.query.graphql'; +import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql'; +import DesignNote from './design_note.vue'; +import DesignReplyForm from './design_reply_form.vue'; +import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update'; +import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; +import ToggleRepliesWidget from './toggle_replies_widget.vue'; + +export default { + components: { + ApolloMutation, + DesignNote, + ReplyPlaceholder, + DesignReplyForm, + GlIcon, + GlLoadingIcon, + GlLink, + ToggleRepliesWidget, + TimeAgoTooltip, + }, + 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: '', + }, + resolvedDiscussionsExpanded: { + type: Boolean, + required: true, + }, + discussionWithOpenForm: { + type: String, + required: true, + }, + }, + apollo: { + activeDiscussion: { + query: activeDiscussionQuery, + result({ data }) { + const discussionId = data.activeDiscussion.id; + if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) { + return; + } + // 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 ( + discussionId && + data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin && + discussionId === this.discussion.notes[0].id + ) { + this.$el.scrollIntoView({ + behavior: 'smooth', + inline: 'start', + }); + } + }, + }, + }, + data() { + return { + discussionComment: '', + isFormRendered: false, + activeDiscussion: {}, + isResolving: false, + shouldChangeResolvedStatus: false, + areRepliesCollapsed: this.discussion.resolved, + }; + }, + 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, + }; + }, + isDiscussionHighlighted() { + return this.discussion.notes[0].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: { + addDiscussionComment( + store, + { + data: { createNote }, + }, + ) { + updateStoreAfterAddDiscussionComment( + store, + createNote, + getDesignQuery, + this.designVariables, + this.discussion.id, + ); + }, + onDone() { + this.discussionComment = ''; + this.hideForm(); + if (this.shouldChangeResolvedStatus) { + this.toggleResolvedStatus(); + } + }, + onCreateNoteError(err) { + this.$emit('createNoteError', err); + }, + hideForm() { + this.isFormRendered = false; + this.discussionComment = ''; + }, + showForm() { + this.$emit('openForm', this.discussion.id); + this.isFormRendered = true; + }, + toggleResolvedStatus() { + this.isResolving = true; + this.$apollo + .mutate({ + mutation: toggleResolveDiscussionMutation, + variables: { id: this.discussion.id, resolve: !this.discussion.resolved }, + }) + .then(({ data }) => { + if (data.errors?.length > 0) { + this.$emit('resolveDiscussionError', data.errors[0]); + } + }) + .catch(err => { + this.$emit('resolveDiscussionError', err); + }) + .finally(() => { + this.isResolving = false; + }); + }, + }, + createNoteMutation, +}; +</script> + +<template> + <div class="design-discussion-wrapper"> + <div + class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center" + :class="{ resolved: discussion.resolved }" + type="button" + > + {{ discussion.index }} + </div> + <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': isDiscussionHighlighted }" + @error="$emit('updateNoteError', $event)" + > + <template v-if="discussion.resolvable" #resolveDiscussion> + <button + v-gl-tooltip + :class="{ 'is-active': discussion.resolved }" + :title="resolveCheckboxText" + :aria-label="resolveCheckboxText" + type="button" + 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 inline /> + </button> + </template> + <template v-if="discussion.resolved" #resolvedStatus> + <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': isDiscussionHighlighted }" + @error="$emit('updateNoteError', $event)" + /> + <li v-show="isReplyPlaceholderVisible" class="reply-wrapper"> + <reply-placeholder + v-if="!isFormVisible" + class="qa-discussion-reply" + :button-text="__('Reply...')" + @onClick="showForm" + /> + <apollo-mutation + v-else + #default="{ mutate, loading }" + :mutation="$options.createNoteMutation" + :variables="{ + input: mutationPayload, + }" + :update="addDiscussionComment" + @done="onDone" + @error="onCreateNoteError" + > + <design-reply-form + v-model="discussionComment" + :is-saving="loading" + :markdown-preview-path="markdownPreviewPath" + @submitForm="mutate" + @cancelForm="hideForm" + > + <template v-if="discussion.resolvable" #resolveCheckbox> + <label data-testid="resolve-checkbox"> + <input v-model="shouldChangeResolvedStatus" type="checkbox" /> + {{ resolveCheckboxText }} + </label> + </template> + </design-reply-form> + </apollo-mutation> + </li> + </ul> + </div> +</template> |