debian-mirror-gitlab/app/assets/javascripts/design_management/pages/index.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

513 lines
17 KiB
Vue
Raw Normal View History

2020-05-24 23:13:21 +05:30
<script>
2021-01-29 00:20:46 +05:30
import { GlLoadingIcon, GlButton, GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
2022-07-23 23:45:48 +05:30
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
2020-10-24 23:57:45 +05:30
import VueDraggable from 'vuedraggable';
2021-01-29 00:20:46 +05:30
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
2021-03-11 19:13:27 +05:30
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
2021-12-11 22:18:48 +05:30
import { getFilename, validateImageName } from '~/lib/utils/file_upload';
2022-08-13 15:12:31 +05:30
import { __, s__ } from '~/locale';
2021-03-11 19:13:27 +05:30
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
2020-05-24 23:13:21 +05:30
import DeleteButton from '../components/delete_button.vue';
import DesignDestroyer from '../components/design_destroyer.vue';
2021-03-11 19:13:27 +05:30
import Design from '../components/list/item.vue';
import UploadButton from '../components/upload/button.vue';
2020-05-24 23:13:21 +05:30
import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
2022-08-13 15:12:31 +05:30
import { MAXIMUM_FILE_UPLOAD_LIMIT, VALID_DESIGN_FILE_MIMETYPE } from '../constants';
2020-10-24 23:57:45 +05:30
import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql';
2021-03-11 19:13:27 +05:30
import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
2020-05-24 23:13:21 +05:30
import allDesignsMixin from '../mixins/all_designs';
2021-03-11 19:13:27 +05:30
import { DESIGNS_ROUTE_NAME } from '../router/constants';
2020-10-24 23:57:45 +05:30
import {
updateStoreAfterUploadDesign,
updateDesignsOnStoreAfterReorder,
} from '../utils/cache_update';
2020-05-24 23:13:21 +05:30
import {
designUploadOptimisticResponse,
isValidDesignFile,
2020-10-24 23:57:45 +05:30
moveDesignOptimisticResponse,
2020-05-24 23:13:21 +05:30
} from '../utils/design_management_utils';
2021-03-11 19:13:27 +05:30
import {
UPLOAD_DESIGN_ERROR,
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
MOVE_DESIGN_ERROR,
UPLOAD_DESIGN_INVALID_FILETYPE_ERROR,
designUploadSkippedWarning,
designDeletionError,
2022-08-13 15:12:31 +05:30
MAXIMUM_FILE_UPLOAD_LIMIT_REACHED,
2021-03-11 19:13:27 +05:30
} from '../utils/error_messages';
2021-01-03 14:25:43 +05:30
import { trackDesignCreate, trackDesignUpdate } from '../utils/tracking';
2020-05-24 23:13:21 +05:30
export default {
components: {
GlLoadingIcon,
GlAlert,
2020-10-24 23:57:45 +05:30
GlButton,
2021-01-29 00:20:46 +05:30
GlSprintf,
GlLink,
2020-05-24 23:13:21 +05:30
UploadButton,
Design,
DesignDestroyer,
DesignVersionDropdown,
DeleteButton,
DesignDropzone,
2020-10-24 23:57:45 +05:30
VueDraggable,
2020-05-24 23:13:21 +05:30
},
2021-01-29 00:20:46 +05:30
dropzoneProps: {
dropToStartMessage: __('Drop your designs to start your upload.'),
isFileValid: isValidDesignFile,
validFileMimetypes: [VALID_DESIGN_FILE_MIMETYPE.mimetype],
},
2020-05-24 23:13:21 +05:30
mixins: [allDesignsMixin],
apollo: {
permissions: {
query: permissionsQuery,
variables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
};
},
2021-03-08 18:12:59 +05:30
update: (data) => data.project.issue.userPermissions,
2020-05-24 23:13:21 +05:30
},
},
2021-03-08 18:12:59 +05:30
beforeRouteUpdate(to, from, next) {
this.selectedDesigns = [];
next();
},
2020-05-24 23:13:21 +05:30
data() {
return {
permissions: {
createDesign: false,
},
filesToBeSaved: [],
selectedDesigns: [],
2020-10-24 23:57:45 +05:30
isDraggingDesign: false,
reorderedDesigns: null,
2021-01-03 14:25:43 +05:30
isReorderingInProgress: false,
2022-08-13 15:12:31 +05:30
uploadError: null,
2020-05-24 23:13:21 +05:30
};
},
computed: {
isLoading() {
2021-01-03 14:25:43 +05:30
return (
this.$apollo.queries.designCollection.loading || this.$apollo.queries.permissions.loading
);
2020-05-24 23:13:21 +05:30
},
isSaving() {
return this.filesToBeSaved.length > 0;
},
2022-07-23 23:45:48 +05:30
isMobile() {
return GlBreakpointInstance.getBreakpointSize() === 'xs';
},
2020-05-24 23:13:21 +05:30
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');
},
2020-10-24 23:57:45 +05:30
isDesignListEmpty() {
return !this.isSaving && !this.hasDesigns;
},
2021-01-03 14:25:43 +05:30
isDesignCollectionCopying() {
return this.designCollection && this.designCollection.copyState === 'IN_PROGRESS';
},
2020-10-24 23:57:45 +05:30
designDropzoneWrapperClass() {
return this.isDesignListEmpty
? 'col-12'
: 'gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3';
},
2020-05-24 23:13:21 +05:30
},
mounted() {
2020-10-24 23:57:45 +05:30
if (this.$route.path === '/designs') {
this.$el.scrollIntoView();
}
2020-05-24 23:13:21 +05:30
},
2021-11-11 11:23:49 +05:30
beforeDestroy() {
document.removeEventListener('paste', this.onDesignPaste);
},
2020-05-24 23:13:21 +05:30
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) {
2022-08-13 15:12:31 +05:30
this.uploadError = MAXIMUM_FILE_UPLOAD_LIMIT_REACHED;
2020-05-24 23:13:21 +05:30
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)
2021-03-08 18:12:59 +05:30
.then((res) => this.onUploadDesignDone(res))
2020-05-24 23:13:21 +05:30
.catch(() => this.onUploadDesignError());
},
2021-03-08 18:12:59 +05:30
afterUploadDesign(store, { data: { designManagementUpload } }) {
2020-05-24 23:13:21 +05:30
updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody);
},
onUploadDesignDone(res) {
2021-01-03 14:25:43 +05:30
// display any warnings, if necessary
2020-05-24 23:13:21 +05:30
const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || [];
const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles);
if (skippedWarningMessage) {
2022-08-13 15:12:31 +05:30
this.uploadError = skippedWarningMessage;
2020-05-24 23:13:21 +05:30
}
// 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 });
}
2021-01-03 14:25:43 +05:30
// reset state
2020-05-24 23:13:21 +05:30
this.resetFilesToBeSaved();
2021-01-03 14:25:43 +05:30
this.trackUploadDesign(res);
},
trackUploadDesign(res) {
2021-03-08 18:12:59 +05:30
(res?.data?.designManagementUpload?.designs || []).forEach((design) => {
2021-01-03 14:25:43 +05:30
if (design.event === 'CREATION') {
trackDesignCreate();
} else if (design.event === 'MODIFICATION') {
trackDesignUpdate();
}
});
2020-05-24 23:13:21 +05:30
},
onUploadDesignError() {
this.resetFilesToBeSaved();
2022-08-13 15:12:31 +05:30
this.uploadError = UPLOAD_DESIGN_ERROR;
2020-05-24 23:13:21 +05:30
},
changeSelectedDesigns(filename) {
if (this.isDesignSelected(filename)) {
2021-03-08 18:12:59 +05:30
this.selectedDesigns = this.selectedDesigns.filter((design) => design !== filename);
2020-05-24 23:13:21 +05:30
} else {
this.selectedDesigns.push(filename);
}
},
toggleDesignsSelection() {
if (this.hasSelectedDesigns) {
this.selectedDesigns = [];
} else {
2021-03-08 18:12:59 +05:30
this.selectedDesigns = this.designs.map((design) => design.filename);
2020-05-24 23:13:21 +05:30
}
},
isDesignSelected(filename) {
return this.selectedDesigns.includes(filename);
},
isDesignToBeSaved(filename) {
2021-03-08 18:12:59 +05:30
return this.filesToBeSaved.some((file) => file.name === filename);
2020-05-24 23:13:21 +05:30
},
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() {
2021-11-18 22:05:49 +05:30
const errorMessage = designDeletionError(this.selectedDesigns.length);
2022-08-13 15:12:31 +05:30
this.uploadError = errorMessage;
2021-01-29 00:20:46 +05:30
},
onDesignDropzoneError() {
2022-08-13 15:12:31 +05:30
this.uploadError = UPLOAD_DESIGN_INVALID_FILETYPE_ERROR;
2020-05-24 23:13:21 +05:30
},
onExistingDesignDropzoneChange(files, existingDesignFilename) {
const filesArr = Array.from(files);
if (filesArr.length > 1) {
2022-08-13 15:12:31 +05:30
this.uploadError = EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE;
2020-05-24 23:13:21 +05:30
return;
}
if (!filesArr.some(({ name }) => existingDesignFilename === name)) {
2022-08-13 15:12:31 +05:30
this.uploadError = EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE;
2020-05-24 23:13:21 +05:30
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();
2021-12-11 22:18:48 +05:30
const fileList = [...files];
fileList.forEach((file) => {
let filename = getFilename(file);
filename = validateImageName(file);
if (!filename || filename === 'image.png') {
filename = `design_${Date.now()}.png`;
}
const newFile = new File([file], filename);
this.onUploadDesign([newFile]);
});
2020-05-24 23:13:21 +05:30
}
},
2020-10-24 23:57:45 +05:30
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;
2020-05-24 23:13:21 +05:30
}
2020-10-24 23:57:45 +05:30
return variables;
},
reorderDesigns({ moved: { newIndex, element } }) {
2021-01-03 14:25:43 +05:30
this.isReorderingInProgress = true;
2020-10-24 23:57:45 +05:30
this.$apollo
.mutate({
mutation: moveDesignMutation,
variables: this.designMoveVariables(newIndex, element),
2020-11-24 15:15:51 +05:30
update: (store, { data: { designManagementMove } }) =>
updateDesignsOnStoreAfterReorder(store, designManagementMove, this.projectQueryBody),
2020-10-24 23:57:45 +05:30
optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns),
})
.catch(() => {
2022-08-13 15:12:31 +05:30
this.uploadError = MOVE_DESIGN_ERROR;
2021-01-03 14:25:43 +05:30
})
.finally(() => {
this.isReorderingInProgress = false;
2020-10-24 23:57:45 +05:30
});
},
onDesignMove(designs) {
this.reorderedDesigns = designs;
2020-05-24 23:13:21 +05:30
},
2022-08-13 15:12:31 +05:30
unsetUpdateError() {
this.uploadError = null;
},
2020-05-24 23:13:21 +05:30
},
2020-10-24 23:57:45 +05:30
dragOptions: {
animation: 200,
ghostClass: 'gl-visibility-hidden',
2020-05-24 23:13:21 +05:30
},
2021-01-29 00:20:46 +05:30
i18n: {
2021-06-08 01:23:25 +05:30
dropzoneDescriptionText: __('Drag your designs here or %{linkStart}click to upload%{linkEnd}.'),
2021-01-29 00:20:46 +05:30
},
2020-05-24 23:13:21 +05:30
};
</script>
<template>
2020-10-24 23:57:45 +05:30
<div
data-testid="designs-root"
class="gl-mt-5"
@mouseenter="toggleOnPasteListener"
@mouseleave="toggleOffPasteListener"
>
2022-08-13 15:12:31 +05:30
<gl-alert
v-if="uploadError"
variant="danger"
class="gl-mb-3"
data-testid="design-update-alert"
@dismiss="unsetUpdateError"
>
{{ uploadError }}
</gl-alert>
2021-01-29 00:20:46 +05:30
<header
v-if="showToolbar"
2022-07-23 23:45:48 +05:30
class="gl-display-flex gl-my-0 gl-text-gray-900"
2021-01-29 00:20:46 +05:30
data-testid="design-toolbar-wrapper"
>
2021-03-11 19:13:27 +05:30
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full gl-flex-wrap"
>
<div class="gl-display-flex gl-align-items-center gl-my-2">
2020-10-24 23:57:45 +05:30
<span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span>
<design-version-dropdown />
</div>
2021-03-11 19:13:27 +05:30
<div
v-show="hasDesigns"
2022-08-27 11:52:29 +05:30
class="gl-display-flex gl-align-items-center gl-my-2"
2022-08-13 15:12:31 +05:30
data-testid="design-selector-toolbar"
2021-03-11 19:13:27 +05:30
>
2020-10-24 23:57:45 +05:30
<gl-button
2020-05-24 23:13:21 +05:30
v-if="isLatestVersion"
2022-07-23 23:45:48 +05:30
category="tertiary"
2020-10-24 23:57:45 +05:30
size="small"
2021-04-17 20:07:23 +05:30
class="gl-mr-3"
data-testid="select-all-designs-button"
2020-05-24 23:13:21 +05:30
@click="toggleDesignsSelection"
2020-10-24 23:57:45 +05:30
>{{ selectAllButtonText }}
</gl-button>
2020-05-24 23:13:21 +05:30
<design-destroyer
#default="{ mutate, loading }"
:filenames="selectedDesigns"
@done="onDesignDelete"
@error="onDesignDeleteError"
>
<delete-button
v-if="isLatestVersion"
:is-deleting="loading"
2021-04-29 21:17:54 +05:30
button-variant="default"
2020-10-24 23:57:45 +05:30
button-class="gl-mr-3"
button-size="small"
2021-01-03 14:25:43 +05:30
data-qa-selector="archive_button"
2020-10-24 23:57:45 +05:30
:loading="loading"
2020-05-24 23:13:21 +05:30
:has-selected-designs="hasSelectedDesigns"
2021-04-17 20:07:23 +05:30
@delete-selected-designs="mutate()"
2020-05-24 23:13:21 +05:30
>
2020-10-24 23:57:45 +05:30
{{ s__('DesignManagement|Archive selected') }}
2020-05-24 23:13:21 +05:30
</delete-button>
</design-destroyer>
2021-01-29 00:20:46 +05:30
<upload-button
v-if="canCreateDesign"
:is-saving="isSaving"
data-testid="design-upload-button"
@upload="onUploadDesign"
/>
2020-05-24 23:13:21 +05:30
</div>
</div>
</header>
2021-01-03 14:25:43 +05:30
<div class="gl-mt-6">
2022-07-23 23:45:48 +05:30
<gl-loading-icon v-if="isLoading" size="lg" />
2020-05-24 23:13:21 +05:30
<gl-alert v-else-if="error" variant="danger" :dismissible="false">
{{ __('An error occurred while loading designs. Please try again.') }}
</gl-alert>
2021-01-03 14:25:43 +05:30
<header
v-else-if="isDesignCollectionCopying"
class="card"
data-testid="design-collection-is-copying"
>
<div class="card-header design-card-header gl-border-b-0">
<div class="card-title gl-display-flex gl-align-items-center gl-my-0 gl-h-7">
{{
s__(
'DesignManagement|Your designs are being copied and are on their way… Please refresh to update.',
)
}}
</div>
</div>
</header>
2020-10-24 23:57:45 +05:30
<vue-draggable
v-else
:value="designs"
2022-07-23 23:45:48 +05:30
:disabled="!isLatestVersion || isReorderingInProgress || isMobile"
2020-10-24 23:57:45 +05:30
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
2021-01-29 00:20:46 +05:30
:display-as-card="hasDesigns"
:enable-drag-behavior="isDraggingDesign"
v-bind="$options.dropzoneProps"
2020-10-24 23:57:45 +05:30
@change="onExistingDesignDropzoneChange($event, design.filename)"
2021-01-29 00:20:46 +05:30
@error="onDesignDropzoneError"
2020-10-24 23:57:45 +05:30
>
<design
v-bind="design"
:is-uploading="isDesignToBeSaved(design.filename)"
class="gl-bg-white"
/>
2021-01-29 00:20:46 +05:30
<template #upload-text="{ openFileUpload }">
<gl-sprintf :message="$options.i18n.dropzoneDescriptionText">
<template #link="{ content }">
<gl-link @click.stop="openFileUpload">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</template>
2020-10-24 23:57:45 +05:30
</design-dropzone>
2020-05-24 23:13:21 +05:30
<input
v-if="canSelectDesign(design.filename)"
:checked="isDesignSelected(design.filename)"
type="checkbox"
class="design-checkbox"
2021-01-03 14:25:43 +05:30
data-qa-selector="design_checkbox"
:data-qa-design="design.filename"
2020-05-24 23:13:21 +05:30
@change="changeSelectedDesigns(design.filename)"
/>
</li>
2020-10-24 23:57:45 +05:30
<template #header>
<li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
<design-dropzone
2021-01-29 00:20:46 +05:30
:enable-drag-behavior="isDraggingDesign"
2020-10-24 23:57:45 +05:30
:class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
2021-01-29 00:20:46 +05:30
:display-as-card="hasDesigns"
v-bind="$options.dropzoneProps"
2021-01-03 14:25:43 +05:30
data-qa-selector="design_dropzone_content"
2020-10-24 23:57:45 +05:30
@change="onUploadDesign"
2021-01-29 00:20:46 +05:30
@error="onDesignDropzoneError"
>
<template #upload-text="{ openFileUpload }">
<gl-sprintf :message="$options.i18n.dropzoneDescriptionText">
<template #link="{ content }">
2021-04-29 21:17:54 +05:30
<gl-link @click.stop="openFileUpload">{{ content }}</gl-link>
2021-01-29 00:20:46 +05:30
</template>
</gl-sprintf>
</template>
</design-dropzone>
2020-10-24 23:57:45 +05:30
</li>
</template>
</vue-draggable>
2020-05-24 23:13:21 +05:30
</div>
2020-06-23 00:09:42 +05:30
<router-view :key="$route.fullPath" />
2020-05-24 23:13:21 +05:30
</div>
</template>