summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/design_management
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
commit8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch)
treea77e7fe7a93de11213032ed4ab1f33a3db51b738 /app/assets/javascripts/design_management
parent00b35af3db1abfe813a778f643dad221aad51fca (diff)
downloadgitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'app/assets/javascripts/design_management')
-rw-r--r--app/assets/javascripts/design_management/components/design_note_pin.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue158
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue40
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue3
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue70
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue42
-rw-r--r--app/assets/javascripts/design_management/components/design_presentation.vue10
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue178
-rw-r--r--app/assets/javascripts/design_management/components/upload/button.vue2
-rw-r--r--app/assets/javascripts/design_management/constants.js2
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql2
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql1
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/discussion_resolved_status.fragment.graphql9
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql17
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue114
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue2
-rw-r--r--app/assets/javascripts/design_management/router/index.js15
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js4
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js5
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');