diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 11:18:50 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-06-18 11:18:50 +0000 |
commit | 8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch) | |
tree | a77e7fe7a93de11213032ed4ab1f33a3db51b738 /app/assets/javascripts/design_management | |
parent | 00b35af3db1abfe813a778f643dad221aad51fca (diff) | |
download | gitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz |
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'app/assets/javascripts/design_management')
19 files changed, 552 insertions, 126 deletions
diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue index 50ea69d52ce..0811397fbad 100644 --- a/app/assets/javascripts/design_management/components/design_note_pin.vue +++ b/app/assets/javascripts/design_management/components/design_note_pin.vue @@ -13,7 +13,7 @@ export default { required: true, }, label: { - type: String, + type: Number, required: false, default: null, }, @@ -47,7 +47,7 @@ export default { 'btn-transparent comment-indicator': isNewNote, 'js-image-badge badge badge-pill': !isNewNote, }" - class="position-absolute" + class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center" type="button" @mousedown="$emit('mousedown', $event)" @mouseup="$emit('mouseup', $event)" 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> diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue index beb51647821..926e7c74802 100644 --- a/app/assets/javascripts/design_management/components/design_overlay.vue +++ b/app/assets/javascripts/design_management/components/design_overlay.vue @@ -33,6 +33,10 @@ export default { required: false, default: false, }, + resolvedDiscussionsExpanded: { + type: Boolean, + required: true, + }, }, apollo: { activeDiscussion: { @@ -140,7 +144,7 @@ export default { }, onExistingNoteMove(e) { const note = this.notes.find(({ id }) => id === this.movingNoteStartPosition.noteId); - if (!note) return; + if (!note || !this.canMoveNote(note)) return; const { position } = note; const { width, height } = position; @@ -186,8 +190,6 @@ export default { }); }, onNoteMousedown({ clientX, clientY }, note) { - if (note && !this.canMoveNote(note)) return; - this.movingNoteStartPosition = { noteId: note?.id, discussionId: note?.discussion.id, @@ -236,6 +238,9 @@ export default { isNoteInactive(note) { return this.activeDiscussion.id && this.activeDiscussion.id !== note.id; }, + designPinClass(note) { + return { inactive: this.isNoteInactive(note), resolved: note.resolved }; + }, }, }; </script> @@ -254,20 +259,23 @@ export default { data-qa-selector="design_image_button" @mouseup="onAddCommentMouseup" ></button> - <design-note-pin - v-for="(note, index) in notes" - :key="note.id" - :label="`${index + 1}`" - :repositioning="isMovingNote(note.id)" - :position=" - isMovingNote(note.id) && movingNoteNewPosition - ? getNotePositionStyle(movingNoteNewPosition) - : getNotePositionStyle(note.position) - " - :class="{ inactive: isNoteInactive(note) }" - @mousedown.stop="onNoteMousedown($event, note)" - @mouseup.stop="onNoteMouseup(note)" - /> + <template v-for="note in notes"> + <design-note-pin + v-if="resolvedDiscussionsExpanded || !note.resolved" + :key="note.id" + :label="note.index" + :repositioning="isMovingNote(note.id)" + :position=" + isMovingNote(note.id) && movingNoteNewPosition + ? getNotePositionStyle(movingNoteNewPosition) + : getNotePositionStyle(note.position) + " + :class="designPinClass(note)" + @mousedown.stop="onNoteMousedown($event, note)" + @mouseup.stop="onNoteMouseup(note)" + /> + </template> + <design-note-pin v-if="currentCommentForm" :position="currentCommentPositionStyle" diff --git a/app/assets/javascripts/design_management/components/design_presentation.vue b/app/assets/javascripts/design_management/components/design_presentation.vue index 5c113b3dbed..84dbb2809d9 100644 --- a/app/assets/javascripts/design_management/components/design_presentation.vue +++ b/app/assets/javascripts/design_management/components/design_presentation.vue @@ -35,6 +35,10 @@ export default { required: false, default: 1, }, + resolvedDiscussionsExpanded: { + type: Boolean, + required: true, + }, }, data() { return { @@ -54,7 +58,10 @@ export default { }, computed: { discussionStartingNotes() { - return this.discussions.map(discussion => discussion.notes[0]); + return this.discussions.map(discussion => ({ + ...discussion.notes[0], + index: discussion.index, + })); }, currentCommentForm() { return (this.isAnnotating && this.currentAnnotationPosition) || null; @@ -305,6 +312,7 @@ export default { :notes="discussionStartingNotes" :current-comment-form="currentCommentForm" :disable-commenting="isDraggingDesign" + :resolved-discussions-expanded="resolvedDiscussionsExpanded" @openCommentForm="openCommentForm" @closeCommentForm="closeCommentForm" @moveNote="moveNote" diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue new file mode 100644 index 00000000000..333ad2557e8 --- /dev/null +++ b/app/assets/javascripts/design_management/components/design_sidebar.vue @@ -0,0 +1,178 @@ +<script> +import { s__ } from '~/locale'; +import Cookies from 'js-cookie'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui'; +import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql'; +import { extractDiscussions, extractParticipants } from '../utils/design_management_utils'; +import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants'; +import DesignDiscussion from './design_notes/design_discussion.vue'; +import Participants from '~/sidebar/components/participants/participants.vue'; + +export default { + components: { + DesignDiscussion, + Participants, + GlCollapse, + GlButton, + GlPopover, + }, + props: { + design: { + type: Object, + required: true, + }, + resolvedDiscussionsExpanded: { + type: Boolean, + required: true, + }, + markdownPreviewPath: { + type: String, + required: true, + }, + }, + data() { + return { + isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)), + discussionWithOpenForm: '', + }; + }, + computed: { + discussions() { + return extractDiscussions(this.design.discussions); + }, + issue() { + return { + ...this.design.issue, + webPath: this.design.issue.webPath.substr(1), + }; + }, + discussionParticipants() { + return extractParticipants(this.issue.participants); + }, + resolvedDiscussions() { + return this.discussions.filter(discussion => discussion.resolved); + }, + unresolvedDiscussions() { + return this.discussions.filter(discussion => !discussion.resolved); + }, + resolvedCommentsToggleIcon() { + return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right'; + }, + }, + methods: { + handleSidebarClick() { + this.isResolvedCommentsPopoverHidden = true; + Cookies.set(this.$options.cookieKey, 'true', { expires: 365 * 10 }); + this.updateActiveDiscussion(); + }, + updateActiveDiscussion(id) { + this.$apollo.mutate({ + mutation: updateActiveDiscussionMutation, + variables: { + id, + source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion, + }, + }); + }, + closeCommentForm() { + this.comment = ''; + this.$emit('closeCommentForm'); + }, + updateDiscussionWithOpenForm(id) { + this.discussionWithOpenForm = id; + }, + }, + resolveCommentsToggleText: s__('DesignManagement|Resolved Comments'), + cookieKey: 'hide_design_resolved_comments_popover', +}; +</script> + +<template> + <div class="image-notes" @click="handleSidebarClick"> + <h2 class="gl-font-weight-bold gl-mt-0"> + {{ issue.title }} + </h2> + <a + class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block" + :href="issue.webUrl" + >{{ issue.webPath }}</a + > + <participants + :participants="discussionParticipants" + :show-participant-label="false" + class="gl-mb-4" + /> + <h2 + v-if="unresolvedDiscussions.length === 0" + class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4" + data-testid="new-discussion-disclaimer" + > + {{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }} + </h2> + <design-discussion + v-for="discussion in unresolvedDiscussions" + :key="discussion.id" + :discussion="discussion" + :design-id="$route.params.id" + :noteable-id="design.id" + :markdown-preview-path="markdownPreviewPath" + :resolved-discussions-expanded="resolvedDiscussionsExpanded" + :discussion-with-open-form="discussionWithOpenForm" + data-testid="unresolved-discussion" + @createNoteError="$emit('onDesignDiscussionError', $event)" + @updateNoteError="$emit('updateNoteError', $event)" + @resolveDiscussionError="$emit('resolveDiscussionError', $event)" + @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" + @openForm="updateDiscussionWithOpenForm" + /> + <template v-if="resolvedDiscussions.length > 0"> + <gl-button + id="resolved-comments" + data-testid="resolved-comments" + :icon="resolvedCommentsToggleIcon" + variant="link" + class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4" + @click="$emit('toggleResolvedComments')" + >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }}) + </gl-button> + <gl-popover + v-if="!isResolvedCommentsPopoverHidden" + :show="!isResolvedCommentsPopoverHidden" + target="resolved-comments" + container="popovercontainer" + placement="top" + :title="s__('DesignManagement|Resolved Comments')" + > + <p> + {{ + s__( + 'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below', + ) + }} + </p> + <a href="#" rel="noopener noreferrer" target="_blank">{{ + s__('DesignManagement|Learn more about resolving comments') + }}</a> + </gl-popover> + <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3"> + <design-discussion + v-for="discussion in resolvedDiscussions" + :key="discussion.id" + :discussion="discussion" + :design-id="$route.params.id" + :noteable-id="design.id" + :markdown-preview-path="markdownPreviewPath" + :resolved-discussions-expanded="resolvedDiscussionsExpanded" + :discussion-with-open-form="discussionWithOpenForm" + data-testid="resolved-discussion" + @error="$emit('onDesignDiscussionError', $event)" + @updateNoteError="$emit('updateNoteError', $event)" + @openForm="updateDiscussionWithOpenForm" + @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" + /> + </gl-collapse> + </template> + <slot name="replyForm"></slot> + </div> +</template> diff --git a/app/assets/javascripts/design_management/components/upload/button.vue b/app/assets/javascripts/design_management/components/upload/button.vue index e3c5e369170..68555104a3c 100644 --- a/app/assets/javascripts/design_management/components/upload/button.vue +++ b/app/assets/javascripts/design_management/components/upload/button.vue @@ -41,7 +41,7 @@ export default { variant="success" @click="openFileUpload" > - {{ s__('DesignManagement|Add designs') }} + {{ s__('DesignManagement|Upload designs') }} <gl-loading-icon v-if="isSaving" inline class="ml-1" /> </gl-deprecated-button> diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js index 59d34669ad7..21ff361a277 100644 --- a/app/assets/javascripts/design_management/constants.js +++ b/app/assets/javascripts/design_management/constants.js @@ -12,3 +12,5 @@ export const ACTIVE_DISCUSSION_SOURCE_TYPES = { pin: 'pin', discussion: 'discussion', }; + +export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0']; diff --git a/app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql index ca5b5a52c71..c1439c56ff5 100644 --- a/app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql +++ b/app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql @@ -1,6 +1,7 @@ #import "./designNote.fragment.graphql" #import "./designList.fragment.graphql" #import "./diffRefs.fragment.graphql" +#import "./discussion_resolved_status.fragment.graphql" fragment DesignItem on Design { ...DesignListItem @@ -12,6 +13,7 @@ fragment DesignItem on Design { nodes { id replyId + ...ResolvedStatus notes { nodes { ...DesignNote diff --git a/app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql index 2ad84f9cb17..cb7cfd89abf 100644 --- a/app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql +++ b/app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql @@ -10,6 +10,7 @@ fragment DesignNote on Note { body bodyHtml createdAt + resolved position { diffRefs { ...DesignDiffRefs diff --git a/app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql new file mode 100644 index 00000000000..7483b508721 --- /dev/null +++ b/app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql @@ -0,0 +1,9 @@ +fragment ResolvedStatus on Discussion { + resolvable + resolved + resolvedAt + resolvedBy { + name + webUrl + } +} diff --git a/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql new file mode 100644 index 00000000000..d5f54ec9b58 --- /dev/null +++ b/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql @@ -0,0 +1,17 @@ +#import "../fragments/designNote.fragment.graphql" +#import "../fragments/discussion_resolved_status.fragment.graphql" + +mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) { + discussionToggleResolve(input: { id: $id, resolve: $resolve }) { + discussion { + id + ...ResolvedStatus + notes { + nodes { + ...DesignNote + } + } + } + errors + } +} diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 7ff3271394d..fe121b6530a 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -1,17 +1,16 @@ <script> -import { ApolloMutation } from 'vue-apollo'; import Mousetrap from 'mousetrap'; import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { ApolloMutation } from 'vue-apollo'; import createFlash from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; import allVersionsMixin from '../../mixins/all_versions'; import Toolbar from '../../components/toolbar/index.vue'; -import DesignDiscussion from '../../components/design_notes/design_discussion.vue'; -import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; import DesignDestroyer from '../../components/design_destroyer.vue'; import DesignScaler from '../../components/design_scaler.vue'; -import Participants from '~/sidebar/components/participants/participants.vue'; import DesignPresentation from '../../components/design_presentation.vue'; +import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; +import DesignSidebar from '../../components/design_sidebar.vue'; import getDesignQuery from '../../graphql/queries/getDesign.query.graphql'; import appDataQuery from '../../graphql/queries/appData.query.graphql'; import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql'; @@ -20,7 +19,6 @@ import updateActiveDiscussionMutation from '../../graphql/mutations/update_activ import { extractDiscussions, extractDesign, - extractParticipants, updateImageDiffNoteOptimisticResponse, } from '../../utils/design_management_utils'; import { @@ -43,15 +41,14 @@ import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; export default { components: { ApolloMutation, + DesignReplyForm, DesignPresentation, - DesignDiscussion, DesignScaler, DesignDestroyer, Toolbar, - DesignReplyForm, GlLoadingIcon, GlAlert, - Participants, + DesignSidebar, }, mixins: [allVersionsMixin], props: { @@ -69,6 +66,7 @@ export default { errorMessage: '', issueIid: '', scale: 1, + resolvedDiscussionsExpanded: false, }; }, apollo: { @@ -103,20 +101,17 @@ export default { return this.$apollo.queries.design.loading && !this.design.filename; }, discussions() { + if (!this.design.discussions) { + return []; + } return extractDiscussions(this.design.discussions); }, - discussionParticipants() { - return extractParticipants(this.design.issue.participants); - }, markdownPreviewPath() { return `/${this.projectPath}/preview_markdown?target_type=Issue`; }, isSubmitButtonDisabled() { return this.comment.trim().length === 0; }, - renderDiscussions() { - return this.discussions.length || this.annotationCoordinates; - }, designVariables() { return { fullPath: this.projectPath, @@ -144,18 +139,25 @@ export default { }, }; }, - issue() { - return { - ...this.design.issue, - webPath: this.design.issue.webPath.substr(1), - }; - }, isAnnotating() { return Boolean(this.annotationCoordinates); }, + resolvedDiscussions() { + return this.discussions.filter(discussion => discussion.resolved); + }, + }, + watch: { + resolvedDiscussions(val) { + if (!val.length) { + this.resolvedDiscussionsExpanded = false; + } + }, }, mounted() { Mousetrap.bind('esc', this.closeDesign); + this.trackEvent(); + // We need to reset the active discussion when opening a new design + this.updateActiveDiscussion(); }, beforeDestroy() { Mousetrap.unbind('esc', this.closeDesign); @@ -247,6 +249,9 @@ export default { onDesignDeleteError(e) { this.onError(designDeletionError({ singular: true }), e); }, + onResolveDiscussionError(e) { + this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); + }, openCommentForm(annotationCoordinates) { this.annotationCoordinates = annotationCoordinates; }, @@ -278,23 +283,9 @@ export default { }, }); }, - }, - beforeRouteEnter(to, from, next) { - next(vm => { - vm.trackEvent(); - }); - }, - beforeRouteUpdate(to, from, next) { - this.trackEvent(); - this.closeCommentForm(); - // We need to reset the active discussion when opening a new design - this.updateActiveDiscussion(); - next(); - }, - beforeRouteLeave(to, from, next) { - // We need to reset the active discussion when moving to design list view - this.updateActiveDiscussion(); - next(); + toggleResolvedComments() { + this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded; + }, }, createImageDiffNoteMutation, DESIGNS_ROUTE_NAME, @@ -337,6 +328,7 @@ export default { :discussions="discussions" :is-annotating="isAnnotating" :scale="scale" + :resolved-discussions-expanded="resolvedDiscussionsExpanded" @openCommentForm="openCommentForm" @closeCommentForm="closeCommentForm" @moveNote="onMoveNote" @@ -346,33 +338,19 @@ export default { <design-scaler @scale="scale = $event" /> </div> </div> - <div class="image-notes" @click="updateActiveDiscussion()"> - <h2 class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0"> - {{ issue.title }} - </h2> - <a class="text-tertiary text-decoration-none mb-3 d-block" :href="issue.webUrl">{{ - issue.webPath - }}</a> - <participants - :participants="discussionParticipants" - :show-participant-label="false" - class="mb-4" - /> - <template v-if="renderDiscussions"> - <design-discussion - v-for="(discussion, index) in discussions" - :key="discussion.id" - :discussion="discussion" - :design-id="id" - :noteable-id="design.id" - :discussion-index="index + 1" - :markdown-preview-path="markdownPreviewPath" - @error="onDesignDiscussionError" - @updateNoteError="onUpdateNoteError" - @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" - /> + <design-sidebar + :design="design" + :resolved-discussions-expanded="resolvedDiscussionsExpanded" + :markdown-preview-path="markdownPreviewPath" + @onDesignDiscussionError="onDesignDiscussionError" + @onCreateImageDiffNoteError="onCreateImageDiffNoteError" + @updateNoteError="onUpdateNoteError" + @resolveDiscussionError="onResolveDiscussionError" + @toggleResolvedComments="toggleResolvedComments" + > + <template #replyForm> <apollo-mutation - v-if="annotationCoordinates" + v-if="isAnnotating" #default="{ mutate, loading }" :mutation="$options.createImageDiffNoteMutation" :variables="{ @@ -388,13 +366,9 @@ export default { :markdown-preview-path="markdownPreviewPath" @submitForm="mutate" @cancelForm="closeCommentForm" - /> - </apollo-mutation> - </template> - <h2 v-else class="new-discussion-disclaimer gl-font-base m-0"> - {{ __("Click the image where you'd like to start a new discussion") }} - </h2> - </div> + /> </apollo-mutation + ></template> + </design-sidebar> </template> </div> </template> diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index 7d419bc3ded..922c800009f 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -318,6 +318,6 @@ export default { </li> </ol> </div> - <router-view /> + <router-view :key="$route.fullPath" /> </div> </template> diff --git a/app/assets/javascripts/design_management/router/index.js b/app/assets/javascripts/design_management/router/index.js index 7dc92f55d47..7494da002c8 100644 --- a/app/assets/javascripts/design_management/router/index.js +++ b/app/assets/javascripts/design_management/router/index.js @@ -2,6 +2,9 @@ import $ from 'jquery'; import Vue from 'vue'; import VueRouter from 'vue-router'; import routes from './routes'; +import { DESIGN_ROUTE_NAME } from './constants'; +import { getPageLayoutElement } from '~/design_management/utils/design_management_utils'; +import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants'; Vue.use(VueRouter); @@ -11,10 +14,20 @@ export default function createRouter(base) { mode: 'history', routes, }); + const pageEl = getPageLayoutElement(); - router.beforeEach(({ meta: { el } }, from, next) => { + router.beforeEach(({ meta: { el }, name }, _, next) => { $(`#${el}`).tab('show'); + // apply a fullscreen layout style in Design View (a.k.a design detail) + if (pageEl) { + if (name === DESIGN_ROUTE_NAME) { + pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST); + } else { + pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST); + } + } + next(); }); diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js index 01c073bddc2..24b374b79fd 100644 --- a/app/assets/javascripts/design_management/utils/cache_update.js +++ b/app/assets/javascripts/design_management/utils/cache_update.js @@ -95,6 +95,10 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) = __typename: 'Discussion', id: createImageDiffNote.note.discussion.id, replyId: createImageDiffNote.note.discussion.replyId, + resolvable: true, + resolved: false, + resolvedAt: null, + resolvedBy: null, notes: { __typename: 'NoteConnection', nodes: [createImageDiffNote.note], diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js index e6d8796ffa4..22705cf67a1 100644 --- a/app/assets/javascripts/design_management/utils/design_management_utils.js +++ b/app/assets/javascripts/design_management/utils/design_management_utils.js @@ -21,8 +21,9 @@ export const extractNodes = elements => elements.edges.map(({ node }) => node); */ export const extractDiscussions = discussions => - discussions.nodes.map(discussion => ({ + discussions.nodes.map((discussion, index) => ({ ...discussion, + index: index + 1, notes: discussion.notes.nodes, })); @@ -123,3 +124,5 @@ const normalizeAuthor = author => ({ }); export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node)); + +export const getPageLayoutElement = () => document.querySelector('.layout-page'); |