Commit 1b47b68c authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'design-dropzone' into 'master'

Drag and drop design uploads

See merge request gitlab-org/gitlab!26139
parents 55d0427e 42b7f91b
...@@ -46,10 +46,16 @@ If the requirements are not met, the **Designs** tab displays a message to the u ...@@ -46,10 +46,16 @@ If the requirements are not met, the **Designs** tab displays a message to the u
Designs support short references in Markdown, but this needs to be enabled by setting Designs support short references in Markdown, but this needs to be enabled by setting
the `:design_management_reference_filter_gfm_pipeline` feature flag. the `:design_management_reference_filter_gfm_pipeline` feature flag.
## Supported files
Files uploaded must have a file extension of either `png`, `jpg`, `jpeg`,
`gif`, `bmp`, `tiff` or `ico`.
Support for [SVG files](https://gitlab.com/gitlab-org/gitlab/issues/12771)
and [PDFs](https://gitlab.com/gitlab-org/gitlab/issues/32811) is planned for a future release.
## Limitations ## Limitations
- Files uploaded must have a file extension of either `png`, `jpg`, `jpeg`, `gif`, `bmp`, `tiff` or `ico`.
The [`svg` extension is not yet supported](https://gitlab.com/gitlab-org/gitlab/issues/12771).
- Design uploads are limited to 10 files at a time. - Design uploads are limited to 10 files at a time.
- Design Management data - Design Management data
[isn't deleted when a project is destroyed](https://gitlab.com/gitlab-org/gitlab/issues/13429) yet. [isn't deleted when a project is destroyed](https://gitlab.com/gitlab-org/gitlab/issues/13429) yet.
...@@ -77,6 +83,11 @@ of the design, and will replace the previous version. ...@@ -77,6 +83,11 @@ of the design, and will replace the previous version.
Designs cannot be added if the issue has been moved, or its Designs cannot be added if the issue has been moved, or its
[discussion is locked](../../discussions/#lock-discussions). [discussion is locked](../../discussions/#lock-discussions).
[Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34353) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9,
you can drag and drop designs onto the dedicated dropzone to upload them.
![Drag and drop design uploads](img/design_drag_and_drop_uploads_v12_9.png)
### Skipped designs ### Skipped designs
Designs with the same filename as an existing uploaded design _and_ whose content has not changed will be skipped. Designs with the same filename as an existing uploaded design _and_ whose content has not changed will be skipped.
......
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import DesignInput from './design_input.vue';
import uploadDesignMutation from '../../graphql/mutations/uploadDesign.mutation.graphql';
import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages';
import { isValidDesignFile } from '../../utils/design_management_utils';
import { VALID_DATA_TRANSFER_TYPE } from '../../constants';
export default {
components: {
GlIcon,
GlLink,
GlSprintf,
DesignInput,
},
data() {
return {
dragCounter: 0,
isDragDataValid: false,
};
},
computed: {
dragging() {
return this.dragCounter !== 0;
},
},
methods: {
isValidUpload(files) {
return files.every(isValidDesignFile);
},
isValidDragDataType({ dataTransfer }) {
return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE));
},
ondrop({ dataTransfer }) {
this.dragCounter = 0;
// User already had feedback when dropzone was active, so bail here
if (!this.isDragDataValid) {
return;
}
const { files } = dataTransfer;
if (!this.isValidUpload(Array.from(files))) {
createFlash(UPLOAD_DESIGN_INVALID_FILETYPE_ERROR);
return;
}
this.$emit('upload', files);
},
ondragenter(e) {
this.dragCounter += 1;
this.isDragDataValid = this.isValidDragDataType(e);
},
ondragleave() {
this.dragCounter -= 1;
},
openFileUpload() {
this.$refs.fileUpload.$el.click();
},
onDesignInputChange(e) {
this.$emit('upload', e.target.files);
},
},
uploadDesignMutation,
};
</script>
<template>
<div
class="w-100 position-relative"
@dragstart.prevent.stop
@dragend.prevent.stop
@dragover.prevent.stop
@dragenter.prevent.stop="ondragenter"
@dragleave.prevent.stop="ondragleave"
@drop.prevent.stop="ondrop"
>
<slot>
<button
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center 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>
<template #link="{ content }">
<gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
</button>
<design-input ref="fileUpload" @change="onDesignInputChange" />
</slot>
<transition name="design-dropzone-fade">
<div
v-show="dragging"
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>
<span>{{
__(
'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
)
}}</span>
</div>
<div v-show="isDragDataValid" class="mw-50 text-center">
<h3>{{ __('Incoming!') }}</h3>
<span>{{ __('Drop your designs to start your upload.') }}</span>
</div>
</div>
</transition>
</div>
</template>
<script> <script>
import VALID_DESIGN_FILE_MIMETYPE from '../../constants'; import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
export default { export default {
VALID_DESIGN_FILE_MIMETYPE, VALID_DESIGN_FILE_MIMETYPE,
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
<input <input
type="file" type="file"
name="design_file" name="design_file"
:accept="$options.VALID_DESIGN_FILE_MIMETYPE" :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
class="hide" class="hide"
multiple multiple
@change="$emit('change', $event)" @change="$emit('change', $event)"
......
// WARNING: replace this with something // WARNING: replace this with something
// more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611 // more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611
const VALID_DESIGN_FILE_MIMETYPE = 'image/*'; export const VALID_DESIGN_FILE_MIMETYPE = {
mimetype: 'image/*',
regex: /image\/.+/,
};
export { VALID_DESIGN_FILE_MIMETYPE as default }; // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
export const VALID_DATA_TRANSFER_TYPE = 'Files';
<script> <script>
import { GlLoadingIcon, GlEmptyState, GlButton } from '@gitlab/ui'; import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import UploadButton from '../components/upload/button.vue'; import UploadButton from '../components/upload/button.vue';
...@@ -7,6 +7,7 @@ import DeleteButton from '../components/delete_button.vue'; ...@@ -7,6 +7,7 @@ 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 uploadDesignMutation from '../graphql/mutations/uploadDesign.mutation.graphql'; import uploadDesignMutation from '../graphql/mutations/uploadDesign.mutation.graphql';
import permissionsQuery from '../graphql/queries/permissions.query.graphql'; import permissionsQuery from '../graphql/queries/permissions.query.graphql';
import projectQuery from '../graphql/queries/project.query.graphql'; import projectQuery from '../graphql/queries/project.query.graphql';
...@@ -26,12 +27,12 @@ export default { ...@@ -26,12 +27,12 @@ export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
UploadButton, UploadButton,
GlEmptyState,
GlButton, GlButton,
Design, Design,
DesignDestroyer, DesignDestroyer,
DesignVersionDropdown, DesignVersionDropdown,
DeleteButton, DeleteButton,
DesignDropzone,
}, },
mixins: [allDesignsMixin], mixins: [allDesignsMixin],
apollo: { apollo: {
...@@ -245,9 +246,13 @@ export default { ...@@ -245,9 +246,13 @@ export default {
<div v-else-if="error" class="alert alert-danger"> <div v-else-if="error" class="alert alert-danger">
{{ __('An error occurred while loading designs. Please try again.') }} {{ __('An error occurred while loading designs. Please try again.') }}
</div> </div>
<ol v-else-if="hasDesigns" class="list-unstyled row"> <ol v-else class="list-unstyled row">
<li class="col-md-6 col-lg-4 mb-3">
<design-dropzone class="design-list-item" @upload="onUploadDesign" />
</li>
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3"> <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
<design v-bind="design" :is-loading="isDesignToBeSaved(design.filename)" /> <design v-bind="design" :is-loading="isDesignToBeSaved(design.filename)" />
<input <input
v-if="canSelectDesign(design.filename)" v-if="canSelectDesign(design.filename)"
:checked="isDesignSelected(design.filename)" :checked="isDesignSelected(design.filename)"
...@@ -257,20 +262,6 @@ export default { ...@@ -257,20 +262,6 @@ export default {
/> />
</li> </li>
</ol> </ol>
<gl-empty-state
v-else
:title="s__('DesignManagement|The one place for your designs')"
:description="
s__(`DesignManagement|Upload and view the latest designs for this issue.
Consistent and easy to find, so everyone is up to date.`)
"
>
<template #actions>
<div v-if="canCreateDesign" class="center">
<upload-button :is-saving="isSaving" @upload="onUploadDesign" />
</div>
</template>
</gl-empty-state>
</div> </div>
<router-view /> <router-view />
</div> </div>
......
import { uniqueId } from 'underscore'; import { uniqueId } from 'underscore';
import { VALID_DESIGN_FILE_MIMETYPE } from '../constants';
export const isValidDesignFile = ({ type }) =>
(type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0;
/** /**
* Returns formatted array that doesn't contain * Returns formatted array that doesn't contain
......
...@@ -16,6 +16,10 @@ export const UPLOAD_DESIGN_ERROR = s__( ...@@ -16,6 +16,10 @@ export const UPLOAD_DESIGN_ERROR = s__(
'DesignManagement|Error uploading a new design. Please try again.', 'DesignManagement|Error uploading a new design. Please try again.',
); );
export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __(
'Could not upload your designs as one or more files uploaded are not supported.',
);
export const DESIGN_NOT_FOUND_ERROR = __('Could not find design'); export const DESIGN_NOT_FOUND_ERROR = __('Could not find design');
export const DESIGN_NOT_EXIST_ERROR = __('Requested design version does not exist'); export const DESIGN_NOT_EXIST_ERROR = __('Requested design version does not exist');
......
...@@ -94,3 +94,40 @@ ...@@ -94,3 +94,40 @@
flex-basis: auto; flex-basis: auto;
} }
} }
.design-dropzone-border {
border: 2px dashed $gray-200;
}
.design-dropzone-card {
transition: border $general-hover-transition-duration $general-hover-transition-curve;
&:focus,
&:active {
outline: none;
border: 2px dashed $purple;
color: $gl-text-color;
}
&:hover {
border-color: $gray-500;
}
}
.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;
}
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
} }
.design-list-item { .design-list-item {
height: 280px;
.card-body { .card-body {
height: 230px; height: 230px;
} }
......
---
title: Support drag-and-drop design uploads in Design Management
merge_request: 26139
author:
type: added
...@@ -29,7 +29,7 @@ describe 'User uploads new design', :js do ...@@ -29,7 +29,7 @@ describe 'User uploads new design', :js do
expect(page).to have_selector('.js-design-list-item', count: 1) expect(page).to have_selector('.js-design-list-item', count: 1)
within first('#designs-tab .card') do within first('#designs-tab .js-design-list-item') do
expect(page).to have_content('dk.png') expect(page).to have_content('dk.png')
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management dropzone component when dragging renders correct template when drag event contains files 1`] = `
<div
class="w-100 position-relative"
>
<button
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
class="d-flex-center flex-column text-center"
>
<gl-icon-stub
class="mb-4"
name="doc-new"
size="48"
/>
<p>
<gl-sprintf-stub
message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
</button>
<design-input-stub />
<transition-stub
name="design-dropzone-fade"
>
<div
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
style=""
>
<div
class="mw-50 text-center"
style="display: none;"
>
<h3>
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.
</span>
</div>
<div
class="mw-50 text-center"
style=""
>
<h3>
Incoming!
</h3>
<span>
Drop your designs to start your upload.
</span>
</div>
</div>
</transition-stub>
</div>
`;
exports[`Design management dropzone component when dragging renders correct template when drag event contains files and text 1`] = `
<div
class="w-100 position-relative"
>
<button
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
class="d-flex-center flex-column text-center"
>
<gl-icon-stub
class="mb-4"
name="doc-new"
size="48"
/>
<p>
<gl-sprintf-stub
message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
</button>
<design-input-stub />
<transition-stub
name="design-dropzone-fade"
>
<div
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
style=""
>
<div
class="mw-50 text-center"
style="display: none;"
>
<h3>
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.
</span>
</div>
<div
class="mw-50 text-center"
style=""
>
<h3>
Incoming!
</h3>
<span>
Drop your designs to start your upload.
</span>
</div>
</div>
</transition-stub>
</div>
`;
exports[`Design management dropzone component when dragging renders correct template when drag event contains text 1`] = `
<div
class="w-100 position-relative"
>
<button
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
class="d-flex-center flex-column text-center"
>
<gl-icon-stub
class="mb-4"
name="doc-new"
size="48"
/>
<p>
<gl-sprintf-stub
message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
</button>
<design-input-stub />
<transition-stub
name="design-dropzone-fade"
>
<div
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
style=""
>
<div
class="mw-50 text-center"
>
<h3>
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.
</span>
</div>
<div
class="mw-50 text-center"
style="display: none;"
>
<h3>
Incoming!
</h3>
<span>
Drop your designs to start your upload.
</span>
</div>
</div>
</transition-stub>
</div>
`;
exports[`Design management dropzone component when dragging renders correct template when drag event is empty 1`] = `
<div
class="w-100 position-relative"
>
<button
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
class="d-flex-center flex-column text-center"
>
<gl-icon-stub
class="mb-4"
name="doc-new"
size="48"
/>
<p>
<gl-sprintf-stub
message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
</button>
<design-input-stub />
<transition-stub
name="design-dropzone-fade"
>
<div
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
style=""
>
<div
class="mw-50 text-center"
>
<h3>
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.
</span>
</div>
<div
class="mw-50 text-center"
style="display: none;"
>
<h3>
Incoming!
</h3>
<span>
Drop your designs to start your upload.
</span>
</div>
</div>
</transition-stub>
</div>
`;
exports[`Design management dropzone component when dragging renders correct template when dragging stops 1`] = `
<div
class="w-100 position-relative"
>
<button
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
class="d-flex-center flex-column text-center"
>
<gl-icon-stub
class="mb-4"
name="doc-new"
size="48"
/>
<p>
<gl-sprintf-stub
message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
</button>
<design-input-stub />
<transition-stub
name="design-dropzone-fade"
>
<div
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
style="display: none;"
>
<div
class="mw-50 text-center"
>
<h3>
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.
</span>
</div>
<div
class="mw-50 text-center"
style="display: none;"
>
<h3>
Incoming!
</h3>
<span>
Drop your designs to start your upload.
</span>
</div>
</div>
</transition-stub>
</div>
`;
exports[`Design management dropzone component when no slot provided renders default dropzone card 1`] = `
<div
class="w-100 position-relative"
>
<button
class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
class="d-flex-center flex-column text-center"
>
<gl-icon-stub
class="mb-4"
name="doc-new"
size="48"
/>
<p>
<gl-sprintf-stub
message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
</button>
<design-input-stub />
<transition-stub
name="design-dropzone-fade"
>
<div
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
style="display: none;"
>
<div
class="mw-50 text-center"
>
<h3>
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.
</span>
</div>
<div
class="mw-50 text-center"
style="display: none;"
>
<h3>
Incoming!
</h3>
<span>
Drop your designs to start your upload.
</span>
</div>
</div>
</transition-stub>
</div>
`;
exports[`Design management dropzone component when slot provided renders dropzone with slot content 1`] = `
<div
class="w-100 position-relative"
>
<div>
dropzone slot
</div>
<transition-stub
name="design-dropzone-fade"
>
<div
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
style="display: none;"
>
<div
class="mw-50 text-center"
>
<h3>
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.
</span>
</div>
<div
class="mw-50 text-center"
style="display: none;"
>
<h3>
Incoming!
</h3>
<span>
Drop your designs to start your upload.
</span>
</div>
</div>
</transition-stub>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import DesignDropzone from 'ee/design_management/components/upload/design_dropzone.vue';
import DesignInput from 'ee/design_management/components/upload/design_input.vue';
import createFlash from '~/flash';
jest.mock('~/flash');
describe('Design management dropzone component', () => {
let wrapper;
const mockDragEvent = ({ types = ['Files'], files = [] }) => {
return { dataTransfer: { types, files } };
};
const findDropzoneCard = () => wrapper.find('.design-dropzone-card');
function createComponent({ slots = {}, data = {} } = {}) {
wrapper = shallowMount(DesignDropzone, {
slots,
data() {
return data;
},
});
}
afterEach(() => {
wrapper.destroy();
});
describe('when slot provided', () => {
it('renders dropzone with slot content', () => {
createComponent({
slots: {
default: ['<div>dropzone slot</div>'],
},
});
expect(wrapper.element).toMatchSnapshot();
});
});
describe('when no slot provided', () => {
it('renders default dropzone card', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('triggers click event on file input element when clicked', () => {
createComponent();
const clickSpy = jest.spyOn(wrapper.find(DesignInput).element, 'click');
findDropzoneCard().trigger('click');
expect(clickSpy).toHaveBeenCalled();
});
});
describe('when dragging', () => {
it.each`
description | eventPayload
${'is empty'} | ${{}}
${'contains text'} | ${mockDragEvent({ types: ['text'] })}
${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })}
${'contains files'} | ${mockDragEvent({ types: ['Files'] })}
`('renders correct template when drag event $description', ({ eventPayload }) => {
createComponent();
wrapper.trigger('dragenter', eventPayload);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('renders correct template when dragging stops', () => {
createComponent();
wrapper.trigger('dragenter');
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.trigger('dragleave');
return wrapper.vm.$nextTick();
})
.then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
});
describe('when dropping', () => {
it('emits upload event', () => {
createComponent();
const mockFile = { name: 'test', type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
wrapper.trigger('dragenter', mockEvent);
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.trigger('drop', mockEvent);
return wrapper.vm.$nextTick();
})
.then(() => {
expect(wrapper.emitted().upload[0]).toEqual([[mockFile]]);
});
});
});
describe('ondrop', () => {
const mockData = { dragCounter: 1, isDragDataValid: true };
describe('when drag data is valid', () => {
it('emits upload event for valid files', () => {
createComponent({ data: mockData });
const mockFile = { type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
wrapper.vm.ondrop(mockEvent);
expect(wrapper.emitted().upload[0]).toEqual([[mockFile]]);
});
it('calls createFlash when files are invalid', () => {
createComponent({ data: mockData });
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
wrapper.vm.ondrop(mockEvent);
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
});
...@@ -10,6 +10,14 @@ exports[`Design management index page designs does not render toolbar when there ...@@ -10,6 +10,14 @@ exports[`Design management index page designs does not render toolbar when there
<ol <ol
class="list-unstyled row" class="list-unstyled row"
> >
<li
class="col-md-6 col-lg-4 mb-3"
>
<design-dropzone-stub
class="design-list-item"
/>
</li>
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
...@@ -102,6 +110,14 @@ exports[`Design management index page designs renders designs list and header wi ...@@ -102,6 +110,14 @@ exports[`Design management index page designs renders designs list and header wi
<ol <ol
class="list-unstyled row" class="list-unstyled row"
> >
<li
class="col-md-6 col-lg-4 mb-3"
>
<design-dropzone-stub
class="design-list-item"
/>
</li>
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
...@@ -208,10 +224,18 @@ exports[`Design management index page when has no designs renders empty text 1`] ...@@ -208,10 +224,18 @@ exports[`Design management index page when has no designs renders empty text 1`]
<div <div
class="mt-4" class="mt-4"
> >
<gl-empty-state-stub <ol
description="Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date." class="list-unstyled row"
title="The one place for your designs" >
/> <li
class="col-md-6 col-lg-4 mb-3"
>
<design-dropzone-stub
class="design-list-item"
/>
</li>
</ol>
</div> </div>
<router-view-stub <router-view-stub
......
...@@ -6,7 +6,7 @@ import { GlEmptyState } from '@gitlab/ui'; ...@@ -6,7 +6,7 @@ import { GlEmptyState } from '@gitlab/ui';
import Index from 'ee/design_management/pages/index.vue'; import Index from 'ee/design_management/pages/index.vue';
import uploadDesignQuery from 'ee/design_management/graphql/mutations/uploadDesign.mutation.graphql'; import uploadDesignQuery from 'ee/design_management/graphql/mutations/uploadDesign.mutation.graphql';
import DesignDestroyer from 'ee/design_management/components/design_destroyer.vue'; import DesignDestroyer from 'ee/design_management/components/design_destroyer.vue';
import UploadButton from 'ee/design_management/components/upload/button.vue'; import DesignDropzone from 'ee/design_management/components/upload/design_dropzone.vue';
import DeleteButton from 'ee/design_management/components/delete_button.vue'; import DeleteButton from 'ee/design_management/components/delete_button.vue';
import { DESIGNS_ROUTE_NAME } from 'ee/design_management/router/constants'; import { DESIGNS_ROUTE_NAME } from 'ee/design_management/router/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -63,7 +63,7 @@ describe('Design management index page', () => { ...@@ -63,7 +63,7 @@ describe('Design management index page', () => {
const findSelectAllButton = () => wrapper.find('.js-select-all'); const findSelectAllButton = () => wrapper.find('.js-select-all');
const findToolbar = () => wrapper.find('.qa-selector-toolbar'); const findToolbar = () => wrapper.find('.qa-selector-toolbar');
const findDeleteButton = () => wrapper.find(DeleteButton); const findDeleteButton = () => wrapper.find(DeleteButton);
const findUploadButton = () => wrapper.find(UploadButton); const findDropzone = () => wrapper.find(DesignDropzone);
function createComponent({ function createComponent({
loading = false, loading = false,
...@@ -225,7 +225,7 @@ describe('Design management index page', () => { ...@@ -225,7 +225,7 @@ describe('Design management index page', () => {
}; };
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
findUploadButton().vm.$emit('upload', [{ name: 'test' }]); findDropzone().vm.$emit('upload', [{ name: 'test' }]);
expect(mutate).toHaveBeenCalledWith(mutationVariables); expect(mutate).toHaveBeenCalledWith(mutationVariables);
expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]); expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
expect(wrapper.vm.isSaving).toBeTruthy(); expect(wrapper.vm.isSaving).toBeTruthy();
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
findVersionId, findVersionId,
designUploadOptimisticResponse, designUploadOptimisticResponse,
updateImageDiffNoteOptimisticResponse, updateImageDiffNoteOptimisticResponse,
isValidDesignFile,
} from 'ee/design_management/utils/design_management_utils'; } from 'ee/design_management/utils/design_management_utils';
describe('extractCurrentDiscussion', () => { describe('extractCurrentDiscussion', () => {
...@@ -134,3 +135,25 @@ describe('optimistic responses', () => { ...@@ -134,3 +135,25 @@ describe('optimistic responses', () => {
); );
}); });
}); });
describe('isValidDesignFile', () => {
// test every filetype that Design Management supports
// https://docs.gitlab.com/ee/user/project/issues/design_management.html#limitations
it.each`
mimetype | isValid
${'image/svg'} | ${true}
${'image/png'} | ${true}
${'image/jpg'} | ${true}
${'image/jpeg'} | ${true}
${'image/gif'} | ${true}
${'image/bmp'} | ${true}
${'image/tiff'} | ${true}
${'image/ico'} | ${true}
${'image/svg'} | ${true}
${'video/mpeg'} | ${false}
${'audio/midi'} | ${false}
${'application/octet-stream'} | ${false}
`('returns $valid for file type $mimetype', ({ mimetype, isValid }) => {
expect(isValidDesignFile({ type: mimetype })).toBe(isValid);
});
});
...@@ -333,6 +333,9 @@ msgstr "" ...@@ -333,6 +333,9 @@ msgstr ""
msgid "%{level_name} is not allowed since the fork source project has lower visibility." msgid "%{level_name} is not allowed since the fork source project has lower visibility."
msgstr "" msgstr ""
msgid "%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
msgstr ""
msgid "%{link_start}Read more%{link_end} about role permissions" msgid "%{link_start}Read more%{link_end} about role permissions"
msgstr "" msgstr ""
...@@ -5660,6 +5663,9 @@ msgstr "" ...@@ -5660,6 +5663,9 @@ msgstr ""
msgid "Could not save prometheus manual configuration" msgid "Could not save prometheus manual configuration"
msgstr "" msgstr ""
msgid "Could not upload your designs as one or more files uploaded are not supported."
msgstr ""
msgid "Country" msgid "Country"
msgstr "" msgstr ""
...@@ -6791,15 +6797,9 @@ msgstr "" ...@@ -6791,15 +6797,9 @@ msgstr ""
msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again." msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again."
msgstr "" msgstr ""
msgid "DesignManagement|The one place for your designs"
msgstr ""
msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance." msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance."
msgstr "" msgstr ""
msgid "DesignManagement|Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date."
msgstr ""
msgid "DesignManagement|Upload skipped." msgid "DesignManagement|Upload skipped."
msgstr "" msgstr ""
...@@ -7117,6 +7117,9 @@ msgstr "" ...@@ -7117,6 +7117,9 @@ msgstr ""
msgid "Downvotes" msgid "Downvotes"
msgstr "" msgstr ""
msgid "Drop your designs to start your upload."
msgstr ""
msgid "Due date" msgid "Due date"
msgstr "" msgstr ""
...@@ -10773,6 +10776,9 @@ msgstr "" ...@@ -10773,6 +10776,9 @@ msgstr ""
msgid "Incoming email" msgid "Incoming email"
msgstr "" msgstr ""
msgid "Incoming!"
msgstr ""
msgid "Incompatible Project" msgid "Incompatible Project"
msgstr "" msgstr ""
...@@ -13517,6 +13523,9 @@ msgstr "" ...@@ -13517,6 +13523,9 @@ msgstr ""
msgid "OfSearchInADropdown|Filter" msgid "OfSearchInADropdown|Filter"
msgstr "" msgstr ""
msgid "Oh no!"
msgstr ""
msgid "Ok let's go" msgid "Ok let's go"
msgstr "" msgstr ""
...@@ -22706,6 +22715,9 @@ msgstr "" ...@@ -22706,6 +22715,9 @@ msgstr ""
msgid "You are receiving this message because you are a GitLab administrator for %{url}." msgid "You are receiving this message because you are a GitLab administrator for %{url}."
msgstr "" msgstr ""
msgid "You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico."
msgstr ""
msgid "You can %{linkStart}view the blob%{linkEnd} instead." msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr "" msgstr ""
......
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