Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /app/assets/javascripts/design_management
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'app/assets/javascripts/design_management')
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue3
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue92
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue45
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue21
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue49
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue59
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue168
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue29
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue14
-rw-r--r--app/assets/javascripts/design_management/constants.js1
-rw-r--r--app/assets/javascripts/design_management/graphql.js59
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql17
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql1
-rw-r--r--app/assets/javascripts/design_management/index.js2
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue29
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue11
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js283
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js34
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/design_management/utils/tracking.js1
22 files changed, 648 insertions, 288 deletions
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index 37686dd5a46..970197ef41b 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -98,6 +98,7 @@ export default {
:loading="loading"
:icon="buttonIcon"
:disabled="isDeleting || !hasSelectedDesigns"
- />
+ ><slot></slot
+ ></gl-button>
</div>
</template>
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 6a20517eed7..845f1aec8cf 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,19 +1,20 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
+import createFlash from '~/flash';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import allVersionsMixin from '../../mixins/all_versions';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
-import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue';
-import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
+import { hasErrors } from '../../utils/cache_update';
+import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
export default {
components: {
@@ -26,6 +27,7 @@ export default {
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -62,22 +64,20 @@ export default {
activeDiscussion: {
query: activeDiscussionQuery,
result({ data }) {
- const discussionId = data.activeDiscussion.id;
if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) {
return;
}
- // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
- // We don't want scrollIntoView to be triggered from the discussion click itself
- if (
- discussionId &&
- data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
- discussionId === this.discussion.notes[0].id
- ) {
- this.$el.scrollIntoView({
- behavior: 'smooth',
- inline: 'start',
- });
- }
+
+ this.$nextTick(() => {
+ // 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 (this.$el && this.shouldScrollToDiscussion(data.activeDiscussion)) {
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ inline: 'start',
+ });
+ }
+ });
},
},
},
@@ -107,8 +107,8 @@ export default {
atVersion: this.designsVersion,
};
},
- isDiscussionHighlighted() {
- return this.discussion.notes[0].id === this.activeDiscussion.id;
+ isDiscussionActive() {
+ return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
},
resolveCheckboxText() {
return this.discussion.resolved
@@ -138,21 +138,10 @@ export default {
},
},
methods: {
- addDiscussionComment(
- store,
- {
- data: { createNote },
- },
- ) {
- updateStoreAfterAddDiscussionComment(
- store,
- createNote,
- getDesignQuery,
- this.designVariables,
- this.discussion.id,
- );
- },
- onDone() {
+ onDone({ data: { createNote } }) {
+ if (hasErrors(createNote)) {
+ createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR });
+ }
this.discussionComment = '';
this.hideForm();
if (this.shouldChangeResolvedStatus) {
@@ -160,14 +149,14 @@ export default {
}
},
onCreateNoteError(err) {
- this.$emit('createNoteError', err);
+ this.$emit('create-note-error', err);
},
hideForm() {
this.isFormRendered = false;
this.discussionComment = '';
},
showForm() {
- this.$emit('openForm', this.discussion.id);
+ this.$emit('open-form', this.discussion.id);
this.isFormRendered = true;
},
toggleResolvedStatus() {
@@ -179,16 +168,24 @@ export default {
})
.then(({ data }) => {
if (data.errors?.length > 0) {
- this.$emit('resolveDiscussionError', data.errors[0]);
+ this.$emit('resolve-discussion-error', data.errors[0]);
}
})
.catch(err => {
- this.$emit('resolveDiscussionError', err);
+ this.$emit('resolve-discussion-error', err);
})
.finally(() => {
this.isResolving = false;
});
},
+ shouldScrollToDiscussion(activeDiscussion) {
+ const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
+ ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
+ ACTIVE_DISCUSSION_SOURCE_TYPES.url,
+ ];
+ const { source } = activeDiscussion;
+ return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
+ },
},
createNoteMutation,
};
@@ -196,13 +193,12 @@ export default {
<template>
<div class="design-discussion-wrapper">
- <div
- class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
+ <gl-badge
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer"
:class="{ resolved: discussion.resolved }"
- type="button"
>
{{ discussion.index }}
- </div>
+ </gl-badge>
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content"
@@ -211,8 +207,8 @@ export default {
:note="firstNote"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @error="$emit('update-note-error', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
<button
@@ -220,7 +216,6 @@ export default {
: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"
@@ -255,8 +250,8 @@ export default {
:note="note"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
- @error="$emit('updateNoteError', $event)"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
+ @error="$emit('update-note-error', $event)"
/>
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
<reply-placeholder
@@ -272,7 +267,6 @@ export default {
:variables="{
input: mutationPayload,
}"
- :update="addDiscussionComment"
@done="onDone"
@error="onCreateNoteError"
>
@@ -280,8 +274,8 @@ export default {
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
>
<template v-if="discussion.resolvable" #resolveCheckbox>
<label data-testid="resolve-checkbox">
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 172e61920ef..7f4b3b31024 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
@@ -1,12 +1,12 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignReplyForm from './design_reply_form.vue';
-import { findNoteId } from '../../utils/design_management_utils';
+import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
import { hasErrors } from '../../utils/cache_update';
export default {
@@ -17,9 +17,11 @@ export default {
DesignReplyForm,
ApolloMutation,
GlIcon,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
props: {
note: {
@@ -46,7 +48,7 @@ export default {
return findNoteId(this.note.id);
},
isNoteLinked() {
- return this.$route.hash === `#note_${this.noteAnchorId}`;
+ return extractDesignNoteId(this.$route.hash) === this.noteAnchorId;
},
mutationPayload() {
return {
@@ -58,11 +60,6 @@ export default {
return !this.isEditing && this.note.userPermissions.adminNote;
},
},
- mounted() {
- if (this.isNoteLinked) {
- this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
- }
- },
methods: {
hideForm() {
this.isEditing = false;
@@ -87,30 +84,30 @@ export default {
:img-alt="author.username"
:img-size="40"
/>
- <div class="d-flex justify-content-between">
+ <div class="gl-display-flex gl-justify-content-space-between">
<div>
- <a
+ <gl-link
v-once
:href="author.webUrl"
class="js-user-link"
:data-user-id="author.id"
:data-username="author.username"
>
- <span class="note-header-author-name bold">{{ author.name }}</span>
- <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
+ <span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>
+ <span v-if="author.status_tooltip_html" v-safe-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
- </a>
+ </gl-link>
<span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>
- <template v-if="note.createdAt">
- <span class="system-note-separator"></span>
- <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`">
- <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
- </a>
- </template>
+ <gl-link
+ class="note-timestamp system-note-separator gl-display-block gl-mb-2"
+ :href="`#note_${noteAnchorId}`"
+ >
+ <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
+ </gl-link>
</span>
</div>
- <div class="gl-display-flex">
+ <div class="gl-display-flex gl-align-items-baseline">
<slot name="resolveDiscussion"></slot>
<button
v-if="isEditButtonVisible"
@@ -126,9 +123,9 @@ export default {
</div>
<template v-if="!isEditing">
<div
+ v-safe-html="note.bodyHtml"
class="note-text js-note-text md"
data-qa-selector="note_content"
- v-html="note.bodyHtml"
></div>
<slot name="resolvedStatus"></slot>
</template>
@@ -147,9 +144,9 @@ export default {
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:is-new-comment="false"
- class="mt-5"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ class="gl-mt-5"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
/>
</apollo-mutation>
</timeline-entry-item>
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 969034909f2..3754e1dbbc1 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
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { s__ } from '~/locale';
@@ -7,7 +7,7 @@ export default {
name: 'DesignReplyForm',
components: {
MarkdownField,
- GlDeprecatedButton,
+ GlButton,
GlModal,
},
props: {
@@ -66,13 +66,13 @@ export default {
},
methods: {
submitForm() {
- if (this.hasValue) this.$emit('submitForm');
+ if (this.hasValue) this.$emit('submit-form');
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
this.$refs.cancelCommentModal.show();
} else {
- this.$emit('cancelForm');
+ this.$emit('cancel-form');
}
},
focusInput() {
@@ -112,20 +112,21 @@ export default {
</markdown-field>
<slot name="resolveCheckbox"></slot>
<div class="note-form-actions gl-display-flex gl-justify-content-space-between">
- <gl-deprecated-button
+ <gl-button
ref="submitButton"
:disabled="!hasValue || isSaving"
+ category="primary"
variant="success"
type="submit"
data-track-event="click_button"
data-qa-selector="save_comment_button"
- @click="$emit('submitForm')"
+ @click="$emit('submit-form')"
>
{{ buttonText }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
+ </gl-button>
+ <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{
__('Cancel')
- }}</gl-deprecated-button>
+ }}</gl-button>
</div>
<gl-modal
ref="cancelCommentModal"
@@ -134,7 +135,7 @@ export default {
:ok-title="modalSettings.okTitle"
:cancel-title="modalSettings.cancelTitle"
modal-id="cancel-comment-modal"
- @ok="$emit('cancelForm')"
+ @ok="$emit('cancel-form')"
>{{ modalSettings.content }}
</gl-modal>
</form>
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 926e7c74802..5c4a3ab5f94 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import DesignNotePin from './design_note_pin.vue';
@@ -236,18 +237,26 @@ export default {
});
},
isNoteInactive(note) {
- return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
+ const discussionNotes = note.discussion.notes.nodes || [];
+
+ return (
+ this.activeDiscussion.id &&
+ !discussionNotes.some(({ id }) => id === this.activeDiscussion.id)
+ );
},
designPinClass(note) {
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
},
},
+ i18n: {
+ newCommentButtonLabel: __('Add comment to design'),
+ },
};
</script>
<template>
<div
- class="position-absolute image-diff-overlay frame"
+ class="gl-absolute gl-top-0 gl-left-0 frame"
:style="overlayStyle"
@mousemove="onOverlayMousemove"
@mouseleave="onNoteMouseup"
@@ -255,26 +264,28 @@ export default {
<button
v-show="!disableCommenting"
type="button"
- class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
+ role="button"
+ :aria-label="$options.i18n.newCommentButtonLabel"
+ class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent design-detail-overlay-add-comment"
data-qa-selector="design_image_button"
@mouseup="onAddCommentMouseup"
></button>
- <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-for="note in notes"
+ 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)"
+ />
<design-note-pin
v-if="currentCommentForm"
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index e5a3590877e..df425e3b96d 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -8,6 +8,8 @@ import { extractDiscussions, extractParticipants } from '../utils/design_managem
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import DesignDiscussion from './design_notes/design_discussion.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
+import DesignTodoButton from './design_todo_button.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -16,7 +18,9 @@ export default {
GlCollapse,
GlButton,
GlPopover,
+ DesignTodoButton,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
design: {
type: Object,
@@ -37,6 +41,14 @@ export default {
discussionWithOpenForm: '',
};
},
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
+ },
+ },
computed: {
discussions() {
return extractDiscussions(this.design.discussions);
@@ -59,6 +71,26 @@ export default {
resolvedCommentsToggleIcon() {
return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right';
},
+ showTodoButton() {
+ return this.glFeatures.designManagementTodoButton;
+ },
+ sidebarWrapperClass() {
+ return {
+ 'gl-pt-0': this.showTodoButton,
+ };
+ },
+ },
+ watch: {
+ isResolvedCommentsPopoverHidden(newVal) {
+ if (!newVal) {
+ this.$refs.resolvedComments.scrollIntoView();
+ }
+ },
+ },
+ mounted() {
+ if (!this.isResolvedCommentsPopoverHidden && this.$refs.resolvedComments) {
+ this.$refs.resolvedComments.$el.scrollIntoView();
+ }
},
methods: {
handleSidebarClick() {
@@ -89,7 +121,14 @@ export default {
</script>
<template>
- <div class="image-notes" @click="handleSidebarClick">
+ <div class="image-notes" :class="sidebarWrapperClass" @click="handleSidebarClick">
+ <div
+ v-if="showTodoButton"
+ class="gl-py-4 gl-mb-4 gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ >
+ <span>{{ __('To-Do') }}</span>
+ <design-todo-button :design="design" @error="$emit('todoError', $event)" />
+ </div>
<h2 class="gl-font-weight-bold gl-mt-0">
{{ issue.title }}
</h2>
@@ -120,15 +159,16 @@ export default {
: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)"
+ @create-note-error="$emit('onDesignDiscussionError', $event)"
+ @update-note-error="$emit('updateNoteError', $event)"
+ @resolve-discussion-error="$emit('resolveDiscussionError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @openForm="updateDiscussionWithOpenForm"
+ @open-form="updateDiscussionWithOpenForm"
/>
<template v-if="resolvedDiscussions.length > 0">
<gl-button
id="resolved-comments"
+ ref="resolvedComments"
data-testid="resolved-comments"
:icon="resolvedCommentsToggleIcon"
variant="link"
@@ -151,9 +191,12 @@ export default {
)
}}
</p>
- <a href="#" rel="noopener noreferrer" target="_blank">{{
- s__('DesignManagement|Learn more about resolving comments')
- }}</a>
+ <a
+ href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads"
+ 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
diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue
new file mode 100644
index 00000000000..aff4f348d15
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_todo_button.vue
@@ -0,0 +1,168 @@
+<script>
+import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
+import getDesignQuery from '../graphql/queries/get_design.query.graphql';
+import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
+import TodoButton from '~/vue_shared/components/todo_button.vue';
+import allVersionsMixin from '../mixins/all_versions';
+import { updateStoreAfterDeleteDesignTodo } from '../utils/cache_update';
+import { findIssueId, findDesignId } from '../utils/design_management_utils';
+import { CREATE_DESIGN_TODO_ERROR, DELETE_DESIGN_TODO_ERROR } from '../utils/error_messages';
+
+export default {
+ components: {
+ TodoButton,
+ },
+ mixins: [allVersionsMixin],
+ props: {
+ design: {
+ type: Object,
+ required: true,
+ },
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ todoLoading: false,
+ };
+ },
+ computed: {
+ designVariables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ designTodoVariables() {
+ return {
+ projectPath: this.projectPath,
+ issueId: findIssueId(this.design.issue.id),
+ designId: findDesignId(this.design.id),
+ issueIid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ pendingTodo() {
+ // TODO data structure pending BE MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40555#note_405732940
+ return this.design.currentUserTodos?.nodes[0];
+ },
+ hasPendingTodo() {
+ return Boolean(this.pendingTodo);
+ },
+ },
+ methods: {
+ updateGlobalTodoCount(additionalTodoCount) {
+ const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
+ const todoToggleEvent = new CustomEvent('todo:toggle', {
+ detail: {
+ count: Math.max(currentCount + additionalTodoCount, 0),
+ },
+ });
+
+ document.dispatchEvent(todoToggleEvent);
+ },
+ incrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(1);
+ },
+ decrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(-1);
+ },
+ createTodo() {
+ this.todoLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: createDesignTodoMutation,
+ variables: this.designTodoVariables,
+ update: (store, { data: { createDesignTodo } }) => {
+ // because this is a @client mutation,
+ // we control what is in errors, and therefore
+ // we are certain that there is at most 1 item in the array
+ const createDesignTodoError = (createDesignTodo.errors || [])[0];
+ if (createDesignTodoError) {
+ this.$emit('error', Error(createDesignTodoError.message));
+ }
+ },
+ })
+ .then(() => {
+ this.incrementGlobalTodoCount();
+ })
+ .catch(err => {
+ this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR));
+ throw err;
+ })
+ .finally(() => {
+ this.todoLoading = false;
+ });
+ },
+ deleteTodo() {
+ if (!this.hasPendingTodo) return Promise.reject();
+
+ const { id } = this.pendingTodo;
+ const { designVariables } = this;
+
+ this.todoLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: todoMarkDoneMutation,
+ variables: {
+ id,
+ },
+ update(
+ store,
+ {
+ data: { todoMarkDone },
+ },
+ ) {
+ const todoMarkDoneFirstError = (todoMarkDone.errors || [])[0];
+ if (todoMarkDoneFirstError) {
+ this.$emit('error', Error(todoMarkDoneFirstError));
+ } else {
+ updateStoreAfterDeleteDesignTodo(
+ store,
+ todoMarkDone,
+ getDesignQuery,
+ designVariables,
+ );
+ }
+ },
+ })
+ .then(() => {
+ this.decrementGlobalTodoCount();
+ })
+ .catch(err => {
+ this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR));
+ throw err;
+ })
+ .finally(() => {
+ this.todoLoading = false;
+ });
+ },
+ toggleTodo() {
+ if (this.hasPendingTodo) {
+ return this.deleteTodo();
+ }
+
+ return this.createTodo();
+ },
+ },
+};
+</script>
+
+<template>
+ <todo-button
+ issuable-type="design"
+ :issuable-id="design.iid"
+ :is-todo="hasPendingTodo"
+ :loading="todoLoading"
+ @click.stop.prevent="toggleTodo"
+ />
+</template>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index 292b6e09055..36ea812d92e 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -1,6 +1,5 @@
<script>
import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { n__, __ } from '~/locale';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
@@ -10,7 +9,6 @@ export default {
GlLoadingIcon,
GlIntersectionObserver,
GlIcon,
- Icon,
Timeago,
},
props: {
@@ -127,12 +125,14 @@ export default {
params: { id: filename },
query: $route.query,
}"
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
>
- <div class="card-body p-0 d-flex-center overflow-hidden position-relative">
- <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
+ <div
+ class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative"
+ >
+ <div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
- <icon :name="icon.name" :size="18" :class="icon.classes" />
+ <gl-icon :name="icon.name" :size="18" :class="icon.classes" />
</span>
</div>
<gl-intersection-observer @appear="onAppear">
@@ -147,25 +147,28 @@ export default {
v-show="showImage"
:src="imageLink"
:alt="filename"
- class="block mx-auto mw-100 mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
data-qa-selector="design_image"
@load="onImageLoad"
@error="onImageError"
/>
</gl-intersection-observer>
</div>
- <div class="card-footer d-flex w-100">
- <div class="d-flex flex-column str-truncated-100">
- <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
+ <div class="card-footer gl-display-flex gl-w-full">
+ <div class="gl-display-flex gl-flex-direction-column str-truncated-100">
+ <span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{
filename
}}</span>
<span v-if="updatedAt" class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
</div>
- <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
- <icon name="comments" class="ml-1" />
- <span :aria-label="notesLabel" class="ml-1">
+ <div
+ v-if="notesCount"
+ class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
+ >
+ <gl-icon name="comments" class="gl-ml-2" />
+ <span :aria-label="notesLabel" class="gl-ml-2">
{{ notesCount }}
</span>
</div>
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
index a03982cb91b..4a1be7b720a 100644
--- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -1,13 +1,13 @@
<script>
-import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import allVersionsMixin from '../../mixins/all_versions';
import { findVersionId } from '../../utils/design_management_utils';
export default {
components: {
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
GlSprintf,
},
mixins: [allVersionsMixin],
@@ -63,8 +63,8 @@ export default {
</script>
<template>
- <gl-new-dropdown :text="dropdownText" size="small">
- <gl-new-dropdown-item
+ <gl-dropdown :text="dropdownText" size="small">
+ <gl-dropdown-item
v-for="(version, index) in allVersions"
:key="version.id"
:is-check-item="true"
@@ -76,6 +76,6 @@ export default {
{{ allVersions.length - index }}
</template>
</gl-sprintf>
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js
index 21ff361a277..63a92ef5ec0 100644
--- a/app/assets/javascripts/design_management/constants.js
+++ b/app/assets/javascripts/design_management/constants.js
@@ -11,6 +11,7 @@ export const VALID_DATA_TRANSFER_TYPE = 'Files';
export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
pin: 'pin',
discussion: 'discussion',
+ url: 'url',
};
export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0'];
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
index fae337aa75b..d1fe977b969 100644
--- a/app/assets/javascripts/design_management/graphql.js
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -1,24 +1,70 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { uniqueId } from 'lodash';
+import produce from 'immer';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
+import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql';
+import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
+import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
+import { addPendingTodoToStore } from './utils/cache_update';
Vue.use(VueApollo);
const resolvers = {
Mutation: {
updateActiveDiscussion: (_, { id = null, source }, { cache }) => {
- const data = cache.readQuery({ query: activeDiscussionQuery });
- data.activeDiscussion = {
- __typename: 'ActiveDiscussion',
- id,
- source,
- };
+ const sourceData = cache.readQuery({ query: activeDiscussionQuery });
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.activeDiscussion = {
+ __typename: 'ActiveDiscussion',
+ id,
+ source,
+ };
+ });
+
cache.writeQuery({ query: activeDiscussionQuery, data });
},
+ createDesignTodo: (
+ _,
+ { projectPath, issueId, designId, issueIid, filenames, atVersion },
+ { cache },
+ ) => {
+ return axios
+ .post(`/${projectPath}/todos`, {
+ issue_id: issueId,
+ issuable_id: designId,
+ issuable_type: 'design',
+ })
+ .then(({ data }) => {
+ const { delete_path } = data;
+ const todoId = extractTodoIdFromDeletePath(delete_path);
+ if (!todoId) {
+ return {
+ errors: [
+ {
+ message: CREATE_DESIGN_TODO_EXISTS_ERROR,
+ },
+ ],
+ };
+ }
+
+ const pendingTodo = createPendingTodo(todoId);
+ addPendingTodoToStore(cache, pendingTodo, getDesignQuery, {
+ fullPath: projectPath,
+ iid: issueIid,
+ filenames,
+ atVersion,
+ });
+
+ return pendingTodo;
+ });
+ },
},
};
@@ -37,6 +83,7 @@ const defaultClient = createDefaultClient(
},
},
typeDefs,
+ assumeImmutableResults: true,
},
);
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
index bc3132f9b42..9bd70e7e886 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql
@@ -5,4 +5,9 @@ fragment DesignListItem on Design {
notesCount
image
imageV432x230
+ currentUserTodos(state: pending) {
+ nodes {
+ id
+ }
+ }
}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
index 26edd2c0be1..28224671326 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
@@ -25,5 +25,10 @@ fragment DesignNote on Note {
}
discussion {
id
+ notes {
+ nodes {
+ id
+ }
+ }
}
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql
new file mode 100644
index 00000000000..0c989b2fdde
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql
@@ -0,0 +1,17 @@
+mutation createDesignTodo(
+ $projectPath: String!
+ $issueId: String!
+ $designId: String!
+ $issueIid: String!
+ $filenames: [String]!
+ $atVersion: String
+) {
+ createDesignTodo(
+ projectPath: $projectPath
+ issueId: $issueId
+ designId: $designId
+ issueIid: $issueIid
+ filenames: $filenames
+ atVersion: $atVersion
+ ) @client
+}
diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
index ab987dda525..96869a404b1 100644
--- a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
+++ b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
@@ -10,6 +10,7 @@ query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [Stri
nodes {
...DesignItem
issue {
+ id
title
webPath
webUrl
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
index 20c9cacf83f..1a87dd38137 100644
--- a/app/assets/javascripts/design_management/index.js
+++ b/app/assets/javascripts/design_management/index.js
@@ -4,7 +4,7 @@ import App from './components/app.vue';
import apolloProvider from './graphql';
export default () => {
- const el = document.querySelector('.js-design-management-new');
+ const el = document.querySelector('.js-design-management');
const { issueIid, projectPath, issuePath } = el.dataset;
const router = createRouter(issuePath);
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 17b72e73127..c6225c516e2 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -19,6 +19,8 @@ import {
extractDiscussions,
extractDesign,
updateImageDiffNoteOptimisticResponse,
+ toDiffNoteGid,
+ extractDesignNoteId,
} from '../../utils/design_management_utils';
import {
updateStoreAfterAddImageDiffNote,
@@ -31,6 +33,7 @@ import {
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
UPDATE_NOTE_ERROR,
+ TOGGLE_TODO_ERROR,
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking';
@@ -145,8 +148,11 @@ export default {
mounted() {
Mousetrap.bind('esc', this.closeDesign);
this.trackEvent();
- // We need to reset the active discussion when opening a new design
- this.updateActiveDiscussion();
+
+ // Set active discussion immediately.
+ // This will ensure that, if a note is specified in the URL hash,
+ // the browser will scroll to, and highlight, the note in the UI
+ this.updateActiveDiscussionFromUrl();
},
beforeDestroy() {
Mousetrap.unbind('esc', this.closeDesign);
@@ -221,7 +227,7 @@ export default {
},
onError(message, e) {
this.errorMessage = message;
- throw e;
+ if (e) throw e;
},
onCreateImageDiffNoteError(e) {
this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e);
@@ -241,6 +247,9 @@ export default {
onResolveDiscussionError(e) {
this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
},
+ onTodoError(e) {
+ this.onError(e?.message || TOGGLE_TODO_ERROR, e);
+ },
openCommentForm(annotationCoordinates) {
this.annotationCoordinates = annotationCoordinates;
if (this.$refs.newDiscussionForm) {
@@ -266,15 +275,20 @@ export default {
this.isLatestVersion,
);
},
- updateActiveDiscussion(id) {
+ updateActiveDiscussion(id, source = ACTIVE_DISCUSSION_SOURCE_TYPES.discussion) {
this.$apollo.mutate({
mutation: updateActiveDiscussionMutation,
variables: {
id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
+ source,
},
});
},
+ updateActiveDiscussionFromUrl() {
+ const noteId = extractDesignNoteId(this.$route.hash);
+ const diffNoteGid = noteId ? toDiffNoteGid(noteId) : undefined;
+ return this.updateActiveDiscussion(diffNoteGid, ACTIVE_DISCUSSION_SOURCE_TYPES.url);
+ },
toggleResolvedComments() {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
@@ -339,6 +353,7 @@ export default {
@updateNoteError="onUpdateNoteError"
@resolveDiscussionError="onResolveDiscussionError"
@toggleResolvedComments="toggleResolvedComments"
+ @todoError="onTodoError"
>
<template #replyForm>
<apollo-mutation
@@ -357,8 +372,8 @@ export default {
v-model="comment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="closeCommentForm"
+ @submit-form="mutate"
+ @cancel-form="closeCommentForm"
/> </apollo-mutation
></template>
</design-sidebar>
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index cd68e9d6c5b..6c4c8c75054 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -281,13 +281,8 @@ export default {
.mutate({
mutation: moveDesignMutation,
variables: this.designMoveVariables(newIndex, element),
- update: (store, { data: { designManagementMove } }) => {
- return updateDesignsOnStoreAfterReorder(
- store,
- designManagementMove,
- this.projectQueryBody,
- );
- },
+ update: (store, { data: { designManagementMove } }) =>
+ updateDesignsOnStoreAfterReorder(store, designManagementMove, this.projectQueryBody),
optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns),
})
.catch(() => {
@@ -327,7 +322,7 @@ export default {
v-if="isLatestVersion"
variant="link"
size="small"
- class="gl-mr-3 js-select-all"
+ class="gl-mr-4 js-select-all"
@click="toggleDesignsSelection"
>{{ selectAllButtonText }}
</gl-button>
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index b79df9d01d5..ff41136fd54 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -1,22 +1,27 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { groupBy } from 'lodash';
+import produce from 'immer';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
+import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
- ADD_DISCUSSION_COMMENT_ERROR,
+ DELETE_DESIGN_TODO_ERROR,
designDeletionError,
} from './error_messages';
+const designsOf = data => data.project.issue.designCollection.designs;
+
const deleteDesignsFromStore = (store, query, selectedDesigns) => {
- const data = store.readQuery(query);
+ const sourceData = store.readQuery(query);
- const changedDesigns = data.project.issue.designCollection.designs.nodes.filter(
- node => !selectedDesigns.includes(node.filename),
- );
- data.project.issue.designCollection.designs.nodes = [...changedDesigns];
+ const data = produce(sourceData, draftData => {
+ const changedDesigns = designsOf(sourceData).nodes.filter(
+ design => !selectedDesigns.includes(design.filename),
+ );
+ designsOf(draftData).nodes = [...changedDesigns];
+ });
store.writeQuery({
...query,
@@ -33,13 +38,15 @@ const deleteDesignsFromStore = (store, query, selectedDesigns) => {
*/
const addNewVersionToStore = (store, query, version) => {
if (!version) return;
+ const sourceData = store.readQuery(query);
- const data = store.readQuery(query);
-
- data.project.issue.designCollection.versions.nodes = [
- version,
- ...data.project.issue.designCollection.versions.nodes,
- ];
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection.versions.nodes = [
+ version,
+ ...draftData.project.issue.designCollection.versions.nodes,
+ ];
+ });
store.writeQuery({
...query,
@@ -47,47 +54,12 @@ const addNewVersionToStore = (store, query, version) => {
});
};
-const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => {
- const data = store.readQuery({
- query,
- variables: queryVariables,
- });
-
- const design = extractDesign(data);
- const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId);
- currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note];
-
- design.notesCount += 1;
- if (
- !design.issue.participants.nodes.some(
- participant => participant.username === createNote.note.author.username,
- )
- ) {
- design.issue.participants.nodes = [
- ...design.issue.participants.nodes,
- {
- __typename: 'User',
- ...createNote.note.author,
- },
- ];
- }
- store.writeQuery({
- query,
- variables: queryVariables,
- data: {
- ...data,
- design: {
- ...design,
- },
- },
- });
-};
-
const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query,
variables,
});
+
const newDiscussion = {
__typename: 'Discussion',
id: createImageDiffNote.note.discussion.id,
@@ -101,100 +73,100 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
nodes: [createImageDiffNote.note],
},
};
- const design = extractDesign(data);
- const notesCount = design.notesCount + 1;
- design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
- if (
- !design.issue.participants.nodes.some(
- participant => participant.username === createImageDiffNote.note.author.username,
- )
- ) {
- design.issue.participants.nodes = [
- ...design.issue.participants.nodes,
- {
- __typename: 'User',
- ...createImageDiffNote.note.author,
- },
- ];
- }
+
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ design.notesCount += 1;
+ design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
+
+ if (
+ !design.issue.participants.nodes.some(
+ participant => participant.username === createImageDiffNote.note.author.username,
+ )
+ ) {
+ design.issue.participants.nodes = [
+ ...design.issue.participants.nodes,
+ {
+ __typename: 'User',
+ ...createImageDiffNote.note.author,
+ },
+ ];
+ }
+ });
+
store.writeQuery({
query,
variables,
- data: {
- ...data,
- design: {
- ...design,
- notesCount,
- },
- },
+ data,
});
};
const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => {
- const data = store.readQuery({
+ const sourceData = store.readQuery({
query,
variables,
});
- const design = extractDesign(data);
- const discussion = extractCurrentDiscussion(
- design.discussions,
- updateImageDiffNote.note.discussion.id,
- );
-
- discussion.notes = {
- ...discussion.notes,
- nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
- };
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const discussion = extractCurrentDiscussion(
+ design.discussions,
+ updateImageDiffNote.note.discussion.id,
+ );
+
+ discussion.notes = {
+ ...discussion.notes,
+ nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
+ };
+ });
store.writeQuery({
query,
variables,
- data: {
- ...data,
- design,
- },
+ data,
});
};
const addNewDesignToStore = (store, designManagementUpload, query) => {
- const data = store.readQuery(query);
+ const sourceData = store.readQuery(query);
- const currentDesigns = data.project.issue.designCollection.designs.nodes;
- const existingDesigns = groupBy(currentDesigns, 'filename');
- const newDesigns = currentDesigns.concat(
- designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
- );
+ const data = produce(sourceData, draftData => {
+ const currentDesigns = extractDesigns(draftData);
+ const existingDesigns = groupBy(currentDesigns, 'filename');
+ const newDesigns = currentDesigns.concat(
+ designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
+ );
- let newVersionNode;
- const findNewVersions = designManagementUpload.designs.find(design => design.versions);
+ let newVersionNode;
+ const findNewVersions = designManagementUpload.designs.find(design => design.versions);
- if (findNewVersions) {
- const findNewVersionsNodes = findNewVersions.versions.nodes;
+ if (findNewVersions) {
+ const findNewVersionsNodes = findNewVersions.versions.nodes;
- if (findNewVersionsNodes && findNewVersionsNodes.length) {
- newVersionNode = [findNewVersionsNodes[0]];
+ if (findNewVersionsNodes && findNewVersionsNodes.length) {
+ newVersionNode = [findNewVersionsNodes[0]];
+ }
}
- }
-
- const newVersions = [
- ...(newVersionNode || []),
- ...data.project.issue.designCollection.versions.nodes,
- ];
- const updatedDesigns = {
- __typename: 'DesignCollection',
- designs: {
- __typename: 'DesignConnection',
- nodes: newDesigns,
- },
- versions: {
- __typename: 'DesignVersionConnection',
- nodes: newVersions,
- },
- };
+ const newVersions = [
+ ...(newVersionNode || []),
+ ...draftData.project.issue.designCollection.versions.nodes,
+ ];
- data.project.issue.designCollection = updatedDesigns;
+ const updatedDesigns = {
+ __typename: 'DesignCollection',
+ designs: {
+ __typename: 'DesignConnection',
+ nodes: newDesigns,
+ },
+ versions: {
+ __typename: 'DesignVersionConnection',
+ nodes: newVersions,
+ },
+ };
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection = updatedDesigns;
+ });
store.writeQuery({
...query,
@@ -203,14 +175,63 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
};
const moveDesignInStore = (store, designManagementMove, query) => {
- const data = store.readQuery(query);
- data.project.issue.designCollection.designs = designManagementMove.designCollection.designs;
+ const sourceData = store.readQuery(query);
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.issue.designCollection.designs =
+ designManagementMove.designCollection.designs;
+ });
+
store.writeQuery({
...query,
data,
});
};
+export const addPendingTodoToStore = (store, pendingTodo, query, queryVariables) => {
+ const sourceData = store.readQuery({
+ query,
+ variables: queryVariables,
+ });
+
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const existingTodos = design.currentUserTodos?.nodes || [];
+ const newTodoNodes = [...existingTodos, { ...pendingTodo, __typename: 'Todo' }];
+
+ if (!design.currentUserTodos) {
+ design.currentUserTodos = {
+ __typename: 'TodoConnection',
+ nodes: newTodoNodes,
+ };
+ } else {
+ design.currentUserTodos.nodes = newTodoNodes;
+ }
+ });
+
+ store.writeQuery({ query, variables: queryVariables, data });
+};
+
+export const deletePendingTodoFromStore = (store, todoMarkDone, query, queryVariables) => {
+ const sourceData = store.readQuery({
+ query,
+ variables: queryVariables,
+ });
+
+ const {
+ todo: { id: todoId },
+ } = todoMarkDone;
+ const data = produce(sourceData, draftData => {
+ const design = extractDesign(draftData);
+ const existingTodos = design.currentUserTodos?.nodes || [];
+
+ design.currentUserTodos.nodes = existingTodos.filter(({ id }) => id !== todoId);
+ });
+
+ store.writeQuery({ query, variables: queryVariables, data });
+};
+
const onError = (data, message) => {
createFlash(message);
throw new Error(data.errors);
@@ -235,20 +256,6 @@ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
}
};
-export const updateStoreAfterAddDiscussionComment = (
- store,
- data,
- query,
- queryVariables,
- discussionId,
-) => {
- if (hasErrors(data)) {
- onError(data, ADD_DISCUSSION_COMMENT_ERROR);
- } else {
- addDiscussionCommentToStore(store, data, query, queryVariables, discussionId);
- }
-};
-
export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => {
if (hasErrors(data)) {
onError(data, ADD_IMAGE_DIFF_NOTE_ERROR);
@@ -280,3 +287,11 @@ export const updateDesignsOnStoreAfterReorder = (store, data, query) => {
moveDesignInStore(store, data, query);
}
};
+
+export const updateStoreAfterDeleteDesignTodo = (store, data, query, queryVariables) => {
+ if (hasErrors(data)) {
+ onError(data, DELETE_DESIGN_TODO_ERROR);
+ } else {
+ deletePendingTodoFromStore(store, data, query, queryVariables);
+ }
+};
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 da8f89ff960..93e4d6060c3 100644
--- a/app/assets/javascripts/design_management/utils/design_management_utils.js
+++ b/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -30,10 +30,25 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
+export const findIssueId = id => (id.match('Issue/(.+$)') || [])[1];
+
+export const findDesignId = id => (id.match('Design/(.+$)') || [])[1];
+
export const extractDesigns = data => data.project.issue.designCollection.designs.nodes;
export const extractDesign = data => (extractDesigns(data) || [])[0];
+export const toDiffNoteGid = noteId => `gid://gitlab/DiffNote/${noteId}`;
+
+/**
+ * Return the note ID from a URL hash parameter
+ * @param {String} urlHash URL hash, including `#` prefix
+ */
+export const extractDesignNoteId = urlHash => {
+ const [, noteId] = urlHash.match('#note_([0-9]+$)') || [];
+ return noteId || null;
+};
+
/**
* Generates optimistic response for a design upload mutation
* @param {Array<File>} files
@@ -135,3 +150,22 @@ const normalizeAuthor = author => ({
export const extractParticipants = users => users.map(node => normalizeAuthor(node));
export const getPageLayoutElement = () => document.querySelector('.layout-page');
+
+/**
+ * Extract the ID of the To-Do for a given 'delete' path
+ * Example of todoDeletePath: /delete/1234
+ * @param {String} todoDeletePath delete_path from REST API response
+ */
+export const extractTodoIdFromDeletePath = todoDeletePath =>
+ (todoDeletePath.match('todos/([0-9]+$)') || [])[1];
+
+const createTodoGid = todoId => {
+ return `gid://gitlab/Todo/${todoId}`;
+};
+
+export const createPendingTodo = todoId => {
+ return {
+ __typename: 'Todo', // eslint-disable-line @gitlab/require-i18n-strings
+ id: createTodoGid(todoId),
+ };
+};
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index c815b11737d..bd21d711462 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -44,6 +44,14 @@ export const MOVE_DESIGN_ERROR = __(
'Something went wrong when reordering designs. Please try again',
);
+export const CREATE_DESIGN_TODO_ERROR = __('Failed to create To-Do for the design.');
+
+export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a To-Do for this design.');
+
+export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove To-Do for the design.');
+
+export const TOGGLE_TODO_ERROR = __('Failed to toggle To-Do for the design.');
+
const MAX_SKIPPED_FILES_LISTINGS = 5;
const oneDesignSkippedMessage = filename =>
diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js
index b3ecc1453a6..49fa306914c 100644
--- a/app/assets/javascripts/design_management/utils/tracking.js
+++ b/app/assets/javascripts/design_management/utils/tracking.js
@@ -5,7 +5,6 @@ const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_contex
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
const DESIGN_TRACKING_EVENT_NAME = 'view_design';
-// eslint-disable-next-line import/prefer-default-export
export function trackDesignDetailView(
referer = '',
owner = '',