diff options
Diffstat (limited to 'app/assets/javascripts/design_management/components/design_notes')
4 files changed, 239 insertions, 32 deletions
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue index c6c5ee88a93..7e442bb295f 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue @@ -1,14 +1,19 @@ <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/createNote.mutation.graphql'; +import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql'; import getDesignQuery from '../../graphql/queries/getDesign.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: { @@ -16,6 +21,14 @@ export default { DesignNote, ReplyPlaceholder, DesignReplyForm, + GlIcon, + GlLoadingIcon, + GlLink, + ToggleRepliesWidget, + TimeAgoTooltip, + }, + directives: { + GlTooltip: GlTooltipDirective, }, mixins: [allVersionsMixin], props: { @@ -31,21 +44,28 @@ export default { type: String, required: true, }, - discussionIndex: { - type: Number, - 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 ( @@ -66,6 +86,9 @@ export default { discussionComment: '', isFormRendered: false, activeDiscussion: {}, + isResolving: false, + shouldChangeResolvedStatus: false, + areRepliesCollapsed: this.discussion.resolved, }; }, computed: { @@ -87,6 +110,32 @@ export default { 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( @@ -106,17 +155,40 @@ export default { onDone() { this.discussionComment = ''; this.hideForm(); + if (this.shouldChangeResolvedStatus) { + this.toggleResolvedStatus(); + } }, - onError(err) { - this.$emit('error', err); + 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, }; @@ -124,22 +196,71 @@ export default { <template> <div class="design-discussion-wrapper"> - <div class="badge badge-pill" type="button">{{ discussionIndex }}</div> <div - class="design-discussion bordered-box position-relative" + 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 - v-for="note in discussion.notes" + :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-700 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message"> + {{ __('Resolved by') }} + <gl-link + class="gl-text-gray-700 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)" /> - <div class="reply-wrapper"> + <li v-show="isReplyPlaceholderVisible" class="reply-wrapper"> <reply-placeholder - v-if="!isFormRendered" + v-if="!isFormVisible" class="qa-discussion-reply" :button-text="__('Reply...')" @onClick="showForm" @@ -153,7 +274,7 @@ export default { }" :update="addDiscussionComment" @done="onDone" - @error="onError" + @error="onCreateNoteError" > <design-reply-form v-model="discussionComment" @@ -161,9 +282,16 @@ export default { :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> - </div> - </div> + </li> + </ul> </div> </template> diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue index c1c19c0a597..b1f3a43a66d 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue @@ -54,6 +54,9 @@ export default { body: this.noteText, }; }, + isEditButtonVisible() { + return !this.isEditing && this.note.userPermissions.adminNote; + }, }, mounted() { if (this.isNoteLinked) { @@ -107,23 +110,28 @@ export default { </template> </span> </div> - <button - v-if="!isEditing && note.userPermissions.adminNote" - v-gl-tooltip - type="button" - title="Edit comment" - class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" - @click="isEditing = true" - > - <gl-icon name="pencil" class="link-highlight" /> - </button> + <div class="gl-display-flex"> + <slot name="resolveDiscussion"></slot> + <button + v-if="isEditButtonVisible" + v-gl-tooltip + type="button" + :title="__('Edit comment')" + class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" + @click="isEditing = true" + > + <gl-icon name="pencil" class="link-highlight" /> + </button> + </div> </div> - <div - v-if="!isEditing" - class="note-text js-note-text md" - data-qa-selector="note_content" - v-html="note.bodyHtml" - ></div> + <template v-if="!isEditing"> + <div + class="note-text js-note-text md" + data-qa-selector="note_content" + v-html="note.bodyHtml" + ></div> + <slot name="resolvedStatus"></slot> + </template> <apollo-mutation v-else #default="{ mutate, loading }" diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue index 40be9867fee..756da7f55aa 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue @@ -107,7 +107,8 @@ export default { </textarea> </template> </markdown-field> - <div class="note-form-actions d-flex justify-content-between"> + <slot name="resolveCheckbox"></slot> + <div class="note-form-actions gl-display-flex gl-justify-content-space-between"> <gl-deprecated-button ref="submitButton" :disabled="!hasValue || isSaving" diff --git a/app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue new file mode 100644 index 00000000000..46c73e3eea8 --- /dev/null +++ b/app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue @@ -0,0 +1,70 @@ +<script> +import { GlIcon, GlButton, GlLink } from '@gitlab/ui'; +import { __, n__ } from '~/locale'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + name: 'ToggleNotesWidget', + components: { + GlIcon, + GlButton, + GlLink, + TimeAgoTooltip, + }, + props: { + collapsed: { + type: Boolean, + required: true, + }, + replies: { + type: Array, + required: true, + }, + }, + computed: { + lastReply() { + return this.replies[this.replies.length - 1]; + }, + iconName() { + return this.collapsed ? 'chevron-right' : 'chevron-down'; + }, + toggleText() { + return this.collapsed + ? `${this.replies.length} ${n__('reply', 'replies', this.replies.length)}` + : __('Collapse replies'); + }, + }, +}; +</script> + +<template> + <li + class="toggle-comments gl-bg-gray-50 gl-display-flex gl-align-items-center gl-py-3" + :class="{ expanded: !collapsed }" + data-testid="toggle-comments-wrapper" + > + <gl-icon :name="iconName" class="gl-ml-3" @click.stop="$emit('toggle')" /> + <gl-button + variant="link" + class="toggle-comments-button gl-ml-2 gl-mr-2" + @click.stop="$emit('toggle')" + > + {{ toggleText }} + </gl-button> + <template v-if="collapsed"> + <span class="gl-text-gray-700">{{ __('Last reply by') }}</span> + <gl-link + :href="lastReply.author.webUrl" + target="_blank" + class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2" + > + {{ lastReply.author.name }} + </gl-link> + <time-ago-tooltip + :time="lastReply.createdAt" + tooltip-placement="bottom" + class="gl-text-gray-700" + /> + </template> + </li> +</template> |