diff options
Diffstat (limited to 'app/assets/javascripts/design_management_new/pages')
-rw-r--r-- | app/assets/javascripts/design_management_new/pages/design/index.vue | 367 | ||||
-rw-r--r-- | app/assets/javascripts/design_management_new/pages/index.vue | 346 |
2 files changed, 0 insertions, 713 deletions
diff --git a/app/assets/javascripts/design_management_new/pages/design/index.vue b/app/assets/javascripts/design_management_new/pages/design/index.vue deleted file mode 100644 index 47f5e3a786f..00000000000 --- a/app/assets/javascripts/design_management_new/pages/design/index.vue +++ /dev/null @@ -1,367 +0,0 @@ -<script> -import Mousetrap from 'mousetrap'; -import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; -import { ApolloMutation } from 'vue-apollo'; -import createFlash from '~/flash'; -import { fetchPolicies } from '~/lib/graphql'; -import allVersionsMixin from '../../mixins/all_versions'; -import Toolbar from '../../components/toolbar/index.vue'; -import DesignDestroyer from '../../components/design_destroyer.vue'; -import DesignScaler from '../../components/design_scaler.vue'; -import DesignPresentation from '../../components/design_presentation.vue'; -import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; -import DesignSidebar from '../../components/design_sidebar.vue'; -import getDesignQuery from '../../graphql/queries/get_design.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'; -import { - extractDiscussions, - extractDesign, - updateImageDiffNoteOptimisticResponse, -} from '../../utils/design_management_utils'; -import { - updateStoreAfterAddImageDiffNote, - updateStoreAfterUpdateImageDiffNote, -} from '../../utils/cache_update'; -import { - ADD_DISCUSSION_COMMENT_ERROR, - ADD_IMAGE_DIFF_NOTE_ERROR, - UPDATE_IMAGE_DIFF_NOTE_ERROR, - DESIGN_NOT_FOUND_ERROR, - DESIGN_VERSION_NOT_EXIST_ERROR, - UPDATE_NOTE_ERROR, - designDeletionError, -} from '../../utils/error_messages'; -import { trackDesignDetailView } from '../../utils/tracking'; -import { DESIGNS_ROUTE_NAME } from '../../router/constants'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; - -export default { - components: { - ApolloMutation, - DesignReplyForm, - DesignPresentation, - DesignScaler, - DesignDestroyer, - Toolbar, - GlLoadingIcon, - GlAlert, - DesignSidebar, - }, - mixins: [allVersionsMixin], - props: { - id: { - type: String, - required: true, - }, - }, - data() { - return { - design: {}, - comment: '', - annotationCoordinates: null, - errorMessage: '', - scale: 1, - resolvedDiscussionsExpanded: false, - }; - }, - apollo: { - design: { - query: getDesignQuery, - // We want to see cached design version if we have one, and fetch newer version on the background to update discussions - fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, - variables() { - return this.designVariables; - }, - update: data => extractDesign(data), - result(res) { - this.onDesignQueryResult(res); - }, - error() { - this.onQueryError(DESIGN_NOT_FOUND_ERROR); - }, - }, - }, - computed: { - isFirstLoading() { - // We only want to show spinner on initial design load (when opened from a deep link to design) - // If we already have cached a design, loading shouldn't be indicated to user - return this.$apollo.queries.design.loading && !this.design.filename; - }, - discussions() { - if (!this.design.discussions) { - return []; - } - return extractDiscussions(this.design.discussions); - }, - markdownPreviewPath() { - return `/${this.projectPath}/preview_markdown?target_type=Issue`; - }, - isSubmitButtonDisabled() { - return this.comment.trim().length === 0; - }, - designVariables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - filenames: [this.$route.params.id], - atVersion: this.designsVersion, - }; - }, - mutationPayload() { - const { x, y, width, height } = this.annotationCoordinates; - return { - noteableId: this.design.id, - body: this.comment, - position: { - headSha: this.design.diffRefs.headSha, - baseSha: this.design.diffRefs.baseSha, - startSha: this.design.diffRefs.startSha, - x, - y, - width, - height, - paths: { - newPath: this.design.fullPath, - }, - }, - }; - }, - isAnnotating() { - return Boolean(this.annotationCoordinates); - }, - resolvedDiscussions() { - return this.discussions.filter(discussion => discussion.resolved); - }, - }, - watch: { - resolvedDiscussions(val) { - if (!val.length) { - this.resolvedDiscussionsExpanded = false; - } - }, - }, - mounted() { - Mousetrap.bind('esc', this.closeDesign); - this.trackEvent(); - // We need to reset the active discussion when opening a new design - this.updateActiveDiscussion(); - }, - beforeDestroy() { - Mousetrap.unbind('esc', this.closeDesign); - }, - methods: { - addImageDiffNoteToStore( - store, - { - data: { createImageDiffNote }, - }, - ) { - updateStoreAfterAddImageDiffNote( - store, - createImageDiffNote, - getDesignQuery, - this.designVariables, - ); - }, - updateImageDiffNoteInStore( - store, - { - data: { updateImageDiffNote }, - }, - ) { - return updateStoreAfterUpdateImageDiffNote( - store, - updateImageDiffNote, - getDesignQuery, - this.designVariables, - ); - }, - onMoveNote({ noteId, discussionId, position }) { - const discussion = this.discussions.find(({ id }) => id === discussionId); - const note = discussion.notes.find( - ({ discussion: noteDiscussion }) => noteDiscussion.id === discussionId, - ); - - const mutationPayload = { - optimisticResponse: updateImageDiffNoteOptimisticResponse(note, { - position, - }), - variables: { - input: { - id: noteId, - position, - }, - }, - mutation: updateImageDiffNoteMutation, - update: this.updateImageDiffNoteInStore, - }; - - return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e)); - }, - onDesignQueryResult({ data, loading }) { - // On the initial load with cache-and-network policy data is undefined while loading is true - // To prevent throwing an error, we don't perform any logic until loading is false - if (loading) { - return; - } - - if (!data || !extractDesign(data)) { - this.onQueryError(DESIGN_NOT_FOUND_ERROR); - } else if (this.$route.query.version && !this.hasValidVersion) { - this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR); - } - }, - onQueryError(message) { - // because we redirect user to /designs (the issue page), - // we want to create these flashes on the issue page - createFlash(message); - this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME }); - }, - onError(message, e) { - this.errorMessage = message; - throw e; - }, - onCreateImageDiffNoteError(e) { - this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e); - }, - onUpdateNoteError(e) { - this.onError(UPDATE_NOTE_ERROR, e); - }, - onDesignDiscussionError(e) { - this.onError(ADD_DISCUSSION_COMMENT_ERROR, e); - }, - onUpdateImageDiffNoteError(e) { - this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); - }, - onDesignDeleteError(e) { - this.onError(designDeletionError({ singular: true }), e); - }, - onResolveDiscussionError(e) { - this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); - }, - openCommentForm(annotationCoordinates) { - this.annotationCoordinates = annotationCoordinates; - if (this.$refs.newDiscussionForm) { - this.$refs.newDiscussionForm.focusInput(); - } - }, - closeCommentForm() { - this.comment = ''; - this.annotationCoordinates = null; - }, - closeDesign() { - this.$router.push({ - name: this.$options.DESIGNS_ROUTE_NAME, - query: this.$route.query, - }); - }, - trackEvent() { - // TODO: This needs to be made aware of referers, or if it's rendered in a different context than a Issue - trackDesignDetailView( - 'issue-design-collection', - 'issue', - this.$route.query.version || this.latestVersionId, - this.isLatestVersion, - ); - }, - updateActiveDiscussion(id) { - this.$apollo.mutate({ - mutation: updateActiveDiscussionMutation, - variables: { - id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion, - }, - }); - }, - toggleResolvedComments() { - this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded; - }, - }, - createImageDiffNoteMutation, - DESIGNS_ROUTE_NAME, -}; -</script> - -<template> - <div - class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row" - > - <gl-loading-icon v-if="isFirstLoading" size="xl" class="align-self-center" /> - <template v-else> - <div class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"> - <design-destroyer - :filenames="[design.filename]" - :project-path="projectPath" - :iid="issueIid" - @done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })" - @error="onDesignDeleteError" - > - <template #default="{ mutate, loading }"> - <toolbar - :id="id" - :is-deleting="loading" - :is-latest-version="isLatestVersion" - v-bind="design" - @delete="mutate" - /> - </template> - </design-destroyer> - - <div v-if="errorMessage" class="p-3"> - <gl-alert variant="danger" @dismiss="errorMessage = null"> - {{ errorMessage }} - </gl-alert> - </div> - <design-presentation - :image="design.image" - :image-name="design.filename" - :discussions="discussions" - :is-annotating="isAnnotating" - :scale="scale" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - @openCommentForm="openCommentForm" - @closeCommentForm="closeCommentForm" - @moveNote="onMoveNote" - /> - - <div class="design-scaler-wrapper position-absolute mb-4 d-flex-center"> - <design-scaler @scale="scale = $event" /> - </div> - </div> - <design-sidebar - :design="design" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - :markdown-preview-path="markdownPreviewPath" - @onDesignDiscussionError="onDesignDiscussionError" - @onCreateImageDiffNoteError="onCreateImageDiffNoteError" - @updateNoteError="onUpdateNoteError" - @resolveDiscussionError="onResolveDiscussionError" - @toggleResolvedComments="toggleResolvedComments" - > - <template #replyForm> - <apollo-mutation - v-if="isAnnotating" - #default="{ mutate, loading }" - :mutation="$options.createImageDiffNoteMutation" - :variables="{ - input: mutationPayload, - }" - :update="addImageDiffNoteToStore" - @done="closeCommentForm" - @error="onCreateImageDiffNoteError" - > - <design-reply-form - ref="newDiscussionForm" - v-model="comment" - :is-saving="loading" - :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="closeCommentForm" - /> </apollo-mutation - ></template> - </design-sidebar> - </template> - </div> -</template> diff --git a/app/assets/javascripts/design_management_new/pages/index.vue b/app/assets/javascripts/design_management_new/pages/index.vue deleted file mode 100644 index 700fa903a9c..00000000000 --- a/app/assets/javascripts/design_management_new/pages/index.vue +++ /dev/null @@ -1,346 +0,0 @@ -<script> -import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui'; -import createFlash from '~/flash'; -import { s__, sprintf } from '~/locale'; -import UploadButton from '../components/upload/button.vue'; -import DeleteButton from '../components/delete_button.vue'; -import Design from '../components/list/item.vue'; -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 permissionsQuery from '../graphql/queries/design_permissions.query.graphql'; -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import allDesignsMixin from '../mixins/all_designs'; -import { - UPLOAD_DESIGN_ERROR, - EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, - EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE, - designUploadSkippedWarning, - designDeletionError, -} from '../utils/error_messages'; -import { updateStoreAfterUploadDesign } from '../utils/cache_update'; -import { - designUploadOptimisticResponse, - isValidDesignFile, -} from '../utils/design_management_utils'; -import { getFilename } from '~/lib/utils/file_upload'; -import { DESIGNS_ROUTE_NAME } from '../router/constants'; - -const MAXIMUM_FILE_UPLOAD_LIMIT = 10; - -export default { - components: { - GlLoadingIcon, - GlAlert, - GlButton, - UploadButton, - Design, - DesignDestroyer, - DesignVersionDropdown, - DeleteButton, - DesignDropzone, - }, - mixins: [allDesignsMixin], - apollo: { - permissions: { - query: permissionsQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - }; - }, - update: data => data.project.issue.userPermissions, - }, - }, - data() { - return { - permissions: { - createDesign: false, - }, - filesToBeSaved: [], - selectedDesigns: [], - }; - }, - computed: { - isLoading() { - return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading; - }, - isSaving() { - return this.filesToBeSaved.length > 0; - }, - canCreateDesign() { - return this.permissions.createDesign; - }, - showToolbar() { - return this.canCreateDesign && this.allVersions.length > 0; - }, - hasDesigns() { - return this.designs.length > 0; - }, - hasSelectedDesigns() { - return this.selectedDesigns.length > 0; - }, - canDeleteDesigns() { - return this.isLatestVersion && this.hasSelectedDesigns; - }, - projectQueryBody() { - return { - query: getDesignListQuery, - variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null }, - }; - }, - selectAllButtonText() { - return this.hasSelectedDesigns - ? 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); - }, - methods: { - resetFilesToBeSaved() { - this.filesToBeSaved = []; - }, - /** - * Determine if a design upload is valid, given [files] - * @param {Array<File>} files - */ - isValidDesignUpload(files) { - if (!this.canCreateDesign) return false; - - if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) { - createFlash( - sprintf( - s__( - 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.', - ), - { - upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT, - }, - ), - ); - - return false; - } - return true; - }, - onUploadDesign(files) { - // convert to Array so that we have Array methods (.map, .some, etc.) - this.filesToBeSaved = Array.from(files); - if (!this.isValidDesignUpload(this.filesToBeSaved)) return null; - - const mutationPayload = { - optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved), - variables: { - files: this.filesToBeSaved, - projectPath: this.projectPath, - iid: this.issueIid, - }, - context: { - hasUpload: true, - }, - mutation: uploadDesignMutation, - update: this.afterUploadDesign, - }; - - return this.$apollo - .mutate(mutationPayload) - .then(res => this.onUploadDesignDone(res)) - .catch(() => this.onUploadDesignError()); - }, - afterUploadDesign( - store, - { - data: { designManagementUpload }, - }, - ) { - updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody); - }, - onUploadDesignDone(res) { - const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || []; - const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles); - if (skippedWarningMessage) { - createFlash(skippedWarningMessage, 'warning'); - } - - // if this upload resulted in a new version being created, redirect user to the latest version - if (!this.isLatestVersion) { - this.$router.push({ name: DESIGNS_ROUTE_NAME }); - } - this.resetFilesToBeSaved(); - }, - onUploadDesignError() { - this.resetFilesToBeSaved(); - createFlash(UPLOAD_DESIGN_ERROR); - }, - changeSelectedDesigns(filename) { - if (this.isDesignSelected(filename)) { - this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename); - } else { - this.selectedDesigns.push(filename); - } - }, - toggleDesignsSelection() { - if (this.hasSelectedDesigns) { - this.selectedDesigns = []; - } else { - this.selectedDesigns = this.designs.map(design => design.filename); - } - }, - isDesignSelected(filename) { - return this.selectedDesigns.includes(filename); - }, - isDesignToBeSaved(filename) { - return this.filesToBeSaved.some(file => file.name === filename); - }, - canSelectDesign(filename) { - return this.isLatestVersion && this.canCreateDesign && !this.isDesignToBeSaved(filename); - }, - onDesignDelete() { - this.selectedDesigns = []; - if (this.$route.query.version) this.$router.push({ name: DESIGNS_ROUTE_NAME }); - }, - onDesignDeleteError() { - const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 }); - createFlash(errorMessage); - }, - onExistingDesignDropzoneChange(files, existingDesignFilename) { - const filesArr = Array.from(files); - - if (filesArr.length > 1) { - createFlash(EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE); - return; - } - - if (!filesArr.some(({ name }) => existingDesignFilename === name)) { - createFlash(EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE); - return; - } - - this.onUploadDesign(files); - }, - onDesignPaste(event) { - const { clipboardData } = event; - const files = Array.from(clipboardData.files); - if (clipboardData && files.length > 0) { - if (!files.some(isValidDesignFile)) { - return; - } - event.preventDefault(); - let filename = getFilename(event); - if (!filename || filename === 'image.png') { - filename = `design_${Date.now()}.png`; - } - const newFile = new File([files[0]], filename); - this.onUploadDesign([newFile]); - } - }, - toggleOnPasteListener() { - document.addEventListener('paste', this.onDesignPaste); - }, - toggleOffPasteListener() { - document.removeEventListener('paste', this.onDesignPaste); - }, - }, - beforeRouteUpdate(to, from, next) { - this.selectedDesigns = []; - next(); - }, -}; -</script> - -<template> - <div - data-testid="designs-root" - class="gl-mt-5" - :class="{ 'designs-root': !isDesignListEmpty }" - @mouseenter="toggleOnPasteListener" - @mouseleave="toggleOffPasteListener" - > - <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex"> - <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-button - v-if="isLatestVersion" - variant="link" - size="small" - class="gl-mr-2 js-select-all" - @click="toggleDesignsSelection" - >{{ selectAllButtonText }} - </gl-button> - <design-destroyer - #default="{ mutate, loading }" - :filenames="selectedDesigns" - @done="onDesignDelete" - @error="onDesignDeleteError" - > - <delete-button - v-if="isLatestVersion" - :is-deleting="loading" - button-variant="danger" - button-class="gl-mr-4" - button-size="small" - :has-selected-designs="hasSelectedDesigns" - @deleteSelectedDesigns="mutate()" - > - {{ s__('DesignManagement|Delete selected') }} - <gl-loading-icon v-if="loading" inline class="ml-1" /> - </delete-button> - </design-destroyer> - <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" /> - </div> - </div> - </header> - <div class="mt-4"> - <gl-loading-icon v-if="isLoading" size="md" /> - <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"> - <span - v-if="isDesignListEmpty && !allVersions.length" - class="gl-font-weight-bold gl-font-weight-bold gl-ml-5 gl-mb-4" - >{{ s__('DesignManagement|Designs') }}</span - > - <li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper"> - <design-dropzone - :class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }" - :has-designs="hasDesigns" - @change="onUploadDesign" - /> - </li> - <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-3 gl-mb-3"> - <design-dropzone - :has-designs="hasDesigns" - @change="onExistingDesignDropzoneChange($event, design.filename)" - ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)" - /></design-dropzone> - - <input - v-if="canSelectDesign(design.filename)" - :checked="isDesignSelected(design.filename)" - type="checkbox" - class="design-checkbox" - @change="changeSelectedDesigns(design.filename)" - /> - </li> - </ol> - </div> - <router-view :key="$route.fullPath" /> - </div> -</template> |