Commit f00500d1 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'tr-upload-component' into 'master'

Move design upload component to vue_shared

See merge request gitlab-org/gitlab!46744
parents 95c5e1cf 91322d28
...@@ -5,9 +5,6 @@ export const VALID_DESIGN_FILE_MIMETYPE = { ...@@ -5,9 +5,6 @@ export const VALID_DESIGN_FILE_MIMETYPE = {
regex: /image\/.+/, regex: /image\/.+/,
}; };
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
export const VALID_DATA_TRANSFER_TYPE = 'Files';
export const ACTIVE_DISCUSSION_SOURCE_TYPES = { export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
pin: 'pin', pin: 'pin',
discussion: 'discussion', discussion: 'discussion',
......
<script> <script>
import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui'; import { GlLoadingIcon, GlButton, GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql'; import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql'; import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash'; import createFlash, { FLASH_TYPES } from '~/flash';
import { s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload'; import { getFilename } from '~/lib/utils/file_upload';
import UploadButton from '../components/upload/button.vue'; import UploadButton from '../components/upload/button.vue';
import DeleteButton from '../components/delete_button.vue'; import DeleteButton from '../components/delete_button.vue';
import Design from '../components/list/item.vue'; import Design from '../components/list/item.vue';
import DesignDestroyer from '../components/design_destroyer.vue'; import DesignDestroyer from '../components/design_destroyer.vue';
import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue'; import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
import DesignDropzone from '../components/upload/design_dropzone.vue'; import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql'; import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql'; import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql';
import allDesignsMixin from '../mixins/all_designs'; import allDesignsMixin from '../mixins/all_designs';
...@@ -20,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ import {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE, EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
MOVE_DESIGN_ERROR, MOVE_DESIGN_ERROR,
UPLOAD_DESIGN_INVALID_FILETYPE_ERROR,
designUploadSkippedWarning, designUploadSkippedWarning,
designDeletionError, designDeletionError,
} from '../utils/error_messages'; } from '../utils/error_messages';
...@@ -34,6 +35,7 @@ import { ...@@ -34,6 +35,7 @@ import {
} from '../utils/design_management_utils'; } from '../utils/design_management_utils';
import { trackDesignCreate, trackDesignUpdate } from '../utils/tracking'; import { trackDesignCreate, trackDesignUpdate } from '../utils/tracking';
import { DESIGNS_ROUTE_NAME } from '../router/constants'; import { DESIGNS_ROUTE_NAME } from '../router/constants';
import { VALID_DESIGN_FILE_MIMETYPE } from '../constants';
const MAXIMUM_FILE_UPLOAD_LIMIT = 10; const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
...@@ -42,6 +44,8 @@ export default { ...@@ -42,6 +44,8 @@ export default {
GlLoadingIcon, GlLoadingIcon,
GlAlert, GlAlert,
GlButton, GlButton,
GlSprintf,
GlLink,
UploadButton, UploadButton,
Design, Design,
DesignDestroyer, DesignDestroyer,
...@@ -50,6 +54,11 @@ export default { ...@@ -50,6 +54,11 @@ export default {
DesignDropzone, DesignDropzone,
VueDraggable, VueDraggable,
}, },
dropzoneProps: {
dropToStartMessage: __('Drop your designs to start your upload.'),
isFileValid: isValidDesignFile,
validFileMimetypes: [VALID_DESIGN_FILE_MIMETYPE.mimetype],
},
mixins: [allDesignsMixin], mixins: [allDesignsMixin],
apollo: { apollo: {
permissions: { permissions: {
...@@ -247,6 +256,9 @@ export default { ...@@ -247,6 +256,9 @@ export default {
const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 }); const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 });
createFlash({ message: errorMessage }); createFlash({ message: errorMessage });
}, },
onDesignDropzoneError() {
createFlash({ message: UPLOAD_DESIGN_INVALID_FILETYPE_ERROR });
},
onExistingDesignDropzoneChange(files, existingDesignFilename) { onExistingDesignDropzoneChange(files, existingDesignFilename) {
const filesArr = Array.from(files); const filesArr = Array.from(files);
...@@ -325,6 +337,9 @@ export default { ...@@ -325,6 +337,9 @@ export default {
animation: 200, animation: 200,
ghostClass: 'gl-visibility-hidden', ghostClass: 'gl-visibility-hidden',
}, },
i18n: {
dropzoneDescriptionText: __('Drop or %{linkStart}upload%{linkEnd} designs to attach'),
},
}; };
</script> </script>
...@@ -335,7 +350,11 @@ export default { ...@@ -335,7 +350,11 @@ export default {
@mouseenter="toggleOnPasteListener" @mouseenter="toggleOnPasteListener"
@mouseleave="toggleOffPasteListener" @mouseleave="toggleOffPasteListener"
> >
<header v-if="showToolbar" class="row-content-block gl-border-t-0 gl-p-3 gl-display-flex"> <header
v-if="showToolbar"
class="row-content-block gl-border-t-0 gl-p-3 gl-display-flex"
data-testid="design-toolbar-wrapper"
>
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"> <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full">
<div> <div>
<span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span> <span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span>
...@@ -371,7 +390,12 @@ export default { ...@@ -371,7 +390,12 @@ export default {
{{ s__('DesignManagement|Archive selected') }} {{ s__('DesignManagement|Archive selected') }}
</delete-button> </delete-button>
</design-destroyer> </design-destroyer>
<upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" /> <upload-button
v-if="canCreateDesign"
:is-saving="isSaving"
data-testid="design-upload-button"
@upload="onUploadDesign"
/>
</div> </div>
</div> </div>
</header> </header>
...@@ -414,15 +438,26 @@ export default { ...@@ -414,15 +438,26 @@ export default {
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile" class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
> >
<design-dropzone <design-dropzone
:has-designs="hasDesigns" :display-as-card="hasDesigns"
:is-dragging-design="isDraggingDesign" :enable-drag-behavior="isDraggingDesign"
v-bind="$options.dropzoneProps"
@change="onExistingDesignDropzoneChange($event, design.filename)" @change="onExistingDesignDropzoneChange($event, design.filename)"
@error="onDesignDropzoneError"
> >
<design <design
v-bind="design" v-bind="design"
:is-uploading="isDesignToBeSaved(design.filename)" :is-uploading="isDesignToBeSaved(design.filename)"
class="gl-bg-white" class="gl-bg-white"
/> />
<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>
</design-dropzone> </design-dropzone>
<input <input
...@@ -438,12 +473,24 @@ export default { ...@@ -438,12 +473,24 @@ export default {
<template #header> <template #header>
<li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper"> <li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
<design-dropzone <design-dropzone
:is-dragging-design="isDraggingDesign" :enable-drag-behavior="isDraggingDesign"
:class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }" :class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
:has-designs="hasDesigns" :display-as-card="hasDesigns"
v-bind="$options.dropzoneProps"
data-qa-selector="design_dropzone_content" data-qa-selector="design_dropzone_content"
@change="onUploadDesign" @change="onUploadDesign"
/> @error="onDesignDropzoneError"
>
<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>
</design-dropzone>
</li> </li>
</template> </template>
</vue-draggable> </vue-draggable>
......
// We may wish to make this more restrictive, as per
// https://gitlab.com/gitlab-org/gitlab/issues/118611
export const VALID_IMAGE_FILE_MIMETYPE = {
mimetype: 'image/*',
regex: /image\/.+/,
};
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
export const VALID_DATA_TRANSFER_TYPE = 'Files';
<script> <script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash'; import { __ } from '~/locale';
import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql'; import { isValidImage } from './utils';
import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages'; import { VALID_DATA_TRANSFER_TYPE, VALID_IMAGE_FILE_MIMETYPE } from './constants';
import { isValidDesignFile } from '../../utils/design_management_utils';
import { VALID_DATA_TRANSFER_TYPE, VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
export default { export default {
components: { components: {
...@@ -13,15 +11,31 @@ export default { ...@@ -13,15 +11,31 @@ export default {
GlSprintf, GlSprintf,
}, },
props: { props: {
hasDesigns: { displayAsCard: {
type: Boolean, type: Boolean,
required: true, required: false,
default: false,
}, },
isDraggingDesign: { enableDragBehavior: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
dropToStartMessage: {
type: String,
required: false,
default: __('Drop your files to start your upload.'),
},
isFileValid: {
type: Function,
required: false,
default: isValidImage,
},
validFileMimetypes: {
type: Array,
required: false,
default: () => [VALID_IMAGE_FILE_MIMETYPE.mimetype],
},
}, },
data() { data() {
return { return {
...@@ -35,14 +49,17 @@ export default { ...@@ -35,14 +49,17 @@ export default {
}, },
iconStyles() { iconStyles() {
return { return {
size: this.hasDesigns ? 24 : 16, size: this.displayAsCard ? 24 : 16,
class: this.hasDesigns ? 'gl-mb-2' : 'gl-mr-3 gl-text-gray-500', class: this.displayAsCard ? 'gl-mb-2' : 'gl-mr-3 gl-text-gray-500',
}; };
}, },
validMimeTypeString() {
return this.validFileMimetypes.join();
},
}, },
methods: { methods: {
isValidUpload(files) { isValidUpload(files) {
return files.every(isValidDesignFile); return files.every(this.isFileValid);
}, },
isValidDragDataType({ dataTransfer }) { isValidDragDataType({ dataTransfer }) {
return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE)); return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE));
...@@ -56,7 +73,7 @@ export default { ...@@ -56,7 +73,7 @@ export default {
const { files } = dataTransfer; const { files } = dataTransfer;
if (!this.isValidUpload(Array.from(files))) { if (!this.isValidUpload(Array.from(files))) {
createFlash({ message: UPLOAD_DESIGN_INVALID_FILETYPE_ERROR }); this.$emit('error');
return; return;
} }
...@@ -72,12 +89,10 @@ export default { ...@@ -72,12 +89,10 @@ export default {
openFileUpload() { openFileUpload() {
this.$refs.fileUpload.click(); this.$refs.fileUpload.click();
}, },
onDesignInputChange(e) { onFileInputChange(e) {
this.$emit('change', e.target.files); this.$emit('change', e.target.files);
}, },
}, },
uploadDesignMutation,
VALID_DESIGN_FILE_MIMETYPE,
}; };
</script> </script>
...@@ -93,23 +108,25 @@ export default { ...@@ -93,23 +108,25 @@ export default {
> >
<slot> <slot>
<button <button
class="card design-dropzone-card design-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3" class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-3"
@click="openFileUpload" @click="openFileUpload"
> >
<div <div
:class="{ 'gl-flex-direction-column': hasDesigns }" :class="{ 'gl-flex-direction-column': displayAsCard }"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center" class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center"
data-testid="dropzone-area" data-testid="dropzone-area"
> >
<gl-icon name="upload" :size="iconStyles.size" :class="iconStyles.class" /> <gl-icon name="upload" :size="iconStyles.size" :class="iconStyles.class" />
<p class="gl-mb-0"> <p class="gl-mb-0">
<gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} designs to attach')"> <slot name="upload-text" :openFileUpload="openFileUpload">
<template #link="{ content }"> <gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} files to attach')">
<gl-link @click.stop="openFileUpload"> <template #link="{ content }">
{{ content }} <gl-link @click.stop="openFileUpload">
</gl-link> {{ content }}
</template> </gl-link>
</gl-sprintf> </template>
</gl-sprintf>
</slot>
</p> </p>
</div> </div>
</button> </button>
...@@ -117,29 +134,37 @@ export default { ...@@ -117,29 +134,37 @@ export default {
<input <input
ref="fileUpload" ref="fileUpload"
type="file" type="file"
name="design_file" name="upload_file"
:accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype" :accept="validFileMimetypes"
class="hide" class="hide"
multiple multiple
@change="onDesignInputChange" @change="onFileInputChange"
/> />
</slot> </slot>
<transition name="design-dropzone-fade"> <transition name="upload-dropzone-fade">
<div <div
v-show="dragging && !isDraggingDesign" v-show="dragging && !enableDragBehavior"
class="card design-dropzone-border design-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white" class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-3 gl-bg-white"
> >
<div v-show="!isDragDataValid" class="mw-50 gl-text-center"> <div v-show="!isDragDataValid" class="mw-50 gl-text-center">
<h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Oh no!') }}</h3> <slot name="invalidDragDataSlot">
<span>{{ <h3 :class="{ 'gl-font-base gl-display-inline': !displayAsCard }">
__( {{ __('Oh no!') }}
'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.', </h3>
) <span>{{
}}</span> __(
'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
)
}}</span>
</slot>
</div> </div>
<div v-show="isDragDataValid" class="mw-50 gl-text-center"> <div v-show="isDragDataValid" class="mw-50 gl-text-center">
<h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Incoming!') }}</h3> <slot name="validDragDataSlot">
<span>{{ __('Drop your designs to start your upload.') }}</span> <h3 :class="{ 'gl-font-base gl-display-inline': !displayAsCard }">
{{ __('Incoming!') }}
</h3>
<span>{{ dropToStartMessage }}</span>
</slot>
</div> </div>
</div> </div>
</transition> </transition>
......
import { VALID_IMAGE_FILE_MIMETYPE } from './constants';
export const isValidImage = ({ type }) =>
(type.match(VALID_IMAGE_FILE_MIMETYPE.regex) || []).length > 0;
...@@ -181,41 +181,3 @@ $t-gray-a-16-design-pin: rgba($black, 0.16); ...@@ -181,41 +181,3 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
.design-card-header { .design-card-header {
background: transparent; background: transparent;
} }
.design-dropzone-border {
border: 2px dashed $gray-100;
}
.design-dropzone-card {
transition: border $gl-transition-duration-medium $general-hover-transition-curve;
color: $gl-text-color;
&:focus,
&:active {
outline: none;
border: 2px dashed $purple;
color: $gl-text-color;
}
&:hover {
border-color: $gray-300;
}
}
.design-dropzone-overlay {
border: 2px dashed $purple;
top: 0;
left: 0;
pointer-events: none;
opacity: 1;
}
.design-dropzone-fade-enter-active,
.design-dropzone-fade-leave-active {
transition: opacity $general-hover-transition-duration $general-hover-transition-curve;
}
.design-dropzone-fade-enter,
.design-dropzone-fade-leave-to {
opacity: 0;
}
.upload-dropzone-border {
border: 2px dashed $gray-100;
}
.upload-dropzone-card {
transition: border $gl-transition-duration-medium $general-hover-transition-curve;
color: $gl-text-color;
&:focus,
&:active {
outline: none;
border: 2px dashed $purple;
color: $gl-text-color;
}
&:hover {
border-color: $gray-300;
}
}
.upload-dropzone-overlay {
border: 2px dashed $purple;
top: 0;
left: 0;
pointer-events: none;
opacity: 1;
}
.upload-dropzone-fade-enter-active,
.upload-dropzone-fade-leave-active {
transition: opacity $general-hover-transition-duration $general-hover-transition-curve;
}
.upload-dropzone-fade-enter,
.upload-dropzone-fade-leave-to {
opacity: 0;
}
...@@ -9757,9 +9757,15 @@ msgstr "" ...@@ -9757,9 +9757,15 @@ msgstr ""
msgid "Drop or %{linkStart}upload%{linkEnd} designs to attach" msgid "Drop or %{linkStart}upload%{linkEnd} designs to attach"
msgstr "" msgstr ""
msgid "Drop or %{linkStart}upload%{linkEnd} files to attach"
msgstr ""
msgid "Drop your designs to start your upload." msgid "Drop your designs to start your upload."
msgstr "" msgstr ""
msgid "Drop your files to start your upload."
msgstr ""
msgid "Due Date" msgid "Due Date"
msgstr "" msgstr ""
......
...@@ -51,7 +51,7 @@ RSpec.describe 'User uploads new design', :js do ...@@ -51,7 +51,7 @@ RSpec.describe 'User uploads new design', :js do
end end
def upload_design(fixture, count:) def upload_design(fixture, count:)
attach_file(:design_file, fixture, match: :first, make_visible: true) attach_file(:upload_file, fixture, match: :first, make_visible: true)
wait_for('designs uploaded') do wait_for('designs uploaded') do
issue.reload.designs.count == count issue.reload.designs.count == count
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
<div
class="gl-mt-5"
data-testid="designs-root"
>
<!---->
<div
class="gl-mt-6"
>
<ol
class="list-unstyled row"
>
<li
class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3"
data-testid="design-dropzone-wrapper"
>
<design-dropzone-stub
class="design-list-item design-list-item-new"
data-qa-selector="design_dropzone_content"
hasdesigns="true"
/>
</li>
<li
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
<design-dropzone-stub
hasdesigns="true"
>
<design-stub
class="gl-bg-white"
event="NONE"
filename="design-1-name"
id="design-1"
image="design-1-image"
notescount="0"
/>
</design-dropzone-stub>
<!---->
</li>
<li
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
<design-dropzone-stub
hasdesigns="true"
>
<design-stub
class="gl-bg-white"
event="NONE"
filename="design-2-name"
id="design-2"
image="design-2-image"
notescount="1"
/>
</design-dropzone-stub>
<!---->
</li>
<li
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
<design-dropzone-stub
hasdesigns="true"
>
<design-stub
class="gl-bg-white"
event="NONE"
filename="design-3-name"
id="design-3"
image="design-3-image"
notescount="0"
/>
</design-dropzone-stub>
<!---->
</li>
</ol>
</div>
<router-view-stub
name="default"
/>
</div>
`;
exports[`Design management index page designs renders designs list and header with upload button 1`] = `
<div
class="gl-mt-5"
data-testid="designs-root"
>
<header
class="row-content-block gl-border-t-0 gl-p-3 gl-display-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"
>
Designs
</span>
<design-version-dropdown-stub />
</div>
<div
class="qa-selector-toolbar gl-display-flex gl-align-items-center"
>
<gl-button-stub
buttontextclasses=""
category="primary"
class="gl-mr-4 js-select-all"
icon=""
size="small"
variant="link"
>
Select all
</gl-button-stub>
<div>
<delete-button-stub
buttoncategory="secondary"
buttonclass="gl-mr-3"
buttonsize="small"
buttonvariant="warning"
data-qa-selector="archive_button"
>
Archive selected
</delete-button-stub>
</div>
<upload-button-stub />
</div>
</div>
</header>
<div
class="gl-mt-6"
>
<ol
class="list-unstyled row"
>
<li
class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3"
data-testid="design-dropzone-wrapper"
>
<design-dropzone-stub
class="design-list-item design-list-item-new"
data-qa-selector="design_dropzone_content"
hasdesigns="true"
/>
</li>
<li
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
<design-dropzone-stub
hasdesigns="true"
>
<design-stub
class="gl-bg-white"
event="NONE"
filename="design-1-name"
id="design-1"
image="design-1-image"
notescount="0"
/>
</design-dropzone-stub>
<input
class="design-checkbox"
data-qa-design="design-1-name"
data-qa-selector="design_checkbox"
type="checkbox"
/>
</li>
<li
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
<design-dropzone-stub
hasdesigns="true"
>
<design-stub
class="gl-bg-white"
event="NONE"
filename="design-2-name"
id="design-2"
image="design-2-image"
notescount="1"
/>
</design-dropzone-stub>
<input
class="design-checkbox"
data-qa-design="design-2-name"
data-qa-selector="design_checkbox"
type="checkbox"
/>
</li>
<li
class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
<design-dropzone-stub
hasdesigns="true"
>
<design-stub
class="gl-bg-white"
event="NONE"
filename="design-3-name"
id="design-3"
image="design-3-image"
notescount="0"
/>
</design-dropzone-stub>
<input
class="design-checkbox"
data-qa-design="design-3-name"
data-qa-selector="design_checkbox"
type="checkbox"
/>
</li>
</ol>
</div>
<router-view-stub
name="default"
/>
</div>
`;
exports[`Design management index page designs renders error 1`] = ` exports[`Design management index page designs renders error 1`] = `
<div <div
class="gl-mt-5" class="gl-mt-5"
...@@ -288,34 +53,3 @@ exports[`Design management index page designs renders loading icon 1`] = ` ...@@ -288,34 +53,3 @@ exports[`Design management index page designs renders loading icon 1`] = `
/> />
</div> </div>
`; `;
exports[`Design management index page when has no designs renders design dropzone 1`] = `
<div
class="gl-mt-5"
data-testid="designs-root"
>
<!---->
<div
class="gl-mt-6"
>
<ol
class="list-unstyled row"
>
<li
class="col-12"
data-testid="design-dropzone-wrapper"
>
<design-dropzone-stub
class=""
data-qa-selector="design_dropzone_content"
/>
</li>
</ol>
</div>
<router-view-stub
name="default"
/>
</div>
`;
...@@ -10,7 +10,7 @@ import permissionsQuery from 'shared_queries/design_management/design_permission ...@@ -10,7 +10,7 @@ import permissionsQuery from 'shared_queries/design_management/design_permission
import Index from '~/design_management/pages/index.vue'; import Index from '~/design_management/pages/index.vue';
import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql'; import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
import DesignDestroyer from '~/design_management/components/design_destroyer.vue'; import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue'; import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import DeleteButton from '~/design_management/components/delete_button.vue'; import DeleteButton from '~/design_management/components/delete_button.vue';
import Design from '~/design_management/components/list/item.vue'; import Design from '~/design_management/components/list/item.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants'; import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
...@@ -105,6 +105,8 @@ describe('Design management index page', () => { ...@@ -105,6 +105,8 @@ describe('Design management index page', () => {
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]'); const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
const findDesigns = () => wrapper.findAll(Design); const findDesigns = () => wrapper.findAll(Design);
const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs; const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs;
const findDesignUploadButton = () => wrapper.find('[data-testid="design-upload-button"]');
const findDesignToolbarWrapper = () => wrapper.find('[data-testid="design-toolbar-wrapper"]');
async function moveDesigns(localWrapper) { async function moveDesigns(localWrapper) {
await jest.runOnlyPendingTimers(); await jest.runOnlyPendingTimers();
...@@ -214,13 +216,17 @@ describe('Design management index page', () => { ...@@ -214,13 +216,17 @@ describe('Design management index page', () => {
it('renders designs list and header with upload button', () => { it('renders designs list and header with upload button', () => {
createComponent({ allVersions: [mockVersion] }); createComponent({ allVersions: [mockVersion] });
expect(wrapper.element).toMatchSnapshot(); expect(findDesignsWrapper().exists()).toBe(true);
expect(findDesigns().length).toBe(3);
expect(findDesignToolbarWrapper().exists()).toBe(true);
expect(findDesignUploadButton().exists()).toBe(true);
}); });
it('does not render toolbar when there is no permission', () => { it('does not render toolbar when there is no permission', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false }); createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
expect(wrapper.element).toMatchSnapshot(); expect(findDesignToolbarWrapper().exists()).toBe(false);
expect(findDesignUploadButton().exists()).toBe(false);
}); });
it('has correct classes applied to design dropzone', () => { it('has correct classes applied to design dropzone', () => {
...@@ -247,7 +253,7 @@ describe('Design management index page', () => { ...@@ -247,7 +253,7 @@ describe('Design management index page', () => {
it('renders design dropzone', () => it('renders design dropzone', () =>
wrapper.vm.$nextTick().then(() => { wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot(); expect(findDropzone().exists()).toBe(true);
})); }));
it('has correct classes applied to design dropzone', () => { it('has correct classes applied to design dropzone', () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue'; import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import createFlash from '~/flash';
jest.mock('~/flash'); jest.mock('~/flash');
describe('Design management dropzone component', () => { describe('Upload dropzone component', () => {
let wrapper; let wrapper;
const mockDragEvent = ({ types = ['Files'], files = [] }) => { const mockDragEvent = ({ types = ['Files'], files = [] }) => {
return { dataTransfer: { types, files } }; return { dataTransfer: { types, files } };
}; };
const findDropzoneCard = () => wrapper.find('.design-dropzone-card'); const findDropzoneCard = () => wrapper.find('.upload-dropzone-card');
const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]'); const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
const findIcon = () => wrapper.find(GlIcon); const findIcon = () => wrapper.find(GlIcon);
function createComponent({ slots = {}, data = {}, props = {} } = {}) { function createComponent({ slots = {}, data = {}, props = {} } = {}) {
wrapper = shallowMount(DesignDropzone, { wrapper = shallowMount(UploadDropzone, {
slots, slots,
propsData: { propsData: {
hasDesigns: true, displayAsCard: true,
...props, ...props,
}, },
data() { data() {
...@@ -126,28 +125,50 @@ describe('Design management dropzone component', () => { ...@@ -126,28 +125,50 @@ describe('Design management dropzone component', () => {
expect(wrapper.emitted().change[0]).toEqual([[mockFile]]); expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
}); });
it('calls createFlash when files are invalid', () => { it('emits error event when files are invalid', () => {
createComponent({ data: mockData }); createComponent({ data: mockData });
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
wrapper.vm.ondrop(mockEvent);
expect(wrapper.emitted()).toHaveProperty('error');
});
it('allows validation function to be overwritten', () => {
createComponent({ data: mockData, props: { isFileValid: () => true } });
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] }); const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
wrapper.vm.ondrop(mockEvent); wrapper.vm.ondrop(mockEvent);
expect(createFlash).toHaveBeenCalledTimes(1); expect(wrapper.emitted()).not.toHaveProperty('error');
}); });
}); });
}); });
it('applies correct classes when there are no designs or no design saving loader', () => { it('applies correct classes when displaying as a standalone item', () => {
createComponent({ props: { hasDesigns: false } }); createComponent({ props: { displayAsCard: false } });
expect(findDropzoneArea().classes()).not.toContain('gl-flex-direction-column'); expect(findDropzoneArea().classes()).not.toContain('gl-flex-direction-column');
expect(findIcon().classes()).toEqual(['gl-mr-3', 'gl-text-gray-500']); expect(findIcon().classes()).toEqual(['gl-mr-3', 'gl-text-gray-500']);
expect(findIcon().props('size')).toBe(16); expect(findIcon().props('size')).toBe(16);
}); });
it('applies correct classes when there are designs or design saving loader', () => { it('applies correct classes when displaying in card mode', () => {
createComponent({ props: { hasDesigns: true } }); createComponent({ props: { displayAsCard: true } });
expect(findDropzoneArea().classes()).toContain('gl-flex-direction-column'); expect(findDropzoneArea().classes()).toContain('gl-flex-direction-column');
expect(findIcon().classes()).toEqual(['gl-mb-2']); expect(findIcon().classes()).toEqual(['gl-mb-2']);
expect(findIcon().props('size')).toBe(24); expect(findIcon().props('size')).toBe(24);
}); });
it('correctly overrides description and drop messages', () => {
createComponent({
props: {
dropToStartMessage: 'Test drop-to-start message.',
validFileMimetypes: ['image/jpg', 'image/jpeg'],
},
slots: {
'upload-text': '<span>Test %{linkStart}description%{linkEnd} message.</span>',
},
});
expect(wrapper.element).toMatchSnapshot();
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment