mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-54486] Copy pasting images from chrome fails (#24718)
This commit is contained in:
parent
2f5ca43158
commit
7caa5bce25
@ -8,8 +8,7 @@ import type {FileInfo} from '@mattermost/types/files';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import FileUpload from 'components/file_upload/file_upload';
|
||||
import type {FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
|
||||
import FileUpload, {type FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {clearFileInput} from 'utils/utils';
|
||||
@ -232,7 +231,11 @@ describe('components/FileUpload', () => {
|
||||
const event = new Event('paste');
|
||||
event.preventDefault = jest.fn();
|
||||
const getAsString = jest.fn();
|
||||
(event as any).clipboardData = {items: [{getAsString, kind: 'string', type: 'text/plain'}], types: ['text/plain'], getData: () => {}};
|
||||
(event as any).clipboardData = {items: [{getAsString, kind: 'string', type: 'text/plain'}],
|
||||
types: ['text/plain'],
|
||||
getData: () => {
|
||||
return '';
|
||||
}};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<FileUpload
|
||||
|
@ -24,6 +24,7 @@ import Constants from 'utils/constants';
|
||||
import DelayedAction from 'utils/delayed_action';
|
||||
import dragster from 'utils/dragster';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||
import {hasPlainText, createFileFromClipboardDataItem} from 'utils/paste';
|
||||
import {
|
||||
isIosChrome,
|
||||
isMobileApp,
|
||||
@ -60,10 +61,6 @@ const holders = defineMessages({
|
||||
id: 'file_upload.zeroBytesFile',
|
||||
defaultMessage: 'You are uploading an empty file: {filename}',
|
||||
},
|
||||
pasted: {
|
||||
id: 'file_upload.pasted',
|
||||
defaultMessage: 'Image Pasted at ',
|
||||
},
|
||||
uploadFile: {
|
||||
id: 'file_upload.upload_files',
|
||||
defaultMessage: 'Upload files',
|
||||
@ -447,10 +444,12 @@ export class FileUpload extends PureComponent<Props, State> {
|
||||
|
||||
containsEventTarget = (targetElement: HTMLInputElement | null, eventTarget: EventTarget | null) => targetElement && targetElement.contains(eventTarget as Node);
|
||||
|
||||
/**
|
||||
* This paste handler sole responsibility is to detect if the clipboard data contains "files" and pass them to the upload file handler.
|
||||
*/
|
||||
pasteUpload = (e: ClipboardEvent) => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
if (!e.clipboardData || !e.clipboardData.items || e.clipboardData.getData('text/html')) {
|
||||
// If the clipboard data doesn't contain anything or it contains plain text, do nothing and let the browser and other handlers do their thing.
|
||||
if (!e.clipboardData || !e.clipboardData.items || hasPlainText(e.clipboardData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -461,53 +460,28 @@ export class FileUpload extends PureComponent<Props, State> {
|
||||
|
||||
this.props.onUploadError(null);
|
||||
|
||||
const items = [];
|
||||
for (let i = 0; i < e.clipboardData.items.length; i++) {
|
||||
const item = e.clipboardData.items[i];
|
||||
const fileClipboardItems = Array.
|
||||
from(e.clipboardData.items).
|
||||
filter((item) => item.kind === 'file');
|
||||
|
||||
if (item.kind !== 'file') {
|
||||
continue;
|
||||
}
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
if (items && items.length > 0) {
|
||||
if (fileClipboardItems.length > 0) {
|
||||
if (!this.props.canUploadFiles) {
|
||||
this.props.onUploadError(localizeMessage('file_upload.disabled', 'File attachments are disabled.'));
|
||||
this.props.onUploadError(this.props.intl.formatMessage({id: 'file_upload.disabled', defaultMessage: 'File attachments are disabled.'}));
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const fileNamePrefixIfNoName = this.props.intl.formatMessage({id: 'file_upload.pasted', defaultMessage: 'Image Pasted at '});
|
||||
|
||||
const files = [];
|
||||
const fileList = fileClipboardItems.
|
||||
map((fileClipboardItem) => createFileFromClipboardDataItem(fileClipboardItem, fileNamePrefixIfNoName)).
|
||||
filter((file): file is NonNullable<typeof file> => file !== null);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const file = items[i].getAsFile();
|
||||
if (fileList.length > 0) {
|
||||
// Prevent default will stop event propagation to other handlers such as those in advanced text editor
|
||||
// so we do that here because we want to only paste the files from the clipboard and not other content.
|
||||
e.preventDefault();
|
||||
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const hour = now.getHours().toString().padStart(2, '0');
|
||||
const minute = now.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
let ext = '';
|
||||
if (file.name && file.name.includes('.')) {
|
||||
ext = file.name.substr(file.name.lastIndexOf('.'));
|
||||
} else if (items[i].type.includes('/')) {
|
||||
ext = '.' + items[i].type.split('/')[1].toLowerCase();
|
||||
}
|
||||
|
||||
const name = file.name || formatMessage(holders.pasted) + now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + ' ' + hour + '-' + minute + ext;
|
||||
|
||||
const newFile: File = new File([file], name, {type: file.type});
|
||||
files.push(newFile);
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
this.checkPluginHooksAndUploadFiles(files);
|
||||
this.checkPluginHooksAndUploadFiles(fileList);
|
||||
this.props.onFileUploadChange();
|
||||
}
|
||||
}
|
||||
@ -735,7 +709,6 @@ export class FileUpload extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<div className={uploadsRemaining <= 0 ? ' style--none btn-file__disabled' : 'style--none'}>
|
||||
{bodyAction}
|
||||
</div>
|
||||
|
@ -1,7 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {parseHtmlTable, getHtmlTable, formatMarkdownMessage, formatGithubCodePaste, formatMarkdownLinkMessage, isTextUrl} from './paste';
|
||||
import {
|
||||
parseHtmlTable,
|
||||
getHtmlTable,
|
||||
formatMarkdownMessage,
|
||||
formatGithubCodePaste,
|
||||
formatMarkdownLinkMessage,
|
||||
isTextUrl,
|
||||
hasPlainText,
|
||||
createFileFromClipboardDataItem,
|
||||
} from './paste';
|
||||
|
||||
const validClipboardData: any = {
|
||||
items: [1],
|
||||
@ -221,3 +230,122 @@ describe('isTextUrl', () => {
|
||||
expect(isTextUrl(clipboardData)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPlainText', () => {
|
||||
test('Should return true when clipboard data has plain text', () => {
|
||||
const clipboardData = {
|
||||
...validClipboardData,
|
||||
types: ['text/plain'],
|
||||
getData: () => {
|
||||
return 'plain text';
|
||||
},
|
||||
};
|
||||
|
||||
expect(hasPlainText(clipboardData)).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return true when clipboard data has plain text along with other types', () => {
|
||||
const clipboardData = {
|
||||
...validClipboardData,
|
||||
types: ['text/html', 'text/plain'],
|
||||
getData: () => {
|
||||
return 'plain text';
|
||||
},
|
||||
};
|
||||
|
||||
expect(hasPlainText(clipboardData)).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return false when clipboard data has empty text', () => {
|
||||
const clipboardData = {
|
||||
...validClipboardData,
|
||||
types: ['text/html', 'text/plain'],
|
||||
getData: () => {
|
||||
return '';
|
||||
},
|
||||
};
|
||||
|
||||
expect(hasPlainText(clipboardData)).toBe(false);
|
||||
});
|
||||
|
||||
test('Should return false when clipboard data doesnt not have plain text type', () => {
|
||||
const clipboardData = {
|
||||
...validClipboardData,
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return 'plain text without type';
|
||||
},
|
||||
};
|
||||
|
||||
expect(hasPlainText(clipboardData)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFileFromClipboardDataItem', () => {
|
||||
test('should return a file from a clipboard item', () => {
|
||||
const item = {
|
||||
getAsFile: jest.fn(() => ({
|
||||
name: 'test1.png',
|
||||
type: 'image/png',
|
||||
})),
|
||||
type: 'image/png',
|
||||
} as unknown as DataTransferItem;
|
||||
|
||||
const file = createFileFromClipboardDataItem(item, '') as File;
|
||||
expect(file).toBeInstanceOf(File);
|
||||
expect(file.name).toEqual('test1.png');
|
||||
expect(file.type).toEqual('image/png');
|
||||
});
|
||||
|
||||
test('should return null if getAsFile is not a file', () => {
|
||||
const item = {
|
||||
getAsFile: jest.fn(() => null),
|
||||
} as unknown as DataTransferItem;
|
||||
|
||||
const file = createFileFromClipboardDataItem(item, '');
|
||||
expect(file).toBeNull();
|
||||
});
|
||||
|
||||
test('Should return correct file name when file name is not available', () => {
|
||||
const item = {
|
||||
getAsFile: jest.fn(() => ({
|
||||
type: 'image/jpeg',
|
||||
})),
|
||||
type: 'image/jpeg',
|
||||
} as unknown as DataTransferItem;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const file = createFileFromClipboardDataItem(item, 'pasted') as File;
|
||||
|
||||
expect(file).toBeInstanceOf(File);
|
||||
expect(file.name).toBe(`pasted${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${now.getHours().toString().padStart(2, '0')}-${now.getMinutes().toString().padStart(2, '0')}.jpeg`);
|
||||
expect(file.type).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
test('Should return correct file extension when file name contains extension', () => {
|
||||
const item = {
|
||||
getAsFile: jest.fn(() => ({
|
||||
name: 'test.jpeg',
|
||||
})),
|
||||
type: 'image/jpeg',
|
||||
} as unknown as DataTransferItem;
|
||||
|
||||
const file = createFileFromClipboardDataItem(item, 'pasted') as File;
|
||||
|
||||
expect(file.name).toContain('.jpeg');
|
||||
});
|
||||
|
||||
test('Should return correct file extension when file name doesnt contains extension', () => {
|
||||
const item = {
|
||||
getAsFile: jest.fn(() => ({
|
||||
type: 'image/JPEG',
|
||||
})),
|
||||
type: 'image/jpeg',
|
||||
} as unknown as DataTransferItem;
|
||||
|
||||
const file = createFileFromClipboardDataItem(item, 'pasted') as File;
|
||||
|
||||
expect(file.name).toContain('.jpeg');
|
||||
});
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ export function parseHtmlTable(html: string): HTMLTableElement | null {
|
||||
}
|
||||
|
||||
export function getHtmlTable(clipboardData: DataTransfer): HTMLTableElement | null {
|
||||
// Check if clipboard data has html as one of its types
|
||||
if (Array.from(clipboardData.types).indexOf('text/html') === -1) {
|
||||
return null;
|
||||
}
|
||||
@ -42,6 +43,18 @@ export function isTextUrl(clipboardData: DataTransfer): boolean {
|
||||
return clipboardText.startsWith('http://') || clipboardText.startsWith('https://');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the clipboard data contains plain text from list of types.
|
||||
**/
|
||||
export function hasPlainText(clipboardData: DataTransfer): boolean {
|
||||
if (Array.from(clipboardData.types).includes('text/plain')) {
|
||||
const clipboardText = clipboardData.getData('text/plain');
|
||||
|
||||
return clipboardText.trim().length > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isTableWithoutHeaderRow(table: HTMLTableElement): boolean {
|
||||
return table.querySelectorAll('th').length === 0;
|
||||
}
|
||||
@ -140,3 +153,34 @@ export function formatMarkdownLinkMessage({message, clipboardData, selectionStar
|
||||
const markdownLink = `[${selectedText}](${clipboardUrl})`;
|
||||
return markdownLink;
|
||||
}
|
||||
|
||||
export function createFileFromClipboardDataItem(item: DataTransferItem, fileNamePrefixIfNoName: string): File | null {
|
||||
const file = item.getAsFile();
|
||||
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let ext = '';
|
||||
if (file.name && file.name.includes('.')) {
|
||||
ext = file.name.slice(file.name.lastIndexOf('.'));
|
||||
} else if (item.type.includes('/')) {
|
||||
ext = '.' + item.type.slice(item.type.lastIndexOf('/') + 1).toLowerCase();
|
||||
}
|
||||
|
||||
let name = '';
|
||||
if (file.name) {
|
||||
name = file.name;
|
||||
} else {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
const date = now.getDate();
|
||||
const hour = now.getHours().toString().padStart(2, '0');
|
||||
const minute = now.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
name = `${fileNamePrefixIfNoName}${year}-${month}-${date} ${hour}-${minute}${ext}`;
|
||||
}
|
||||
|
||||
return new File([file as Blob], name, {type: file.type});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user