Commit f7b54a14 authored by Natalia Tepluhina's avatar Natalia Tepluhina

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

parent d7388857
......@@ -6,6 +6,7 @@ import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
import { n__, __ } from '~/locale';
import { getFilename } from '~/lib/utils/file_upload';
Dropzone.autoDiscover = false;
......@@ -41,7 +42,6 @@ export default function dropzoneInput(form) {
let addFileToForm;
let updateAttachingMessage;
let isImage;
let getFilename;
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>');
......@@ -235,17 +235,6 @@ export default function dropzoneInput(form) {
$(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 closeSpinner = () => $uploadingProgressContainer.addClass('hide');
......
......@@ -14,3 +14,14 @@ export default (buttonSelector, fileSelector) => {
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.
![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
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.
......
......@@ -20,7 +20,11 @@ import {
designDeletionError,
} from '../utils/error_messages';
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';
const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
......@@ -93,6 +97,9 @@ export default {
: s__('DesignManagement|Select all');
},
},
mounted() {
this.toggleOnPasteListener(this.$route.name);
},
methods: {
resetFilesToBeSaved() {
this.filesToBeSaved = [];
......@@ -215,11 +222,36 @@ export default {
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) {
this.toggleOnPasteListener(to.name);
this.selectedDesigns = [];
next();
},
beforeRouteLeave(to, from, next) {
this.toggleOnPasteListener(to.name);
next();
},
};
</script>
......
......@@ -96,6 +96,7 @@ describe('Design management index page', () => {
localVue,
router,
stubs: { DesignDestroyer, ApolloMutation, ...stubs },
attachToDocument: true,
});
wrapper.setData({
......@@ -469,4 +470,49 @@ describe('Design management index page', () => {
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', () => {
beforeEach(() => {
......@@ -62,3 +62,15 @@ describe('File upload', () => {
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