Commit da55bff9 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '202634-upload-a-design-by-copy-pasting-the-file-into-the-design-tab' into 'master'

Upload a design by copy/pasting the file into the Design Tab

See merge request gitlab-org/gitlab!27776
parents 60d5a40d f7b54a14
...@@ -6,6 +6,7 @@ import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table'; ...@@ -6,6 +6,7 @@ import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import csrf from './lib/utils/csrf'; import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { n__, __ } from '~/locale'; import { n__, __ } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
...@@ -41,7 +42,6 @@ export default function dropzoneInput(form) { ...@@ -41,7 +42,6 @@ export default function dropzoneInput(form) {
let addFileToForm; let addFileToForm;
let updateAttachingMessage; let updateAttachingMessage;
let isImage; let isImage;
let getFilename;
let uploadFile; let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>'); formTextarea.wrap('<div class="div-dropzone"></div>');
...@@ -235,17 +235,6 @@ export default function dropzoneInput(form) { ...@@ -235,17 +235,6 @@ export default function dropzoneInput(form) {
$(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`); $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
}; };
getFilename = e => {
let value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
value = e.clipboardData.getData('text/plain');
}
value = value.split('\r');
return value[0];
};
const showSpinner = () => $uploadingProgressContainer.removeClass('hide'); const showSpinner = () => $uploadingProgressContainer.removeClass('hide');
const closeSpinner = () => $uploadingProgressContainer.addClass('hide'); const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
......
...@@ -14,3 +14,14 @@ export default (buttonSelector, fileSelector) => { ...@@ -14,3 +14,14 @@ export default (buttonSelector, fileSelector) => {
form.querySelector('.js-filename').textContent = fileInput.value.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape form.querySelector('.js-filename').textContent = fileInput.value.replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
}); });
}; };
export const getFilename = ({ clipboardData }) => {
let value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
} else if (clipboardData && clipboardData.getData) {
value = clipboardData.getData('text/plain');
}
value = value.split('\r');
return value[0];
};
---
title: Upload a design by copy/pasting the file into the Design Tab
merge_request: 27776
author:
type: added
...@@ -75,6 +75,19 @@ you can drag and drop designs onto the dedicated dropzone to upload them. ...@@ -75,6 +75,19 @@ 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)
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202634)
in GitLab 12.10, you can also copy images from your file system and
paste them directly on GitLab's Design page as a new design.
On macOS you can also take a screenshot and immediately copy it to
the clipboard by simultaneously clicking <kbd>Control</kbd> + <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>3</kbd>, and then paste it as a design.
Copy-and-pasting has some limitations:
- You can paste only one image at a time. When copy/pasting multiple files, only the first one will be uploaded.
- All images will be converted to `png` format under the hood, so when you want to copy/paste `gif` file, it will result in broken animation.
- Copy/pasting designs is not supported on Internet Explorer.
Designs with the same filename as an existing uploaded design will create a new version 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, 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. provided the filenames are the same.
......
...@@ -20,7 +20,11 @@ import { ...@@ -20,7 +20,11 @@ import {
designDeletionError, designDeletionError,
} from '../utils/error_messages'; } from '../utils/error_messages';
import { updateStoreAfterUploadDesign } from '../utils/cache_update'; import { updateStoreAfterUploadDesign } from '../utils/cache_update';
import { designUploadOptimisticResponse } from '../utils/design_management_utils'; import {
designUploadOptimisticResponse,
isValidDesignFile,
} from '../utils/design_management_utils';
import { getFilename } from '~/lib/utils/file_upload';
import { DESIGNS_ROUTE_NAME } from '../router/constants'; import { DESIGNS_ROUTE_NAME } from '../router/constants';
const MAXIMUM_FILE_UPLOAD_LIMIT = 10; const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
...@@ -93,6 +97,9 @@ export default { ...@@ -93,6 +97,9 @@ export default {
: s__('DesignManagement|Select all'); : s__('DesignManagement|Select all');
}, },
}, },
mounted() {
this.toggleOnPasteListener(this.$route.name);
},
methods: { methods: {
resetFilesToBeSaved() { resetFilesToBeSaved() {
this.filesToBeSaved = []; this.filesToBeSaved = [];
...@@ -215,11 +222,36 @@ export default { ...@@ -215,11 +222,36 @@ export default {
this.onUploadDesign(files); 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();
const filename = getFilename(event) || 'image.png';
const newFile = new File([files[0]], filename);
this.onUploadDesign([newFile]);
}
},
toggleOnPasteListener(route) {
if (route === DESIGNS_ROUTE_NAME) {
document.addEventListener('paste', this.onDesignPaste);
} else {
document.removeEventListener('paste', this.onDesignPaste);
}
},
}, },
beforeRouteUpdate(to, from, next) { beforeRouteUpdate(to, from, next) {
this.toggleOnPasteListener(to.name);
this.selectedDesigns = []; this.selectedDesigns = [];
next(); next();
}, },
beforeRouteLeave(to, from, next) {
this.toggleOnPasteListener(to.name);
next();
},
}; };
</script> </script>
......
...@@ -96,6 +96,7 @@ describe('Design management index page', () => { ...@@ -96,6 +96,7 @@ describe('Design management index page', () => {
localVue, localVue,
router, router,
stubs: { DesignDestroyer, ApolloMutation, ...stubs }, stubs: { DesignDestroyer, ApolloMutation, ...stubs },
attachToDocument: true,
}); });
wrapper.setData({ wrapper.setData({
...@@ -469,4 +470,49 @@ describe('Design management index page', () => { ...@@ -469,4 +470,49 @@ describe('Design management index page', () => {
expect(findSelectAllButton().exists()).toBe(false); expect(findSelectAllButton().exists()).toBe(false);
}); });
}); });
describe('pasting a design', () => {
let event;
beforeEach(() => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
wrapper.setMethods({
onUploadDesign: jest.fn(),
});
event = new Event('paste');
router.replace({
name: DESIGNS_ROUTE_NAME,
query: {
version: '2',
},
});
});
it('calls onUploadDesign with valid paste', () => {
event.clipboardData = {
files: [{ name: 'image.png', type: 'image/png' }],
getData: () => 'test.png',
};
document.dispatchEvent(event);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
new File([{ name: 'image.png' }], 'test.png'),
]);
});
it('does not call onUploadDesign with invalid paste', () => {
event.clipboardData = {
items: [{ type: 'text/plain' }, { type: 'text' }],
files: [],
};
document.dispatchEvent(event);
expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
});
});
}); });
import fileUpload from '~/lib/utils/file_upload'; import fileUpload, { getFilename } from '~/lib/utils/file_upload';
describe('File upload', () => { describe('File upload', () => {
beforeEach(() => { beforeEach(() => {
...@@ -62,3 +62,15 @@ describe('File upload', () => { ...@@ -62,3 +62,15 @@ describe('File upload', () => {
expect(input.click).not.toHaveBeenCalled(); expect(input.click).not.toHaveBeenCalled();
}); });
}); });
describe('getFilename', () => {
it('returns first value correctly', () => {
const event = {
clipboardData: {
getData: () => 'test.png\rtest.txt',
},
};
expect(getFilename(event)).toBe('test.png');
});
});
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