diff options
Diffstat (limited to 'app/assets/javascripts/design_management')
31 files changed, 496 insertions, 425 deletions
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue index 1fd902c9ed7..37686dd5a46 100644 --- a/app/assets/javascripts/design_management/components/delete_button.vue +++ b/app/assets/javascripts/design_management/components/delete_button.vue @@ -1,11 +1,12 @@ <script> -import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui'; +import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import { uniqueId } from 'lodash'; +import { s__, __ } from '~/locale'; export default { name: 'DeleteButton', components: { - GlDeprecatedButton, + GlButton, GlModal, }, directives: { @@ -25,40 +26,78 @@ export default { buttonVariant: { type: String, required: false, - default: '', + default: 'info', + }, + buttonCategory: { + type: String, + required: false, + default: 'primary', + }, + buttonIcon: { + type: String, + required: false, + default: undefined, + }, + buttonSize: { + type: String, + required: false, + default: 'medium', }, hasSelectedDesigns: { type: Boolean, required: false, default: true, }, + loading: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { modalId: uniqueId('design-deletion-confirmation-'), }; }, + modal: { + title: s__('DesignManagement|Are you sure you want to archive the selected designs?'), + actionPrimary: { + text: s__('DesignManagement|Archive designs'), + attributes: { variant: 'warning' }, + }, + actionCancel: { + text: __('Cancel'), + }, + }, }; </script> <template> - <div> + <div class="gl-display-flex gl-align-items-center gl-h-full"> <gl-modal :modal-id="modalId" - :title="s__('DesignManagement|Delete designs confirmation')" - :ok-title="s__('DesignManagement|Delete')" - ok-variant="danger" + :title="$options.modal.title" + :action-primary="$options.modal.actionPrimary" + :action-cancel="$options.modal.actionCancel" @ok="$emit('deleteSelectedDesigns')" > - <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p> + <p> + {{ + s__( + 'DesignManagement|Archived designs will still be available in previous versions of the design collection.', + ) + }} + </p> </gl-modal> - <gl-deprecated-button + <gl-button v-gl-modal-directive="modalId" :variant="buttonVariant" - :disabled="isDeleting || !hasSelectedDesigns" + :category="buttonCategory" + :size="buttonSize" :class="buttonClass" - > - <slot></slot> - </gl-deprecated-button> + :loading="loading" + :icon="buttonIcon" + :disabled="isDeleting || !hasSelectedDesigns" + /> </div> </template> diff --git a/app/assets/javascripts/design_management/components/design_destroyer.vue b/app/assets/javascripts/design_management/components/design_destroyer.vue index 62460ca551c..7ae569216f0 100644 --- a/app/assets/javascripts/design_management/components/design_destroyer.vue +++ b/app/assets/javascripts/design_management/components/design_destroyer.vue @@ -13,13 +13,14 @@ export default { type: Array, required: true, }, + }, + inject: { projectPath: { - type: String, - required: true, + default: '', }, iid: { - type: String, - required: true, + from: 'issueIid', + defaut: '', }, }, computed: { 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 0811397fbad..2b5e62c2870 100644 --- a/app/assets/javascripts/design_management/components/design_note_pin.vue +++ b/app/assets/javascripts/design_management/components/design_note_pin.vue @@ -1,11 +1,11 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { name: 'DesignNotePin', components: { - Icon, + GlIcon, }, props: { position: { @@ -47,13 +47,13 @@ export default { 'btn-transparent comment-indicator': isNewNote, 'js-image-badge badge badge-pill': !isNewNote, }" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center" + class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0" type="button" @mousedown="$emit('mousedown', $event)" @mouseup="$emit('mouseup', $event)" @click="$emit('click', $event)" > - <icon v-if="isNewNote" name="image-comment-dark" /> + <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" /> <template v-else> {{ label }} </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 4aaf43e3a5b..6a20517eed7 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 @@ -230,10 +230,10 @@ export default { </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"> + <p class="gl-text-gray-500 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" + class="gl-text-gray-500 gl-text-decoration-none gl-font-sm link-inherit-color" :href="discussion.resolvedBy.webUrl" target="_blank" >{{ discussion.resolvedBy.name }}</gl-link 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 b1f3a43a66d..172e61920ef 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 @@ -60,7 +60,7 @@ export default { }, mounted() { if (this.isNoteLinked) { - this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' }); + this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' }); } }, methods: { @@ -80,7 +80,7 @@ export default { </script> <template> - <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form"> + <timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form"> <user-avatar-link :link-href="author.webUrl" :img-src="author.avatarUrl" 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 index 46c73e3eea8..2e366282de3 100644 --- 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 @@ -52,18 +52,18 @@ export default { {{ toggleText }} </gl-button> <template v-if="collapsed"> - <span class="gl-text-gray-700">{{ __('Last reply by') }}</span> + <span class="gl-text-gray-500">{{ __('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" + class="link-inherit-color gl-text-body 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" + class="gl-text-gray-500" /> </template> </li> diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue index 333ad2557e8..e5a3590877e 100644 --- a/app/assets/javascripts/design_management/components/design_sidebar.vue +++ b/app/assets/javascripts/design_management/components/design_sidebar.vue @@ -1,8 +1,8 @@ <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 { s__ } from '~/locale'; +import { parseBoolean } from '~/lib/utils/common_utils'; 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'; @@ -48,7 +48,7 @@ export default { }; }, discussionParticipants() { - return extractParticipants(this.issue.participants); + return extractParticipants(this.issue.participants.nodes); }, resolvedDiscussions() { return this.discussions.filter(discussion => discussion.resolved); @@ -94,7 +94,7 @@ export default { {{ issue.title }} </h2> <a - class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block" + class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block" :href="issue.webUrl" >{{ issue.webPath }}</a > @@ -132,7 +132,7 @@ export default { 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" + class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4" @click="$emit('toggleResolvedComments')" >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }}) </gl-button> diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue index eaa641d85d6..292b6e09055 100644 --- a/app/assets/javascripts/design_management/components/list/item.vue +++ b/app/assets/javascripts/design_management/components/list/item.vue @@ -74,7 +74,7 @@ export default { deletion: { name: 'file-deletion-solid', classes: 'text-danger-500', - tooltip: __('Deleted in this version'), + tooltip: __('Archived in this version'), }, }; @@ -127,10 +127,10 @@ export default { params: { id: filename }, query: $route.query, }" - class="card cursor-pointer text-plain js-design-list-item design-list-item" + class="card 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" class="design-event position-absolute"> + <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute"> <span :title="icon.tooltip" :aria-label="icon.tooltip"> <icon :name="icon.name" :size="18" :class="icon.classes" /> </span> diff --git a/app/assets/javascripts/design_management/components/toolbar/pagination.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue index bf62a8f66a6..afca8ed2c6f 100644 --- a/app/assets/javascripts/design_management/components/toolbar/pagination.vue +++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue @@ -1,14 +1,15 @@ <script> /* global Mousetrap */ import 'mousetrap'; +import { GlButton, GlButtonGroup } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; -import PaginationButton from './pagination_button.vue'; import allDesignsMixin from '../../mixins/all_designs'; import { DESIGN_ROUTE_NAME } from '../../router/constants'; export default { components: { - PaginationButton, + GlButton, + GlButtonGroup, }, mixins: [allDesignsMixin], props: { @@ -31,12 +32,12 @@ export default { }); }, previousDesign() { - if (!this.designsCount) return null; + if (this.currentIndex === 0) return null; return this.designs[this.currentIndex - 1]; }, nextDesign() { - if (!this.designsCount) return null; + if (this.currentIndex + 1 === this.designsCount) return null; return this.designs[this.currentIndex + 1]; }, @@ -65,19 +66,21 @@ export default { <template> <div v-if="designsCount" class="d-flex align-items-center"> {{ paginationText }} - <div class="btn-group ml-3 mr-3"> - <pagination-button - :design="previousDesign" + <gl-button-group class="ml-3 mr-3"> + <gl-button + :disabled="!previousDesign" :title="s__('DesignManagement|Go to previous design')" - icon-name="angle-left" + icon="angle-left" class="js-previous-design" + @click="navigateToDesign(previousDesign)" /> - <pagination-button - :design="nextDesign" + <gl-button + :disabled="!nextDesign" :title="s__('DesignManagement|Go to next design')" - icon-name="angle-right" + icon="angle-right" class="js-next-design" + @click="navigateToDesign(nextDesign)" /> - </div> + </gl-button-group> </div> </template> diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue index b998dfc47b8..a1cb57123ab 100644 --- a/app/assets/javascripts/design_management/components/toolbar/index.vue +++ b/app/assets/javascripts/design_management/components/toolbar/index.vue @@ -1,20 +1,18 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; +import { GlButton, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; -import Pagination from './pagination.vue'; +import DesignNavigation from './design_navigation.vue'; import DeleteButton from '../delete_button.vue'; import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql'; -import appDataQuery from '../../graphql/queries/app_data.query.graphql'; import { DESIGNS_ROUTE_NAME } from '../../router/constants'; export default { components: { - Icon, - Pagination, + GlButton, + GlIcon, + DesignNavigation, DeleteButton, - GlDeprecatedButton, }, mixins: [timeagoMixin], props: { @@ -55,19 +53,17 @@ export default { permissions: { createDesign: false, }, - projectPath: '', - issueIid: null, }; }, - apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, + inject: { + projectPath: { + default: '', }, + issueIid: { + default: '', + }, + }, + apollo: { permissions: { query: permissionsQuery, variables() { @@ -95,32 +91,36 @@ export default { </script> <template> - <header class="d-flex p-2 bg-white align-items-center js-design-header"> - <router-link - :to="{ - name: $options.DESIGNS_ROUTE_NAME, - query: $route.query, - }" - :aria-label="s__('DesignManagement|Go back to designs')" - class="mr-3 text-plain d-flex justify-content-center align-items-center" - > - <icon :size="18" name="close" /> - </router-link> - <div class="overflow-hidden d-flex align-items-center"> - <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2> - <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small> + <header + class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-bg-white gl-py-4 gl-pl-4 js-design-header" + > + <div class="gl-display-flex gl-align-items-center"> + <router-link + :to="{ + name: $options.DESIGNS_ROUTE_NAME, + query: $route.query, + }" + :aria-label="s__('DesignManagement|Go back to designs')" + data-testid="close-design" + class="gl-mr-5 gl-display-flex gl-align-items-center gl-justify-content-center text-plain" + > + <gl-icon name="close" /> + </router-link> + <div class="overflow-hidden d-flex align-items-center"> + <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2> + <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small> + </div> </div> - <pagination :id="id" class="ml-auto flex-shrink-0" /> - <gl-deprecated-button :href="image" class="mr-2"> - <icon :size="18" name="download" /> - </gl-deprecated-button> + <design-navigation :id="id" class="ml-auto flex-shrink-0" /> + <gl-button :href="image" icon="download" /> <delete-button v-if="isLatestVersion && canDeleteDesign" + class="gl-ml-3" :is-deleting="isDeleting" - button-variant="danger" + button-variant="warning" + button-icon="archive" + button-category="secondary" @deleteSelectedDesigns="$emit('delete')" - > - <icon :size="18" name="remove" /> - </delete-button> + /> </header> </template> diff --git a/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue deleted file mode 100644 index f00ecefca01..00000000000 --- a/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import Icon from '~/vue_shared/components/icon.vue'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - Icon, - }, - props: { - design: { - type: Object, - required: false, - default: null, - }, - title: { - type: String, - required: true, - }, - iconName: { - type: String, - required: true, - }, - }, - computed: { - designLink() { - if (!this.design) return {}; - - return { - name: DESIGN_ROUTE_NAME, - params: { id: this.design.filename }, - query: this.$route.query, - }; - }, - }, -}; -</script> - -<template> - <router-link - :to="designLink" - :disabled="!design" - :class="{ disabled: !design }" - :aria-label="title" - class="btn btn-default" - > - <icon :name="iconName" /> - </router-link> -</template> diff --git a/app/assets/javascripts/design_management/components/upload/button.vue b/app/assets/javascripts/design_management/components/upload/button.vue index 68555104a3c..c76041c74a8 100644 --- a/app/assets/javascripts/design_management/components/upload/button.vue +++ b/app/assets/javascripts/design_management/components/upload/button.vue @@ -1,10 +1,10 @@ <script> -import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants'; export default { components: { - GlDeprecatedButton, + GlButton, GlLoadingIcon, }, directives: { @@ -30,7 +30,7 @@ export default { <template> <div> - <gl-deprecated-button + <gl-button v-gl-tooltip.hover :title=" s__( @@ -38,12 +38,13 @@ export default { ) " :disabled="isSaving" - variant="success" + variant="default" + size="small" @click="openFileUpload" > {{ s__('DesignManagement|Upload designs') }} <gl-loading-icon v-if="isSaving" inline class="ml-1" /> - </gl-deprecated-button> + </gl-button> <input ref="fileUpload" diff --git a/app/assets/javascripts/design_management/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management/components/upload/design_dropzone.vue index 33261134c15..7254b7cd16a 100644 --- a/app/assets/javascripts/design_management/components/upload/design_dropzone.vue +++ b/app/assets/javascripts/design_management/components/upload/design_dropzone.vue @@ -1,6 +1,6 @@ <script> import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql'; import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages'; import { isValidDesignFile } from '../../utils/design_management_utils'; @@ -12,6 +12,17 @@ export default { GlLink, GlSprintf, }, + props: { + hasDesigns: { + type: Boolean, + required: true, + }, + isDraggingDesign: { + type: Boolean, + required: false, + default: false, + }, + }, data() { return { dragCounter: 0, @@ -22,6 +33,12 @@ export default { dragging() { return this.dragCounter !== 0; }, + iconStyles() { + return { + size: this.hasDesigns ? 24 : 16, + class: this.hasDesigns ? 'gl-mb-2' : 'gl-mr-3 gl-text-gray-500', + }; + }, }, methods: { isValidUpload(files) { @@ -76,25 +93,21 @@ export default { > <slot> <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" + class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3" @click="openFileUpload" > - <div class="d-flex-center flex-column text-center"> - <gl-icon name="doc-new" :size="48" class="mb-4" /> - <p> - <gl-sprintf - :message=" - __( - '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.', - ) - " - > - <template #lineOne="{ content }" - ><span class="d-block">{{ content }}</span> - </template> - + <div + :class="{ 'gl-flex-direction-column': hasDesigns }" + class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center" + data-testid="dropzone-area" + > + <gl-icon name="upload" :size="iconStyles.size" :class="iconStyles.class" /> + <p class="gl-mb-0"> + <gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} designs to attach')"> <template #link="{ content }"> - <gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link> + <gl-link @click.stop="openFileUpload"> + {{ content }} + </gl-link> </template> </gl-sprintf> </p> @@ -113,11 +126,11 @@ export default { </slot> <transition name="design-dropzone-fade"> <div - v-show="dragging" + v-show="dragging && !isDraggingDesign" class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" > <div v-show="!isDragDataValid" class="mw-50 text-center"> - <h3>{{ __('Oh no!') }}</h3> + <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Oh no!') }}</h3> <span>{{ __( 'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.', @@ -125,7 +138,7 @@ export default { }}</span> </div> <div v-show="isDragDataValid" class="mw-50 text-center"> - <h3>{{ __('Incoming!') }}</h3> + <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Incoming!') }}</h3> <span>{{ __('Drop your designs to start your upload.') }}</span> </div> </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 993eac6f37f..a03982cb91b 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,14 @@ <script> -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import allVersionsMixin from '../../mixins/all_versions'; import { findVersionId } from '../../utils/design_management_utils'; export default { components: { - GlDropdown, - GlDropdownItem, + GlNewDropdown, + GlNewDropdownItem, + GlSprintf, }, mixins: [allVersionsMixin], computed: { @@ -18,7 +19,7 @@ export default { if (!this.queryVersion) return 0; const idx = this.allVersions.findIndex( - version => this.findVersionId(version.node.id) === this.queryVersion, + version => this.findVersionId(version.id) === this.queryVersion, ); // if the currentVersionId isn't a valid version (i.e. not in allVersions) @@ -29,48 +30,52 @@ export default { if (this.queryVersion) return this.queryVersion; const currentVersion = this.allVersions[this.currentVersionIdx]; - return this.findVersionId(currentVersion.node.id); + return this.findVersionId(currentVersion.id); }, dropdownText() { if (this.isLatestVersion) { - return __('Showing Latest Version'); + return __('Showing latest version'); } // allVersions is sorted in reverse chronological order (latest first) const currentVersionNumber = this.allVersions.length - this.currentVersionIdx; - return sprintf(__('Showing Version #%{versionNumber}'), { + return sprintf(__('Showing version #%{versionNumber}'), { versionNumber: currentVersionNumber, }); }, }, methods: { findVersionId, + routeToVersion(versionId) { + this.$router.push({ + path: this.$route.path, + query: { version: this.findVersionId(versionId) }, + }); + }, + versionText(versionId) { + if (this.findVersionId(versionId) === this.latestVersionId) { + return __('Version %{versionNumber} (latest)'); + } + return __('Version %{versionNumber}'); + }, }, }; </script> <template> - <gl-dropdown :text="dropdownText" variant="link" class="design-version-dropdown"> - <gl-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id"> - <router-link - class="d-flex js-version-link" - :to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }" - > - <div class="flex-grow-1 ml-2"> - <div> - <strong - >{{ __('Version') }} {{ allVersions.length - index }} - <span v-if="findVersionId(version.node.id) === latestVersionId" - >({{ __('latest') }})</span - > - </strong> - </div> - </div> - <i - v-if="findVersionId(version.node.id) === currentVersionId" - class="fa fa-check pull-right" - ></i> - </router-link> - </gl-dropdown-item> - </gl-dropdown> + <gl-new-dropdown :text="dropdownText" size="small"> + <gl-new-dropdown-item + v-for="(version, index) in allVersions" + :key="version.id" + :is-check-item="true" + :is-checked="findVersionId(version.id) === currentVersionId" + @click="routeToVersion(version.id)" + > + <gl-sprintf :message="versionText(version.id)"> + <template #versionNumber> + {{ allVersions.length - index }} + </template> + </gl-sprintf> + </gl-new-dropdown-item> + </gl-new-dropdown> </template> diff --git a/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql index c8ade328120..0b8400ac040 100644 --- a/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql +++ b/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql @@ -8,10 +8,8 @@ mutation createImageDiffNote($input: CreateImageDiffNoteInput!) { id replyId notes { - edges { - node { - ...DesignNote - } + nodes { + ...DesignNote } } } diff --git a/app/assets/javascripts/design_management/graphql/mutations/move_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/move_design.mutation.graphql new file mode 100644 index 00000000000..144b2729999 --- /dev/null +++ b/app/assets/javascripts/design_management/graphql/mutations/move_design.mutation.graphql @@ -0,0 +1,18 @@ +#import "../fragments/design_list.fragment.graphql" + +mutation DesignManagementMove( + $id: DesignManagementDesignID! + $previous: DesignManagementDesignID + $next: DesignManagementDesignID +) { + designManagementMove(input: { id: $id, previous: $previous, next: $next }) { + designCollection { + designs { + nodes { + ...DesignListItem + } + } + } + errors + } +} diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql index d694e6558a0..84aeb374351 100644 --- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql +++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql @@ -5,11 +5,9 @@ mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) { designs { ...DesignItem versions { - edges { - node { - id - sha - } + nodes { + id + sha } } } diff --git a/app/assets/javascripts/design_management/graphql/queries/app_data.query.graphql b/app/assets/javascripts/design_management/graphql/queries/app_data.query.graphql deleted file mode 100644 index e1269761206..00000000000 --- a/app/assets/javascripts/design_management/graphql/queries/app_data.query.graphql +++ /dev/null @@ -1,4 +0,0 @@ -query projectFullPath { - projectPath @client - issueIid @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 07a9af55787..ab987dda525 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 @@ -7,19 +7,15 @@ query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [Stri issue(iid: $iid) { designCollection { designs(atVersion: $atVersion, filenames: $filenames) { - edges { - node { - ...DesignItem - issue { - title - webPath - webUrl - participants { - edges { - node { - ...Author - } - } + nodes { + ...DesignItem + issue { + title + webPath + webUrl + participants { + nodes { + ...Author } } } diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql index 121a50555b3..96efa8e8242 100644 --- a/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql +++ b/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql @@ -7,17 +7,13 @@ query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) { issue(iid: $iid) { designCollection { designs(atVersion: $atVersion) { - edges { - node { - ...DesignListItem - } + nodes { + ...DesignListItem } } versions { - edges { - node { - ...VersionListItem - } + nodes { + ...VersionListItem } } } diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js index 1fc5779515a..20c9cacf83f 100644 --- a/app/assets/javascripts/design_management/index.js +++ b/app/assets/javascripts/design_management/index.js @@ -1,32 +1,15 @@ -// This application is being moved, please do not touch this files -// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details - -import $ from 'jquery'; import Vue from 'vue'; import createRouter from './router'; import App from './components/app.vue'; import apolloProvider from './graphql'; -import getDesignListQuery from './graphql/queries/get_design_list.query.graphql'; -import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants'; export default () => { - const el = document.querySelector('.js-design-management'); - const badge = document.querySelector('.js-designs-count'); + const el = document.querySelector('.js-design-management-new'); const { issueIid, projectPath, issuePath } = el.dataset; const router = createRouter(issuePath); - $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => { - if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) { - router.push({ name: DESIGNS_ROUTE_NAME }); - } else if (id === 'discussion') { - router.push({ name: ROOT_ROUTE_NAME }); - } - }); - apolloProvider.clients.defaultClient.cache.writeData({ data: { - projectPath, - issueIid, activeDiscussion: { __typename: 'ActiveDiscussion', id: null, @@ -35,25 +18,14 @@ export default () => { }, }); - apolloProvider.clients.defaultClient - .watchQuery({ - query: getDesignListQuery, - variables: { - fullPath: projectPath, - iid: issueIid, - atVersion: null, - }, - }) - .subscribe(({ data }) => { - if (badge) { - badge.textContent = data.project.issue.designCollection.designs.edges.length; - } - }); - return new Vue({ el, router, apolloProvider, + provide: { + projectPath, + issueIid, + }, render(createElement) { return createElement(App); }, diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js index f7d6551c46c..0c2858bb14b 100644 --- a/app/assets/javascripts/design_management/mixins/all_designs.js +++ b/app/assets/javascripts/design_management/mixins/all_designs.js @@ -1,8 +1,7 @@ import { propertyOf } from 'lodash'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { s__ } from '~/locale'; import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import { extractNodes } from '../utils/design_management_utils'; import allVersionsMixin from './all_versions'; import { DESIGNS_ROUTE_NAME } from '../router/constants'; @@ -19,9 +18,15 @@ export default { }; }, update: data => { - const designEdges = propertyOf(data)(['project', 'issue', 'designCollection', 'designs']); - if (designEdges) { - return extractNodes(designEdges); + const designNodes = propertyOf(data)([ + 'project', + 'issue', + 'designCollection', + 'designs', + 'nodes', + ]); + if (designNodes) { + return designNodes; } return []; }, diff --git a/app/assets/javascripts/design_management/mixins/all_versions.js b/app/assets/javascripts/design_management/mixins/all_versions.js index 3966fe71732..7a094f23378 100644 --- a/app/assets/javascripts/design_management/mixins/all_versions.js +++ b/app/assets/javascripts/design_management/mixins/all_versions.js @@ -1,17 +1,8 @@ import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import appDataQuery from '../graphql/queries/app_data.query.graphql'; import { findVersionId } from '../utils/design_management_utils'; export default { apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, - }, allVersions: { query: getDesignListQuery, variables() { @@ -21,7 +12,15 @@ export default { atVersion: null, }; }, - update: data => data.project.issue.designCollection.versions.edges, + update: data => data.project.issue.designCollection.versions.nodes, + }, + }, + inject: { + projectPath: { + default: '', + }, + issueIid: { + default: '', }, }, computed: { @@ -29,7 +28,7 @@ export default { return ( this.$route.query.version && this.allVersions && - this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version)) + this.allVersions.some(version => version.id.endsWith(this.$route.query.version)) ); }, designsVersion() { @@ -39,7 +38,7 @@ export default { }, latestVersionId() { const latestVersion = this.allVersions[0]; - return latestVersion && findVersionId(latestVersion.node.id); + return latestVersion && findVersionId(latestVersion.id); }, isLatestVersion() { if (this.allVersions.length > 0) { @@ -55,8 +54,6 @@ export default { data() { return { allVersions: [], - projectPath: '', - issueIid: null, }; }, }; diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 9a959222e22..17b72e73127 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -2,7 +2,7 @@ import Mousetrap from 'mousetrap'; import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; import { ApolloMutation } from 'vue-apollo'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; import allVersionsMixin from '../../mixins/all_versions'; import Toolbar from '../../components/toolbar/index.vue'; @@ -12,7 +12,6 @@ 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/get_design.query.graphql'; -import appDataQuery from '../../graphql/queries/app_data.query.graphql'; import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql'; import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql'; import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql'; @@ -62,22 +61,12 @@ export default { design: {}, comment: '', annotationCoordinates: null, - projectPath: '', errorMessage: '', - issueIid: '', scale: 1, resolvedDiscussionsExpanded: false, }; }, apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, - }, design: { query: getDesignQuery, // We want to see cached design version if we have one, and fetch newer version on the background to update discussions diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index d14a1fc8c1c..cd68e9d6c5b 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -1,6 +1,7 @@ <script> -import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui'; +import VueDraggable from 'vuedraggable'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { s__, sprintf } from '~/locale'; import UploadButton from '../components/upload/button.vue'; import DeleteButton from '../components/delete_button.vue'; @@ -9,6 +10,7 @@ import DesignDestroyer from '../components/design_destroyer.vue'; import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue'; import DesignDropzone from '../components/upload/design_dropzone.vue'; import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql'; +import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql'; import permissionsQuery from '../graphql/queries/design_permissions.query.graphql'; import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; import allDesignsMixin from '../mixins/all_designs'; @@ -16,13 +18,18 @@ import { UPLOAD_DESIGN_ERROR, EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE, + MOVE_DESIGN_ERROR, designUploadSkippedWarning, designDeletionError, } from '../utils/error_messages'; -import { updateStoreAfterUploadDesign } from '../utils/cache_update'; +import { + updateStoreAfterUploadDesign, + updateDesignsOnStoreAfterReorder, +} from '../utils/cache_update'; import { designUploadOptimisticResponse, isValidDesignFile, + moveDesignOptimisticResponse, } from '../utils/design_management_utils'; import { getFilename } from '~/lib/utils/file_upload'; import { DESIGNS_ROUTE_NAME } from '../router/constants'; @@ -33,13 +40,14 @@ export default { components: { GlLoadingIcon, GlAlert, - GlDeprecatedButton, + GlButton, UploadButton, Design, DesignDestroyer, DesignVersionDropdown, DeleteButton, DesignDropzone, + VueDraggable, }, mixins: [allDesignsMixin], apollo: { @@ -61,6 +69,8 @@ export default { }, filesToBeSaved: [], selectedDesigns: [], + isDraggingDesign: false, + reorderedDesigns: null, }; }, computed: { @@ -96,9 +106,19 @@ export default { ? s__('DesignManagement|Deselect all') : s__('DesignManagement|Select all'); }, + isDesignListEmpty() { + return !this.isSaving && !this.hasDesigns; + }, + designDropzoneWrapperClass() { + return this.isDesignListEmpty + ? 'col-12' + : 'gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3'; + }, }, mounted() { - this.toggleOnPasteListener(this.$route.name); + if (this.$route.path === '/designs') { + this.$el.scrollIntoView(); + } }, methods: { resetFilesToBeSaved() { @@ -238,56 +258,97 @@ export default { this.onUploadDesign([newFile]); } }, - toggleOnPasteListener(route) { - if (route === DESIGNS_ROUTE_NAME) { - document.addEventListener('paste', this.onDesignPaste); - } else { - document.removeEventListener('paste', this.onDesignPaste); + toggleOnPasteListener() { + document.addEventListener('paste', this.onDesignPaste); + }, + toggleOffPasteListener() { + document.removeEventListener('paste', this.onDesignPaste); + }, + designMoveVariables(newIndex, element) { + const variables = { + id: element.id, + }; + if (newIndex > 0) { + variables.previous = this.reorderedDesigns[newIndex - 1].id; + } + if (newIndex < this.reorderedDesigns.length - 1) { + variables.next = this.reorderedDesigns[newIndex + 1].id; } + return variables; + }, + reorderDesigns({ moved: { newIndex, element } }) { + this.$apollo + .mutate({ + mutation: moveDesignMutation, + variables: this.designMoveVariables(newIndex, element), + update: (store, { data: { designManagementMove } }) => { + return updateDesignsOnStoreAfterReorder( + store, + designManagementMove, + this.projectQueryBody, + ); + }, + optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns), + }) + .catch(() => { + createFlash(MOVE_DESIGN_ERROR); + }); + }, + onDesignMove(designs) { + this.reorderedDesigns = designs; }, }, beforeRouteUpdate(to, from, next) { - this.toggleOnPasteListener(to.name); this.selectedDesigns = []; next(); }, - beforeRouteLeave(to, from, next) { - this.toggleOnPasteListener(to.name); - next(); + dragOptions: { + animation: 200, + ghostClass: 'gl-visibility-hidden', }, }; </script> <template> - <div> + <div + data-testid="designs-root" + class="gl-mt-5" + @mouseenter="toggleOnPasteListener" + @mouseleave="toggleOffPasteListener" + > <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex"> - <div class="d-flex justify-content-between align-items-center w-100"> - <design-version-dropdown /> - <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]"> - <gl-deprecated-button + <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"> + <div> + <span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span> + <design-version-dropdown /> + </div> + <div v-show="hasDesigns" class="qa-selector-toolbar gl-display-flex gl-align-items-center"> + <gl-button v-if="isLatestVersion" variant="link" - class="mr-2 js-select-all" + size="small" + class="gl-mr-3 js-select-all" @click="toggleDesignsSelection" - >{{ selectAllButtonText }}</gl-deprecated-button - > + >{{ selectAllButtonText }} + </gl-button> <design-destroyer #default="{ mutate, loading }" :filenames="selectedDesigns" - :project-path="projectPath" - :iid="issueIid" @done="onDesignDelete" @error="onDesignDeleteError" > <delete-button v-if="isLatestVersion" :is-deleting="loading" - button-class="btn-danger btn-inverted mr-2" + button-variant="warning" + button-category="secondary" + button-class="gl-mr-3" + button-size="small" + :loading="loading" :has-selected-designs="hasSelectedDesigns" @deleteSelectedDesigns="mutate()" > - {{ s__('DesignManagement|Delete selected') }} - <gl-loading-icon v-if="loading" inline class="ml-1" /> + {{ s__('DesignManagement|Archive selected') }} </delete-button> </design-destroyer> <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" /> @@ -299,14 +360,35 @@ export default { <gl-alert v-else-if="error" variant="danger" :dismissible="false"> {{ __('An error occurred while loading designs. Please try again.') }} </gl-alert> - <ol v-else class="list-unstyled row"> - <li class="col-md-6 col-lg-4 mb-3"> - <design-dropzone class="design-list-item" @change="onUploadDesign" /> - </li> - <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3"> - <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)" - ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)" - /></design-dropzone> + <vue-draggable + v-else + :value="designs" + :disabled="!isLatestVersion" + v-bind="$options.dragOptions" + tag="ol" + draggable=".js-design-tile" + class="list-unstyled row" + @start="isDraggingDesign = true" + @end="isDraggingDesign = false" + @change="reorderDesigns" + @input="onDesignMove" + > + <li + v-for="design in designs" + :key="design.id" + class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile" + > + <design-dropzone + :has-designs="hasDesigns" + :is-dragging-design="isDraggingDesign" + @change="onExistingDesignDropzoneChange($event, design.filename)" + > + <design + v-bind="design" + :is-uploading="isDesignToBeSaved(design.filename)" + class="gl-bg-white" + /> + </design-dropzone> <input v-if="canSelectDesign(design.filename)" @@ -316,7 +398,17 @@ export default { @change="changeSelectedDesigns(design.filename)" /> </li> - </ol> + <template #header> + <li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper"> + <design-dropzone + :is-dragging-design="isDraggingDesign" + :class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }" + :has-designs="hasDesigns" + @change="onUploadDesign" + /> + </li> + </template> + </vue-draggable> </div> <router-view :key="$route.fullPath" /> </div> diff --git a/app/assets/javascripts/design_management/router/constants.js b/app/assets/javascripts/design_management/router/constants.js index abeef520e33..dd2ee8d8689 100644 --- a/app/assets/javascripts/design_management/router/constants.js +++ b/app/assets/javascripts/design_management/router/constants.js @@ -1,3 +1,2 @@ -export const ROOT_ROUTE_NAME = 'root'; export const DESIGNS_ROUTE_NAME = 'designs'; export const DESIGN_ROUTE_NAME = 'design'; diff --git a/app/assets/javascripts/design_management/router/index.js b/app/assets/javascripts/design_management/router/index.js index 7494da002c8..cbeb2f7ce42 100644 --- a/app/assets/javascripts/design_management/router/index.js +++ b/app/assets/javascripts/design_management/router/index.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import Vue from 'vue'; import VueRouter from 'vue-router'; import routes from './routes'; @@ -16,9 +15,7 @@ export default function createRouter(base) { }); const pageEl = getPageLayoutElement(); - router.beforeEach(({ meta: { el }, name }, _, next) => { - $(`#${el}`).tab('show'); - + router.beforeEach(({ name }, _, next) => { // apply a fullscreen layout style in Design View (a.k.a design detail) if (pageEl) { if (name === DESIGN_ROUTE_NAME) { diff --git a/app/assets/javascripts/design_management/router/routes.js b/app/assets/javascripts/design_management/router/routes.js index 788910e5514..d888b856611 100644 --- a/app/assets/javascripts/design_management/router/routes.js +++ b/app/assets/javascripts/design_management/router/routes.js @@ -1,44 +1,29 @@ import Home from '../pages/index.vue'; import DesignDetail from '../pages/design/index.vue'; -import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants'; +import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants'; export default [ { - name: ROOT_ROUTE_NAME, + name: DESIGNS_ROUTE_NAME, path: '/', component: Home, - meta: { - el: 'discussion', - }, + alias: '/designs', }, { - name: DESIGNS_ROUTE_NAME, - path: '/designs', - component: Home, - meta: { - el: 'designs', - }, - children: [ + name: DESIGN_ROUTE_NAME, + path: '/designs/:id', + component: DesignDetail, + beforeEnter( { - name: DESIGN_ROUTE_NAME, - path: ':id', - component: DesignDetail, - meta: { - el: 'designs', - }, - beforeEnter( - { - params: { id }, - }, - from, - next, - ) { - if (typeof id === 'string') { - next(); - } - }, - props: ({ params: { id } }) => ({ id }), + params: { id }, }, - ], + _, + next, + ) { + if (typeof id === 'string') { + next(); + } + }, + props: ({ params: { id } }) => ({ id }), }, ]; diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js index 24b374b79fd..b79df9d01d5 100644 --- a/app/assets/javascripts/design_management/utils/cache_update.js +++ b/app/assets/javascripts/design_management/utils/cache_update.js @@ -1,6 +1,7 @@ /* eslint-disable @gitlab/require-i18n-strings */ -import createFlash from '~/flash'; +import { groupBy } from 'lodash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { extractCurrentDiscussion, extractDesign } from './design_management_utils'; import { ADD_IMAGE_DIFF_NOTE_ERROR, @@ -12,10 +13,10 @@ import { const deleteDesignsFromStore = (store, query, selectedDesigns) => { const data = store.readQuery(query); - const changedDesigns = data.project.issue.designCollection.designs.edges.filter( - ({ node }) => !selectedDesigns.includes(node.filename), + const changedDesigns = data.project.issue.designCollection.designs.nodes.filter( + node => !selectedDesigns.includes(node.filename), ); - data.project.issue.designCollection.designs.edges = [...changedDesigns]; + data.project.issue.designCollection.designs.nodes = [...changedDesigns]; store.writeQuery({ ...query, @@ -34,11 +35,10 @@ const addNewVersionToStore = (store, query, version) => { if (!version) return; const data = store.readQuery(query); - const newEdge = { node: version, __typename: 'DesignVersionEdge' }; - data.project.issue.designCollection.versions.edges = [ - newEdge, - ...data.project.issue.designCollection.versions.edges, + data.project.issue.designCollection.versions.nodes = [ + version, + ...data.project.issue.designCollection.versions.nodes, ]; store.writeQuery({ @@ -59,18 +59,15 @@ const addDiscussionCommentToStore = (store, createNote, query, queryVariables, d design.notesCount += 1; if ( - !design.issue.participants.edges.some( - participant => participant.node.username === createNote.note.author.username, + !design.issue.participants.nodes.some( + participant => participant.username === createNote.note.author.username, ) ) { - design.issue.participants.edges = [ - ...design.issue.participants.edges, + design.issue.participants.nodes = [ + ...design.issue.participants.nodes, { - __typename: 'UserEdge', - node: { - __typename: 'User', - ...createNote.note.author, - }, + __typename: 'User', + ...createNote.note.author, }, ]; } @@ -108,18 +105,15 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) = const notesCount = design.notesCount + 1; design.discussions.nodes = [...design.discussions.nodes, newDiscussion]; if ( - !design.issue.participants.edges.some( - participant => participant.node.username === createImageDiffNote.note.author.username, + !design.issue.participants.nodes.some( + participant => participant.username === createImageDiffNote.note.author.username, ) ) { - design.issue.participants.edges = [ - ...design.issue.participants.edges, + design.issue.participants.nodes = [ + ...design.issue.participants.nodes, { - __typename: 'UserEdge', - node: { - __typename: 'User', - ...createImageDiffNote.note.author, - }, + __typename: 'User', + ...createImageDiffNote.note.author, }, ]; } @@ -166,42 +160,37 @@ const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables const addNewDesignToStore = (store, designManagementUpload, query) => { const data = store.readQuery(query); - const newDesigns = data.project.issue.designCollection.designs.edges.reduce((acc, design) => { - if (!acc.find(d => d.filename === design.node.filename)) { - acc.push(design.node); - } - - return acc; - }, designManagementUpload.designs); + const currentDesigns = data.project.issue.designCollection.designs.nodes; + 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); if (findNewVersions) { - const findNewVersionsEdges = findNewVersions.versions.edges; + const findNewVersionsNodes = findNewVersions.versions.nodes; - if (findNewVersionsEdges && findNewVersionsEdges.length) { - newVersionNode = [findNewVersionsEdges[0]]; + if (findNewVersionsNodes && findNewVersionsNodes.length) { + newVersionNode = [findNewVersionsNodes[0]]; } } const newVersions = [ ...(newVersionNode || []), - ...data.project.issue.designCollection.versions.edges, + ...data.project.issue.designCollection.versions.nodes, ]; const updatedDesigns = { __typename: 'DesignCollection', designs: { __typename: 'DesignConnection', - edges: newDesigns.map(design => ({ - __typename: 'DesignEdge', - node: design, - })), + nodes: newDesigns, }, versions: { __typename: 'DesignVersionConnection', - edges: newVersions, + nodes: newVersions, }, }; @@ -213,6 +202,15 @@ const addNewDesignToStore = (store, designManagementUpload, query) => { }); }; +const moveDesignInStore = (store, designManagementMove, query) => { + const data = store.readQuery(query); + data.project.issue.designCollection.designs = designManagementMove.designCollection.designs; + store.writeQuery({ + ...query, + data, + }); +}; + const onError = (data, message) => { createFlash(message); throw new Error(data.errors); @@ -274,3 +272,11 @@ export const updateStoreAfterUploadDesign = (store, data, query) => { addNewDesignToStore(store, data, query); } }; + +export const updateDesignsOnStoreAfterReorder = (store, data, query) => { + if (hasErrors(data)) { + createFlash(data.errors[0]); + } else { + moveDesignInStore(store, data, query); + } +}; 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 22705cf67a1..da8f89ff960 100644 --- a/app/assets/javascripts/design_management/utils/design_management_utils.js +++ b/app/assets/javascripts/design_management/utils/design_management_utils.js @@ -5,17 +5,7 @@ export const isValidDesignFile = ({ type }) => (type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0; /** - * Returns formatted array that doesn't contain - * `edges`->`node` nesting - * - * @param {Array} elements - */ - -export const extractNodes = elements => elements.edges.map(({ node }) => node); - -/** - * Returns formatted array of discussions that doesn't contain - * `edges`->`node` nesting for child notes + * Returns formatted array of discussions * * @param {Array} discussions */ @@ -40,9 +30,9 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1]; export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1]; -export const extractDesigns = data => data.project.issue.designCollection.designs.edges; +export const extractDesigns = data => data.project.issue.designCollection.designs.nodes; -export const extractDesign = data => (extractDesigns(data) || [])[0]?.node; +export const extractDesign = data => (extractDesigns(data) || [])[0]; /** * Generates optimistic response for a design upload mutation @@ -72,13 +62,10 @@ export const designUploadOptimisticResponse = files => { }, versions: { __typename: 'DesignVersionConnection', - edges: { - __typename: 'DesignVersionEdge', - node: { - __typename: 'DesignVersion', - id: -uniqueId(), - sha: -uniqueId(), - }, + nodes: { + __typename: 'DesignVersion', + id: -uniqueId(), + sha: -uniqueId(), }, }, })); @@ -98,7 +85,8 @@ export const designUploadOptimisticResponse = files => { /** * Generates optimistic response for a design upload mutation - * @param {Array<File>} files + * @param {Object} note + * @param {Object} position */ export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({ // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 @@ -117,12 +105,33 @@ export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({ }, }); +/** + * Generates optimistic response for a design upload mutation + * @param {Array} designs + */ +export const moveDesignOptimisticResponse = designs => ({ + // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 + // eslint-disable-next-line @gitlab/require-i18n-strings + __typename: 'Mutation', + designManagementMove: { + __typename: 'DesignManagementMovePayload', + designCollection: { + __typename: 'DesignCollection', + designs: { + __typename: 'DesignConnection', + nodes: designs, + }, + }, + errors: [], + }, +}); + const normalizeAuthor = author => ({ ...author, web_url: author.webUrl, avatar_url: author.avatarUrl, }); -export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node)); +export const extractParticipants = users => users.map(node => normalizeAuthor(node)); export const getPageLayoutElement = () => document.querySelector('.layout-page'); diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js index 7666c726c2f..c815b11737d 100644 --- a/app/assets/javascripts/design_management/utils/error_messages.js +++ b/app/assets/javascripts/design_management/utils/error_messages.js @@ -40,6 +40,10 @@ export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __( 'You must upload a file with the same file name when dropping onto an existing design.', ); +export const MOVE_DESIGN_ERROR = __( + 'Something went wrong when reordering designs. Please try again', +); + const MAX_SKIPPED_FILES_LISTINGS = 5; const oneDesignSkippedMessage = filename => @@ -69,7 +73,7 @@ const someDesignsSkippedMessage = skippedFiles => { export const designDeletionError = ({ singular = true } = {}) => { const design = singular ? __('a design') : __('designs'); - return sprintf(s__('Could not delete %{design}. Please try again.'), { + return sprintf(s__('Could not archive %{design}. Please try again.'), { design, }); }; |