diff options
Diffstat (limited to 'app/assets/javascripts/design_management/components/design_notes')
4 files changed, 185 insertions, 10 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 5affd448419..45f33967476 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 @@ -292,7 +292,9 @@ export default { <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" + :class="{ 'gl-bg-blue-50': isDiscussionActive }" data-qa-selector="design_discussion_content" + data-testid="design-discussion-content" > <design-note :note="firstNote" @@ -300,7 +302,7 @@ export default { :is-resolving="isResolving" :is-discussion="true" :noteable-id="noteableId" - :class="{ 'gl-bg-blue-50': isDiscussionActive }" + :design-variables="designVariables" @delete-note="showDeleteNoteConfirmationModal($event)" > <template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion> @@ -343,7 +345,7 @@ export default { :is-resolving="isResolving" :noteable-id="noteableId" :is-discussion="false" - :class="{ 'gl-bg-blue-50': isDiscussionActive }" + :design-variables="designVariables" @delete-note="showDeleteNoteConfirmationModal($event)" /> <li 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 0eac2cad68d..1f2c9f19a95 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 @@ -7,14 +7,21 @@ import { GlLink, GlTooltipDirective, } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { produce } from 'immer'; import SafeHtml from '~/vue_shared/directives/safe_html'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_USER } from '~/graphql_shared/constants'; import { __ } from '~/locale'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import EmojiPicker from '~/emoji/components/picker.vue'; +import getDesignQuery from '../../graphql/queries/get_design.query.graphql'; import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql'; +import designNoteAwardEmojiToggleMutation from '../../graphql/mutations/design_note_award_emoji_toggle.mutation.graphql'; import { hasErrors } from '../../utils/cache_update'; import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils'; +import DesignNoteAwardsList from './design_note_awards_list.vue'; import DesignReplyForm from './design_reply_form.vue'; export default { @@ -24,7 +31,9 @@ export default { deleteCommentText: __('Delete comment'), }, components: { + DesignNoteAwardsList, DesignReplyForm, + EmojiPicker, GlAvatar, GlAvatarLink, GlButton, @@ -37,6 +46,7 @@ export default { GlTooltip: GlTooltipDirective, SafeHtml, }, + inject: ['issueIid', 'projectPath'], props: { note: { type: Object, @@ -56,6 +66,10 @@ export default { type: String, required: true, }, + designVariables: { + type: Object, + required: true, + }, }, data() { return { @@ -64,6 +78,26 @@ export default { }; }, computed: { + currentUserId() { + return window.gon.current_user_id; + }, + currentUserFullName() { + return window.gon.current_user_fullname; + }, + canAwardEmoji() { + return this.note.userPermissions.awardEmoji; + }, + awards() { + return this.note.awardEmoji.nodes.map((award) => { + return { + ...award, + user: { + ...award.user, + id: getIdFromGraphQLId(award.user.id), + }, + }; + }); + }, author() { return this.note.author; }, @@ -124,6 +158,93 @@ export default { this.$emit('error', data.errors[0]); } }, + isEmojiPresentForCurrentUser(name) { + return ( + this.awards.findIndex( + (emoji) => emoji.name === name && emoji.user.id === this.currentUserId, + ) > -1 + ); + }, + /** + * Prepare award emoji nodes based on emoji name + * and whether the user has toggled the emoji off or on + */ + getAwardEmojiNodes(name, toggledOn) { + // If the emoji toggled on, add the emoji + if (toggledOn) { + // If emoji is already present in award list, no action is needed + if (this.isEmojiPresentForCurrentUser(name)) { + return this.note.awardEmoji.nodes; + } + + // else make a copy of unmutable list and return the list after adding the new emoji + const awardEmojiNodes = [...this.note.awardEmoji.nodes]; + awardEmojiNodes.push({ + name, + __typename: 'AwardEmoji', + user: { + id: convertToGraphQLId(TYPENAME_USER, this.currentUserId), + name: this.currentUserFullName, + __typename: 'UserCore', + }, + }); + + return awardEmojiNodes; + } + + // else just filter the emoji + return this.note.awardEmoji.nodes.filter( + (emoji) => + !(emoji.name === name && getIdFromGraphQLId(emoji.user.id) === this.currentUserId), + ); + }, + handleAwardEmoji(name) { + this.$apollo + .mutate({ + mutation: designNoteAwardEmojiToggleMutation, + variables: { + name, + awardableId: this.note.id, + }, + optimisticResponse: { + awardEmojiToggle: { + errors: [], + toggledOn: !this.isEmojiPresentForCurrentUser(name), + }, + }, + update: ( + cache, + { + data: { + awardEmojiToggle: { toggledOn }, + }, + }, + ) => { + const query = { + query: getDesignQuery, + variables: this.designVariables, + }; + + const sourceData = cache.readQuery(query); + + const newData = produce(sourceData, (draftState) => { + const { + awardEmoji, + } = draftState.project.issue.designCollection.designs.nodes[0].discussions.nodes + .find((d) => d.id === this.note.discussion.id) + .notes.nodes.find((n) => n.id === this.note.id); + + awardEmoji.nodes = this.getAwardEmojiNodes(name, toggledOn); + }); + + cache.writeQuery({ ...query, data: newData }); + }, + }) + .catch((error) => { + Sentry.captureException(error); + this.$emit('error', error); + }); + }, }, updateNoteMutation, }; @@ -131,7 +252,12 @@ export default { <template> <timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form"> - <gl-avatar-link :href="author.webUrl" class="gl-float-left gl-mr-3"> + <gl-avatar-link + :href="author.webUrl" + :data-user-id="authorId" + :data-username="author.username" + class="gl-float-left gl-mr-3 link-inherit-color js-user-link" + > <gl-avatar :size="32" :src="author.avatarUrl" :entity-name="author.username" /> </gl-avatar-link> @@ -140,7 +266,7 @@ export default { <gl-link v-once :href="author.webUrl" - class="js-user-link" + class="js-user-link link-inherit-color" data-testid="user-link" :data-user-id="authorId" :data-username="author.username" @@ -152,15 +278,23 @@ export default { <span class="note-headline-light note-headline-meta"> <span class="system-note-message"> <slot></slot> </span> <gl-link - class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm" + class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm link-inherit-color" :href="`#note_${noteAnchorId}`" > <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> </gl-link> </span> </div> - <div class="gl-display-flex gl-align-items-baseline gl-mt-n2 gl-mr-n2"> + <div class="gl-display-flex gl-align-items-flex-start gl-mt-n2 gl-mr-n2"> <slot name="resolve-discussion"></slot> + <emoji-picker + v-if="canAwardEmoji" + toggle-class="note-action-button note-emoji-button btn-icon btn-default-tertiary" + boundary="viewport" + :right="false" + data-testid="note-emoji-button" + @click="handleAwardEmoji" + /> <gl-button v-if="isEditingAndHasPermissions" v-gl-tooltip @@ -175,7 +309,6 @@ export default { <gl-disclosure-dropdown v-if="isEditingAndHasPermissions" v-gl-tooltip.hover - toggle-class="btn-sm" icon="ellipsis_v" category="tertiary" data-qa-selector="design_discussion_actions_ellipsis_dropdown" @@ -198,8 +331,14 @@ export default { ></div> <slot name="resolved-status"></slot> </template> + <design-note-awards-list + v-if="awards.length" + :awards="awards" + :can-award-emoji="note.userPermissions.awardEmoji" + @award="handleAwardEmoji" + /> <design-reply-form - v-else + v-if="isEditing" :markdown-preview-path="markdownPreviewPath" :design-note-mutation="$options.updateNoteMutation" :mutation-variables="mutationVariables" diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note_awards_list.vue b/app/assets/javascripts/design_management/components/design_notes/design_note_awards_list.vue new file mode 100644 index 00000000000..f5456f47410 --- /dev/null +++ b/app/assets/javascripts/design_management/components/design_notes/design_note_awards_list.vue @@ -0,0 +1,34 @@ +<script> +import AwardsList from '~/vue_shared/components/awards_list.vue'; + +export default { + components: { + AwardsList, + }, + props: { + awards: { + type: Array, + required: true, + }, + canAwardEmoji: { + type: Boolean, + required: true, + }, + }, + computed: { + currentUserId() { + return window.gon.current_user_id; + }, + }, +}; +</script> + +<template> + <awards-list + :awards="awards" + :can-award-emoji="canAwardEmoji" + :current-user-id="currentUserId" + class="gl-px-2 gl-mt-5" + @award="$emit('award', $event)" + /> +</template> 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 7474f8f3298..764c78ff581 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 @@ -233,7 +233,7 @@ export default { </template> </markdown-field> <slot name="resolve-checkbox"></slot> - <div class="note-form-actions gl-display-flex"> + <div class="note-form-actions gl-display-flex gl-mt-4!"> <gl-button ref="submitButton" :disabled="!hasValue" |