Commit dd4092bb authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'design-dropzone-design-cards' into 'master'

Make design cards dropzones

See merge request gitlab-org/gitlab!26446
parents c4409050 67d2ff37
...@@ -77,17 +77,18 @@ Navigate to the **Design Management** page from any issue by clicking the **Desi ...@@ -77,17 +77,18 @@ Navigate to the **Design Management** page from any issue by clicking the **Desi
To upload design images, click the **Upload Designs** button and select images to upload. To upload design images, click the **Upload Designs** button and select images to upload.
Designs with the same filename as an existing uploaded design will create a new version
of the design, and will replace the previous version.
Designs cannot be added if the issue has been moved, or its
[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, [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. 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) ![Drag and drop design uploads](img/design_drag_and_drop_uploads_v12_9.png)
Designs with the same filename as an existing uploaded design will create a new version
of the design, and will replace the previous version. [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34353) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9, dropping a design on an existing uploaded design will also create a new version,
provided the filenames are the same.
Designs cannot be added if the issue has been moved, or its
[discussion is locked](../../discussions/#lock-discussions).
### 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.
......
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
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));
}, },
ondrop({ dataTransfer }) { ondrop({ dataTransfer = {} }) {
this.dragCounter = 0; this.dragCounter = 0;
// User already had feedback when dropzone was active, so bail here // User already had feedback when dropzone was active, so bail here
if (!this.isDragDataValid) { if (!this.isDragDataValid) {
...@@ -45,7 +45,7 @@ export default { ...@@ -45,7 +45,7 @@ export default {
return; return;
} }
this.$emit('upload', files); this.$emit('change', files);
}, },
ondragenter(e) { ondragenter(e) {
this.dragCounter += 1; this.dragCounter += 1;
...@@ -58,7 +58,7 @@ export default { ...@@ -58,7 +58,7 @@ export default {
this.$refs.fileUpload.$el.click(); this.$refs.fileUpload.$el.click();
}, },
onDesignInputChange(e) { onDesignInputChange(e) {
this.$emit('upload', e.target.files); this.$emit('change', e.target.files);
}, },
}, },
uploadDesignMutation, uploadDesignMutation,
......
...@@ -14,6 +14,8 @@ import projectQuery from '../graphql/queries/project.query.graphql'; ...@@ -14,6 +14,8 @@ import projectQuery from '../graphql/queries/project.query.graphql';
import allDesignsMixin from '../mixins/all_designs'; import allDesignsMixin from '../mixins/all_designs';
import { import {
UPLOAD_DESIGN_ERROR, UPLOAD_DESIGN_ERROR,
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
designUploadSkippedWarning, designUploadSkippedWarning,
designDeletionError, designDeletionError,
} from '../utils/error_messages'; } from '../utils/error_messages';
...@@ -197,6 +199,21 @@ export default { ...@@ -197,6 +199,21 @@ export default {
const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 }); const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 });
createFlash(errorMessage); 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);
},
}, },
beforeRouteUpdate(to, from, next) { beforeRouteUpdate(to, from, next) {
this.selectedDesigns = []; this.selectedDesigns = [];
...@@ -248,10 +265,12 @@ export default { ...@@ -248,10 +265,12 @@ export default {
</div> </div>
<ol v-else class="list-unstyled row"> <ol v-else class="list-unstyled row">
<li class="col-md-6 col-lg-4 mb-3"> <li class="col-md-6 col-lg-4 mb-3">
<design-dropzone class="design-list-item" @upload="onUploadDesign" /> <design-dropzone class="design-list-item" @change="onUploadDesign" />
</li> </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-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
><design v-bind="design" :is-loading="isDesignToBeSaved(design.filename)"
/></design-dropzone>
<input <input
v-if="canSelectDesign(design.filename)" v-if="canSelectDesign(design.filename)"
......
...@@ -30,6 +30,14 @@ const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( ...@@ -30,6 +30,14 @@ const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__(
'The designs you tried uploading did not change.', 'The designs you tried uploading did not change.',
)}`; )}`;
export const EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE = __(
'You can only upload one design when dropping onto an existing design.',
);
export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __(
'You must upload a file with the same file name when dropping onto an existing design.',
);
const MAX_SKIPPED_FILES_LISTINGS = 5; const MAX_SKIPPED_FILES_LISTINGS = 5;
const oneDesignSkippedMessage = filename => const oneDesignSkippedMessage = filename =>
......
---
title: Support drag-and-drop on existing designs in Design Management
merge_request: 26446
author:
type: added
...@@ -101,7 +101,7 @@ describe('Design management dropzone component', () => { ...@@ -101,7 +101,7 @@ describe('Design management dropzone component', () => {
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}) })
.then(() => { .then(() => {
expect(wrapper.emitted().upload[0]).toEqual([[mockFile]]); expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
}); });
}); });
}); });
...@@ -117,7 +117,7 @@ describe('Design management dropzone component', () => { ...@@ -117,7 +117,7 @@ describe('Design management dropzone component', () => {
const mockEvent = mockDragEvent({ files: [mockFile] }); const mockEvent = mockDragEvent({ files: [mockFile] });
wrapper.vm.ondrop(mockEvent); wrapper.vm.ondrop(mockEvent);
expect(wrapper.emitted().upload[0]).toEqual([[mockFile]]); expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
}); });
it('calls createFlash when files are invalid', () => { it('calls createFlash when files are invalid', () => {
......
...@@ -21,39 +21,45 @@ exports[`Design management index page designs does not render toolbar when there ...@@ -21,39 +21,45 @@ exports[`Design management index page designs does not render toolbar when there
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
<design-stub <design-dropzone-stub>
event="NONE" <design-stub
filename="design-1-name" event="NONE"
id="design-1" filename="design-1-name"
image="design-1-image" id="design-1"
notescount="0" image="design-1-image"
/> notescount="0"
/>
</design-dropzone-stub>
<!----> <!---->
</li> </li>
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
<design-stub <design-dropzone-stub>
event="NONE" <design-stub
filename="design-2-name" event="NONE"
id="design-2" filename="design-2-name"
image="design-2-image" id="design-2"
notescount="1" image="design-2-image"
/> notescount="1"
/>
</design-dropzone-stub>
<!----> <!---->
</li> </li>
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
<design-stub <design-dropzone-stub>
event="NONE" <design-stub
filename="design-3-name" event="NONE"
id="design-3" filename="design-3-name"
image="design-3-image" id="design-3"
notescount="0" image="design-3-image"
/> notescount="0"
/>
</design-dropzone-stub>
<!----> <!---->
</li> </li>
...@@ -121,13 +127,15 @@ exports[`Design management index page designs renders designs list and header wi ...@@ -121,13 +127,15 @@ exports[`Design management index page designs renders designs list and header wi
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
<design-stub <design-dropzone-stub>
event="NONE" <design-stub
filename="design-1-name" event="NONE"
id="design-1" filename="design-1-name"
image="design-1-image" id="design-1"
notescount="0" image="design-1-image"
/> notescount="0"
/>
</design-dropzone-stub>
<input <input
class="design-checkbox" class="design-checkbox"
...@@ -137,13 +145,15 @@ exports[`Design management index page designs renders designs list and header wi ...@@ -137,13 +145,15 @@ exports[`Design management index page designs renders designs list and header wi
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
<design-stub <design-dropzone-stub>
event="NONE" <design-stub
filename="design-2-name" event="NONE"
id="design-2" filename="design-2-name"
image="design-2-image" id="design-2"
notescount="1" image="design-2-image"
/> notescount="1"
/>
</design-dropzone-stub>
<input <input
class="design-checkbox" class="design-checkbox"
...@@ -153,13 +163,15 @@ exports[`Design management index page designs renders designs list and header wi ...@@ -153,13 +163,15 @@ exports[`Design management index page designs renders designs list and header wi
<li <li
class="col-md-6 col-lg-4 mb-3" class="col-md-6 col-lg-4 mb-3"
> >
<design-stub <design-dropzone-stub>
event="NONE" <design-stub
filename="design-3-name" event="NONE"
id="design-3" filename="design-3-name"
image="design-3-image" id="design-3"
notescount="0" image="design-3-image"
/> notescount="0"
/>
</design-dropzone-stub>
<input <input
class="design-checkbox" class="design-checkbox"
......
...@@ -9,6 +9,10 @@ import DesignDestroyer from 'ee/design_management/components/design_destroyer.vu ...@@ -9,6 +9,10 @@ import DesignDestroyer from 'ee/design_management/components/design_destroyer.vu
import DesignDropzone from 'ee/design_management/components/upload/design_dropzone.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 {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
} from 'ee/design_management/utils/error_messages';
import createFlash from '~/flash'; import createFlash from '~/flash';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -63,7 +67,8 @@ describe('Design management index page', () => { ...@@ -63,7 +67,8 @@ 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 findDropzone = () => wrapper.find(DesignDropzone); const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
function createComponent({ function createComponent({
loading = false, loading = false,
...@@ -225,7 +230,7 @@ describe('Design management index page', () => { ...@@ -225,7 +230,7 @@ describe('Design management index page', () => {
}; };
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
findDropzone().vm.$emit('upload', [{ name: 'test' }]); findDropzone().vm.$emit('change', [{ 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();
...@@ -328,6 +333,42 @@ describe('Design management index page', () => { ...@@ -328,6 +333,42 @@ describe('Design management index page', () => {
); );
}); });
}); });
describe('dragging onto an existing design', () => {
beforeEach(() => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
});
it('calls onUploadDesign with valid upload', () => {
wrapper.setMethods({
onUploadDesign: jest.fn(),
});
const mockUploadPayload = [
{
name: mockDesigns[0].filename,
},
];
const designDropzone = findFirstDropzoneWithDesign();
designDropzone.vm.$emit('change', mockUploadPayload);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith(mockUploadPayload);
});
it.each`
description | eventPayload | message
${'> 1 file'} | ${[{ name: 'test' }, { name: 'test-2' }]} | ${EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE}
${'different filename'} | ${[{ name: 'wrong-name' }]} | ${EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE}
`('calls createFlash when upload has $description', ({ eventPayload, message }) => {
const designDropzone = findFirstDropzoneWithDesign();
designDropzone.vm.$emit('change', eventPayload);
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(message);
});
});
}); });
describe('on latest version when has designs', () => { describe('on latest version when has designs', () => {
......
...@@ -22791,6 +22791,9 @@ msgstr "" ...@@ -22791,6 +22791,9 @@ msgstr ""
msgid "You can only transfer the project to namespaces you manage." msgid "You can only transfer the project to namespaces you manage."
msgstr "" msgstr ""
msgid "You can only upload one design when dropping onto an existing design."
msgstr ""
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr "" msgstr ""
...@@ -22944,6 +22947,9 @@ msgstr "" ...@@ -22944,6 +22947,9 @@ msgstr ""
msgid "You must set up incoming email before it becomes active." msgid "You must set up incoming email before it becomes active."
msgstr "" msgstr ""
msgid "You must upload a file with the same file name when dropping onto an existing design."
msgstr ""
msgid "You need a different license to enable FileLocks feature" msgid "You need a different license to enable FileLocks feature"
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