refactor: Upload use batch start (#29474)

* chore: Update Upload version

* useMergedStatus

* fix file list parser

* fix list replacement

* bump rc-upload

* add Upload.LIST_IGNORE

* support LIST_IGNORE

* fix uploadList batch test

* test: fix batch case test case changed

* test: Finish test case

* fix: Not change uid if exist

* use proxy

* fix proxy

* fix ts

* part test case check

* more test back

* back of test all

* update test case

* more unmount

* adjust test cae

* add unmount
This commit is contained in:
二货机器人 2021-02-26 15:18:37 +08:00 committed by GitHub
parent 5d22b0ef7a
commit 79f8fece4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 274 additions and 170 deletions

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import RcUpload from 'rc-upload'; import RcUpload, { UploadProps as RcUploadProps } from 'rc-upload';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import classNames from 'classnames'; import classNames from 'classnames';
import Dragger from './Dragger'; import Dragger from './Dragger';
import UploadList from './UploadList'; import UploadList from './UploadList';
@ -13,19 +14,19 @@ import {
UploadType, UploadType,
UploadListType, UploadListType,
} from './interface'; } from './interface';
import { T, fileToObject, getFileItem, removeFileItem } from './utils'; import { T, wrapFile, getFileItem, removeFileItem } from './utils';
import LocaleReceiver from '../locale-provider/LocaleReceiver'; import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default'; import defaultLocale from '../locale/default';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning'; import devWarning from '../_util/devWarning';
import useForceUpdate from '../_util/hooks/useForceUpdate';
import useFreshState from './useFreshState'; const LIST_IGNORE = `__LIST_IGNORE_${Date.now()}__`;
export { UploadProps }; export { UploadProps };
const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (props, ref) => { const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (props, ref) => {
const { const {
fileList: fileListProp, fileList,
defaultFileList, defaultFileList,
onRemove, onRemove,
showUploadList, showUploadList,
@ -48,14 +49,11 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
maxCount, maxCount,
} = props; } = props;
const [dragState, setDragState] = React.useState<string>('drop'); const [mergedFileList, setMergedFileList] = useMergedState(defaultFileList || [], {
const forceUpdate = useForceUpdate(); value: fileList,
});
// Refresh always use fresh data const [dragState, setDragState] = React.useState<string>('drop');
const [getFileList, setFileList] = useFreshState<UploadFile<any>[]>(
fileListProp || defaultFileList || [],
fileListProp,
);
const upload = React.useRef<any>(); const upload = React.useRef<any>();
@ -77,13 +75,19 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
React.useEffect(() => { React.useEffect(() => {
const timestamp = Date.now(); const timestamp = Date.now();
(fileListProp || []).forEach((file, index) => { (fileList || []).forEach((file, index) => {
file.uid = file.uid ?? `__AUTO__${timestamp}_${index}__`; if (!file.uid) {
file.uid = `__AUTO__${timestamp}_${index}__`;
}
}); });
}, [fileListProp]); }, [fileList]);
const onInternalChange = (info: UploadChangeParam) => { const onInternalChange = (
let cloneList = [...info.fileList]; file: UploadFile,
changedFileList: UploadFile[],
event?: { percent: number },
) => {
let cloneList = [...changedFileList];
// Cut to match count // Cut to match count
if (maxCount === 1) { if (maxCount === 1) {
@ -92,19 +96,47 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
cloneList = cloneList.slice(0, maxCount); cloneList = cloneList.slice(0, maxCount);
} }
setFileList(cloneList); setMergedFileList(cloneList);
onChange?.({ const changeInfo: UploadChangeParam = {
...info, file,
fileList: cloneList, fileList: cloneList,
};
if (event) {
changeInfo.event = event;
}
onChange?.(changeInfo);
};
const onBatchStart: RcUploadProps['onBatchStart'] = batchFileInfoList => {
// Skip file which marked as `LIST_IGNORE`, these file will not add to file list
const filteredFileInfoList = batchFileInfoList.filter(info => !(info.file as any)[LIST_IGNORE]);
// Nothing to do since no file need upload
if (!filteredFileInfoList.length) {
return;
}
const objectFileList = filteredFileInfoList.map(info => wrapFile(info.file));
// Concat new files with prev files
const newFileList = [...mergedFileList];
objectFileList.forEach(fileObj => {
if (newFileList.every(existFile => existFile.uid !== fileObj.uid)) {
newFileList.push(fileObj);
}
}); });
onInternalChange(filteredFileInfoList[0]?.file, newFileList);
}; };
const onStart = (file: RcFile) => { const onStart = (file: RcFile) => {
const targetItem = fileToObject(file); const targetItem = wrapFile(file);
targetItem.status = 'uploading'; targetItem.status = 'uploading';
const nextFileList = getFileList().concat(); const nextFileList = [...mergedFileList];
const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid); const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid);
if (fileIndex === -1) { if (fileIndex === -1) {
@ -113,10 +145,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
nextFileList[fileIndex] = targetItem; nextFileList[fileIndex] = targetItem;
} }
onInternalChange({ onInternalChange(targetItem, nextFileList);
file: targetItem,
fileList: nextFileList,
});
}; };
const onSuccess = (response: any, file: UploadFile, xhr: any) => { const onSuccess = (response: any, file: UploadFile, xhr: any) => {
@ -127,7 +156,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
} catch (e) { } catch (e) {
/* do nothing */ /* do nothing */
} }
const targetItem = getFileItem(file, getFileList()); const targetItem = getFileItem(file, mergedFileList);
// removed // removed
if (!targetItem) { if (!targetItem) {
return; return;
@ -135,28 +164,21 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
targetItem.status = 'done'; targetItem.status = 'done';
targetItem.response = response; targetItem.response = response;
targetItem.xhr = xhr; targetItem.xhr = xhr;
onInternalChange({ onInternalChange(wrapFile(targetItem), [...mergedFileList]);
file: { ...targetItem },
fileList: getFileList().concat(),
});
}; };
const onProgress = (e: { percent: number }, file: UploadFile) => { const onProgress = (e: { percent: number }, file: UploadFile) => {
const targetItem = getFileItem(file, getFileList()); const targetItem = getFileItem(file, mergedFileList);
// removed // removed
if (!targetItem) { if (!targetItem) {
return; return;
} }
targetItem.percent = e.percent; targetItem.percent = e.percent;
onInternalChange({ onInternalChange(wrapFile(targetItem), [...mergedFileList], e);
event: e,
file: { ...targetItem },
fileList: getFileList().concat(),
});
}; };
const onError = (error: Error, response: any, file: UploadFile) => { const onError = (error: Error, response: any, file: UploadFile) => {
const targetItem = getFileItem(file, getFileList()); const targetItem = getFileItem(file, mergedFileList);
// removed // removed
if (!targetItem) { if (!targetItem) {
return; return;
@ -164,10 +186,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
targetItem.error = error; targetItem.error = error;
targetItem.response = response; targetItem.response = response;
targetItem.status = 'error'; targetItem.status = 'error';
onInternalChange({ onInternalChange(wrapFile(targetItem), [...mergedFileList]);
file: { ...targetItem },
fileList: getFileList().concat(),
});
}; };
const handleRemove = (file: UploadFile) => { const handleRemove = (file: UploadFile) => {
@ -178,12 +197,12 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
return; return;
} }
const fileList = getFileList(); const removedFileList = removeFileItem(file, mergedFileList);
const removedFileList = removeFileItem(file, fileList);
if (removedFileList) { if (removedFileList) {
currentFile = { ...file, status: 'removed' }; currentFile = wrapFile(file);
fileList?.forEach(item => { currentFile.status = 'removed';
mergedFileList?.forEach(item => {
const matchKey = currentFile.uid !== undefined ? 'uid' : 'name'; const matchKey = currentFile.uid !== undefined ? 'uid' : 'name';
if (item[matchKey] === currentFile[matchKey]) { if (item[matchKey] === currentFile[matchKey]) {
item.status = 'removed'; item.status = 'removed';
@ -191,10 +210,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
}); });
upload.current?.abort(currentFile); upload.current?.abort(currentFile);
onInternalChange({ onInternalChange(currentFile, removedFileList);
file: currentFile,
fileList: removedFileList,
});
} }
}); });
}; };
@ -203,43 +219,47 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
setDragState(e.type); setDragState(e.type);
}; };
const beforeUpload = (file: RcFile, fileListArgs: RcFile[]) => { const mergedBeforeUpload = async (file: RcFile, fileListArgs: RcFile[]) => {
const { beforeUpload: beforeUploadProp } = props; const { beforeUpload, transformFile } = props;
if (!beforeUploadProp) {
return true;
}
const result = beforeUploadProp(file, fileListArgs);
if (result === false) {
// Get unique file list
const uniqueList: UploadFile<any>[] = [];
getFileList()
.concat(fileListArgs.map(fileToObject))
.forEach(f => {
if (uniqueList.every(uf => uf.uid !== f.uid)) {
uniqueList.push(f);
}
});
onInternalChange({ let parsedFile: File | Blob | string = file;
file, if (beforeUpload) {
fileList: uniqueList, const result = await beforeUpload(file, fileListArgs);
});
return false; if (result === false) {
return false;
}
// Hack for LIST_IGNORE, we add additional info to remove from the list
delete (file as any)[LIST_IGNORE];
if ((result as any) === LIST_IGNORE) {
Object.defineProperty(file, LIST_IGNORE, {
value: true,
configurable: true,
});
return false;
}
if (typeof result === 'object' && result) {
parsedFile = result as File;
}
} }
if (result && (result as PromiseLike<any>).then) {
return result; if (transformFile) {
parsedFile = await transformFile(parsedFile as any);
} }
return true;
return parsedFile as RcFile;
}; };
// Test needs // Test needs
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
onStart, onStart,
onSuccess, onSuccess,
onProgress, onProgress,
onError, onError,
fileList: getFileList(), fileList: mergedFileList,
upload: upload.current, upload: upload.current,
forceUpdate,
})); }));
const { getPrefixCls, direction } = React.useContext(ConfigContext); const { getPrefixCls, direction } = React.useContext(ConfigContext);
@ -247,13 +267,14 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
const prefixCls = getPrefixCls('upload', customizePrefixCls); const prefixCls = getPrefixCls('upload', customizePrefixCls);
const rcUploadProps = { const rcUploadProps = {
onBatchStart,
onStart, onStart,
onError, onError,
onProgress, onProgress,
onSuccess, onSuccess,
...props, ...props,
prefixCls, prefixCls,
beforeUpload, beforeUpload: mergedBeforeUpload,
onChange: undefined, onChange: undefined,
}; };
@ -277,7 +298,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
return ( return (
<UploadList <UploadList
listType={listType} listType={listType}
items={getFileList(true)} items={mergedFileList}
previewFile={previewFile} previewFile={previewFile}
onPreview={onPreview} onPreview={onPreview}
onDownload={onDownload} onDownload={onDownload}
@ -306,7 +327,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
prefixCls, prefixCls,
{ {
[`${prefixCls}-drag`]: true, [`${prefixCls}-drag`]: true,
[`${prefixCls}-drag-uploading`]: getFileList().some(file => file.status === 'uploading'), [`${prefixCls}-drag-uploading`]: mergedFileList.some(file => file.status === 'uploading'),
[`${prefixCls}-drag-hover`]: dragState === 'dragover', [`${prefixCls}-drag-hover`]: dragState === 'dragover',
[`${prefixCls}-disabled`]: disabled, [`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: direction === 'rtl',
@ -365,12 +386,15 @@ interface CompoundedComponent
React.PropsWithChildren<UploadProps> & React.RefAttributes<any> React.PropsWithChildren<UploadProps> & React.RefAttributes<any>
> { > {
Dragger: typeof Dragger; Dragger: typeof Dragger;
LIST_IGNORE: {};
} }
const Upload = React.forwardRef<unknown, UploadProps>(InternalUpload) as CompoundedComponent; const Upload = React.forwardRef<unknown, UploadProps>(InternalUpload) as CompoundedComponent;
Upload.Dragger = Dragger; Upload.Dragger = Dragger;
Upload.LIST_IGNORE = LIST_IGNORE;
Upload.displayName = 'Upload'; Upload.displayName = 'Upload';
Upload.defaultProps = { Upload.defaultProps = {

View File

@ -5,7 +5,7 @@ import { act } from 'react-dom/test-utils';
import produce from 'immer'; import produce from 'immer';
import Upload from '..'; import Upload from '..';
import Form from '../../form'; import Form from '../../form';
import { T, fileToObject, getFileItem, removeFileItem } from '../utils'; import { T, wrapFile, getFileItem, removeFileItem } from '../utils';
import { setup, teardown } from './mock'; import { setup, teardown } from './mock';
import { resetWarned } from '../../_util/devWarning'; import { resetWarned } from '../../_util/devWarning';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
@ -291,11 +291,29 @@ describe('Upload', () => {
expect(res).toBe(true); expect(res).toBe(true);
}); });
it('should be able to copy file instance', () => { describe('wrapFile', () => {
const file = new File([], 'aaa.zip'); it('should be able to copy file instance when Proxy not support', () => {
const copiedFile = fileToObject(file); const file = new File([], 'aaa.zip');
['uid', 'lastModified', 'lastModifiedDate', 'name', 'size', 'type'].forEach(key => {
expect(key in copiedFile).toBe(true); const OriginProxy = global.Proxy;
global.Proxy = undefined;
const copiedFile = wrapFile(file);
['uid', 'lastModified', 'lastModifiedDate', 'name', 'size', 'type'].forEach(key => {
expect(key in copiedFile).toBe(true);
});
global.Proxy = OriginProxy;
});
it('Proxy support', () => {
const file = new File([], 'aaa.zip');
const copiedFile = wrapFile(file);
console.log(Object.keys(copiedFile));
['uid', 'lastModified', 'lastModifiedDate', 'name', 'size', 'type'].forEach(key => {
expect(key in copiedFile).toBe(true);
});
}); });
}); });
@ -617,15 +635,20 @@ describe('Upload', () => {
switch (callTimes) { switch (callTimes) {
case 1: case 1:
expect(file.status).toBe(undefined);
break;
case 2: case 2:
case 3:
console.log('===>', file.status);
expect(file).toEqual(expect.objectContaining({ status: 'uploading', percent: 0 })); expect(file).toEqual(expect.objectContaining({ status: 'uploading', percent: 0 }));
break; break;
case 3: case 4:
expect(file).toEqual(expect.objectContaining({ status: 'uploading', percent: 100 })); expect(file).toEqual(expect.objectContaining({ status: 'uploading', percent: 100 }));
break; break;
case 4: case 5:
expect(file).toEqual(expect.objectContaining({ status: 'done', percent: 100 })); expect(file).toEqual(expect.objectContaining({ status: 'done', percent: 100 }));
break; break;

View File

@ -104,6 +104,7 @@ describe('Upload List', () => {
expect(linkNode.prop('href')).toBe(file.url); expect(linkNode.prop('href')).toBe(file.url);
expect(imgNode.prop('src')).toBe(file.thumbUrl); expect(imgNode.prop('src')).toBe(file.thumbUrl);
}); });
wrapper.unmount();
}); });
// https://github.com/ant-design/ant-design/issues/7269 // https://github.com/ant-design/ant-design/issues/7269
@ -145,6 +146,8 @@ describe('Upload List', () => {
// console.log(wrapper.html()); // console.log(wrapper.html());
expect(wrapper.find('.ant-upload-list-text-container').hostNodes().length).toBe(1); expect(wrapper.find('.ant-upload-list-text-container').hostNodes().length).toBe(1);
wrapper.unmount();
}); });
it('should be uploading when upload a file', done => { it('should be uploading when upload a file', done => {
@ -157,6 +160,7 @@ describe('Upload List', () => {
} }
if (file.status === 'done') { if (file.status === 'done') {
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
wrapper.unmount();
done(); done();
} }
@ -181,8 +185,9 @@ describe('Upload List', () => {
it('handle error', done => { it('handle error', done => {
let wrapper; let wrapper;
const onChange = ({ file }) => { const onChange = ({ file }) => {
if (file.status !== 'uploading') { if (file.status === 'error') {
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
wrapper.unmount();
done(); done();
} }
}; };
@ -202,7 +207,7 @@ describe('Upload List', () => {
}); });
}); });
it('does concat filelist when beforeUpload returns false', () => { it('does concat fileList when beforeUpload returns false', async () => {
const handleChange = jest.fn(); const handleChange = jest.fn();
const ref = React.createRef(); const ref = React.createRef();
const wrapper = mount( const wrapper = mount(
@ -222,11 +227,17 @@ describe('Upload List', () => {
}, },
}); });
await sleep();
expect(ref.current.fileList.length).toBe(fileList.length + 1); expect(ref.current.fileList.length).toBe(fileList.length + 1);
expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3); expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3);
wrapper.unmount();
}); });
it('In the case of listType=picture, the error status does not show the download.', () => { it('In the case of listType=picture, the error status does not show the download.', () => {
global.testName =
'In the case of listType=picture, the error status does not show the download.';
const file = { status: 'error', uid: 'file' }; const file = { status: 'error', uid: 'file' };
const wrapper = mount( const wrapper = mount(
<Upload listType="picture" fileList={[file]} showUploadList={{ showDownloadIcon: true }}> <Upload listType="picture" fileList={[file]} showUploadList={{ showDownloadIcon: true }}>
@ -238,9 +249,13 @@ describe('Upload List', () => {
wrapper.find('.ant-upload-list-item-error').simulate('mouseenter'); wrapper.find('.ant-upload-list-item-error').simulate('mouseenter');
expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0); expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0);
wrapper.unmount();
}); });
it('In the case of listType=picture-card, the error status does not show the download.', () => { it('In the case of listType=picture-card, the error status does not show the download.', () => {
global.testName =
'In the case of listType=picture-card, the error status does not show the download.';
const file = { status: 'error', uid: 'file' }; const file = { status: 'error', uid: 'file' };
const wrapper = mount( const wrapper = mount(
<Upload listType="picture-card" fileList={[file]} showUploadList={{ showDownloadIcon: true }}> <Upload listType="picture-card" fileList={[file]} showUploadList={{ showDownloadIcon: true }}>
@ -248,6 +263,8 @@ describe('Upload List', () => {
</Upload>, </Upload>,
); );
expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0); expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0);
wrapper.unmount();
}); });
it('In the case of listType=text, the error status does not show the download.', () => { it('In the case of listType=text, the error status does not show the download.', () => {
@ -258,6 +275,8 @@ describe('Upload List', () => {
</Upload>, </Upload>,
); );
expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0); expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0);
wrapper.unmount();
}); });
it('should support onPreview', () => { it('should support onPreview', () => {
@ -271,6 +290,8 @@ describe('Upload List', () => {
expect(handlePreview).toHaveBeenCalledWith(fileList[0]); expect(handlePreview).toHaveBeenCalledWith(fileList[0]);
wrapper.find('.anticon-eye').at(1).simulate('click'); wrapper.find('.anticon-eye').at(1).simulate('click');
expect(handlePreview).toHaveBeenCalledWith(fileList[1]); expect(handlePreview).toHaveBeenCalledWith(fileList[1]);
wrapper.unmount();
}); });
it('should support onRemove', async () => { it('should support onRemove', async () => {
@ -292,6 +313,8 @@ describe('Upload List', () => {
expect(handleRemove).toHaveBeenCalledWith(fileList[1]); expect(handleRemove).toHaveBeenCalledWith(fileList[1]);
await sleep(); await sleep();
expect(handleChange.mock.calls.length).toBe(2); expect(handleChange.mock.calls.length).toBe(2);
wrapper.unmount();
}); });
it('should support onDownload', async () => { it('should support onDownload', async () => {
@ -316,6 +339,8 @@ describe('Upload List', () => {
</Upload>, </Upload>,
); );
wrapper.find('.anticon-download').at(0).simulate('click'); wrapper.find('.anticon-download').at(0).simulate('click');
wrapper.unmount();
}); });
it('should support no onDownload', async () => { it('should support no onDownload', async () => {
@ -338,6 +363,8 @@ describe('Upload List', () => {
</Upload>, </Upload>,
); );
wrapper.find('.anticon-download').at(0).simulate('click'); wrapper.find('.anticon-download').at(0).simulate('click');
wrapper.unmount();
}); });
describe('should generate thumbUrl from file', () => { describe('should generate thumbUrl from file', () => {
@ -360,7 +387,7 @@ describe('Upload List', () => {
delete newFile.thumbUrl; delete newFile.thumbUrl;
newFileList.push(newFile); newFileList.push(newFile);
const ref = React.createRef(); const ref = React.createRef();
mount( const wrapper = mount(
<Upload <Upload
ref={ref} ref={ref}
listType="picture-card" listType="picture-card"
@ -370,7 +397,7 @@ describe('Upload List', () => {
<button type="button">upload</button> <button type="button">upload</button>
</Upload>, </Upload>,
); );
ref.current.forceUpdate(); wrapper.update();
await sleep(); await sleep();
expect(ref.current.fileList[2].thumbUrl).not.toBe(undefined); expect(ref.current.fileList[2].thumbUrl).not.toBe(undefined);
@ -383,6 +410,8 @@ describe('Upload List', () => {
} else { } else {
expect(offsetY === 0).toBeTruthy(); expect(offsetY === 0).toBeTruthy();
} }
wrapper.unmount();
}); });
}); });
}); });
@ -462,6 +491,8 @@ describe('Upload List', () => {
</Upload>, </Upload>,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
wrapper.unmount();
}); });
it('should support showRemoveIcon and showPreviewIcon', () => { it('should support showRemoveIcon and showPreviewIcon', () => {
@ -493,6 +524,8 @@ describe('Upload List', () => {
</Upload>, </Upload>,
); );
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
wrapper.unmount();
}); });
it('should support custom onClick in custom icon', async () => { it('should support custom onClick in custom icon', async () => {
@ -525,6 +558,8 @@ describe('Upload List', () => {
expect(myClick).toHaveBeenCalled(); expect(myClick).toHaveBeenCalled();
await sleep(); await sleep();
expect(handleChange.mock.calls.length).toBe(2); expect(handleChange.mock.calls.length).toBe(2);
wrapper.unmount();
}); });
it('should support removeIcon and downloadIcon', () => { it('should support removeIcon and downloadIcon', () => {
@ -574,6 +609,9 @@ describe('Upload List', () => {
</Upload>, </Upload>,
); );
expect(wrapper2.render()).toMatchSnapshot(); expect(wrapper2.render()).toMatchSnapshot();
wrapper.unmount();
wrapper2.unmount();
}); });
// https://github.com/ant-design/ant-design/issues/7762 // https://github.com/ant-design/ant-design/issues/7762
@ -626,19 +664,22 @@ describe('Upload List', () => {
wrapper.find(Form).simulate('submit'); wrapper.find(Form).simulate('submit');
await sleep(); await sleep();
expect(formRef.getFieldError(['file'])).toEqual([]); expect(formRef.getFieldError(['file'])).toEqual([]);
wrapper.unmount();
}); });
it('return when prop onPreview not exists', () => { it('return when prop onPreview not exists', () => {
const ref = React.createRef(); const ref = React.createRef();
mount(<UploadList ref={ref} />); const wrapper = mount(<UploadList ref={ref} />);
expect(ref.current.handlePreview()).toBe(undefined); expect(ref.current.handlePreview()).toBe(undefined);
wrapper.unmount();
}); });
it('return when prop onDownload not exists', () => { it('return when prop onDownload not exists', () => {
const file = new File([''], 'test.txt', { type: 'text/plain' }); const file = new File([''], 'test.txt', { type: 'text/plain' });
const items = [{ uid: 'upload-list-item', url: '' }]; const items = [{ uid: 'upload-list-item', url: '' }];
const ref = React.createRef(); const ref = React.createRef();
mount( const wrapper = mount(
<UploadList <UploadList
ref={ref} ref={ref}
items={items} items={items}
@ -647,6 +688,7 @@ describe('Upload List', () => {
/>, />,
); );
expect(ref.current.handleDownload(file)).toBe(undefined); expect(ref.current.handleDownload(file)).toBe(undefined);
wrapper.unmount();
}); });
it('previewFile should work correctly', async () => { it('previewFile should work correctly', async () => {
@ -655,7 +697,9 @@ describe('Upload List', () => {
const wrapper = mount( const wrapper = mount(
<UploadList listType="picture-card" items={items} locale={{ previewFile: '' }} />, <UploadList listType="picture-card" items={items} locale={{ previewFile: '' }} />,
); );
return wrapper.props().previewFile(file); expect(wrapper.props().previewFile(file)).toBeTruthy();
wrapper.unmount();
}); });
it('downloadFile should work correctly', async () => { it('downloadFile should work correctly', async () => {
@ -670,7 +714,11 @@ describe('Upload List', () => {
showUploadList={{ showDownloadIcon: true }} showUploadList={{ showDownloadIcon: true }}
/>, />,
); );
return wrapper.props().onDownload(file);
// Not throw
wrapper.props().onDownload(file);
wrapper.unmount();
}); });
it('extname should work correctly when url not exists', () => { it('extname should work correctly when url not exists', () => {
@ -679,6 +727,7 @@ describe('Upload List', () => {
<UploadList listType="picture-card" items={items} locale={{ previewFile: '' }} />, <UploadList listType="picture-card" items={items} locale={{ previewFile: '' }} />,
); );
expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(1); expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(1);
wrapper.unmount();
}); });
it('extname should work correctly when url exists', done => { it('extname should work correctly when url exists', done => {
@ -688,6 +737,7 @@ describe('Upload List', () => {
listType="picture" listType="picture"
onDownload={file => { onDownload={file => {
expect(file.url).toBe('/example'); expect(file.url).toBe('/example');
wrapper.unmount();
done(); done();
}} }}
items={items} items={items}
@ -705,6 +755,8 @@ describe('Upload List', () => {
); );
expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(1); expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(1);
expect(wrapper.find('.ant-upload-list-item-thumbnail').text()).toBe('uploading'); expect(wrapper.find('.ant-upload-list-item-thumbnail').text()).toBe('uploading');
wrapper.unmount();
}); });
it('onPreview should be called, when url exists', () => { it('onPreview should be called, when url exists', () => {
@ -725,6 +777,8 @@ describe('Upload List', () => {
wrapper.setProps({ items: [{ thumbUrl: 'thumbUrl', uid: 'upload-list-item' }] }); wrapper.setProps({ items: [{ thumbUrl: 'thumbUrl', uid: 'upload-list-item' }] });
wrapper.find('.ant-upload-list-item-name').simulate('click'); wrapper.find('.ant-upload-list-item-name').simulate('click');
expect(onPreview).toHaveBeenCalled(); expect(onPreview).toHaveBeenCalled();
wrapper.unmount();
}); });
it('upload image file should be converted to the base64', async () => { it('upload image file should be converted to the base64', async () => {
@ -735,12 +789,14 @@ describe('Upload List', () => {
const wrapper = mount( const wrapper = mount(
<UploadList listType="picture-card" items={fileList} locale={{ uploading: 'uploading' }} />, <UploadList listType="picture-card" items={fileList} locale={{ uploading: 'uploading' }} />,
); );
return wrapper await wrapper
.props() .props()
.previewFile(mockFile) .previewFile(mockFile)
.then(dataUrl => { .then(dataUrl => {
expect(dataUrl).toEqual('data:image/png;base64,'); expect(dataUrl).toEqual('data:image/png;base64,');
}); });
wrapper.unmount();
}); });
it("upload non image file shouldn't be converted to the base64", async () => { it("upload non image file shouldn't be converted to the base64", async () => {
@ -751,12 +807,14 @@ describe('Upload List', () => {
const wrapper = mount( const wrapper = mount(
<UploadList listType="picture-card" items={fileList} locale={{ uploading: 'uploading' }} />, <UploadList listType="picture-card" items={fileList} locale={{ uploading: 'uploading' }} />,
); );
return wrapper await wrapper
.props() .props()
.previewFile(mockFile) .previewFile(mockFile)
.then(dataUrl => { .then(dataUrl => {
expect(dataUrl).toBe(''); expect(dataUrl).toBe('');
}); });
wrapper.unmount();
}); });
describe('customize previewFile support', () => { describe('customize previewFile support', () => {
@ -775,12 +833,14 @@ describe('Upload List', () => {
<button type="button">button</button> <button type="button">button</button>
</Upload>, </Upload>,
); );
ref.current.forceUpdate(); wrapper.update();
expect(previewFile).toHaveBeenCalledWith(file.originFileObj); expect(previewFile).toHaveBeenCalledWith(file.originFileObj);
await sleep(100); await sleep(100);
wrapper.update(); wrapper.update();
expect(wrapper.find('.ant-upload-list-item-thumbnail img').prop('src')).toBe(mockThumbnail); expect(wrapper.find('.ant-upload-list-item-thumbnail img').prop('src')).toBe(mockThumbnail);
wrapper.unmount();
}); });
} }
test('File', () => new File([], 'xxx.png')); test('File', () => new File([], 'xxx.png'));
@ -808,6 +868,8 @@ describe('Upload List', () => {
); );
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img');
expect(imgNode.length).toBe(2); expect(imgNode.length).toBe(2);
wrapper.unmount();
}); });
it('should render <img /> when custom imageUrl return true', () => { it('should render <img /> when custom imageUrl return true', () => {
const isImageUrl = jest.fn(() => true); const isImageUrl = jest.fn(() => true);
@ -819,6 +881,8 @@ describe('Upload List', () => {
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img');
expect(isImageUrl).toHaveBeenCalled(); expect(isImageUrl).toHaveBeenCalled();
expect(imgNode.length).toBe(3); expect(imgNode.length).toBe(3);
wrapper.unmount();
}); });
it('should not render <img /> when custom imageUrl return false', () => { it('should not render <img /> when custom imageUrl return false', () => {
const isImageUrl = jest.fn(() => false); const isImageUrl = jest.fn(() => false);
@ -830,6 +894,8 @@ describe('Upload List', () => {
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img');
expect(isImageUrl).toHaveBeenCalled(); expect(isImageUrl).toHaveBeenCalled();
expect(imgNode.length).toBe(0); expect(imgNode.length).toBe(0);
wrapper.unmount();
}); });
}); });
@ -897,9 +963,13 @@ describe('Upload List', () => {
}); });
const afterImgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); const afterImgNode = wrapper.find('.ant-upload-list-item-thumbnail img');
expect(afterImgNode.length).toBeTruthy(); expect(afterImgNode.length).toBeTruthy();
wrapper.unmount();
}); });
it('should not render <img /> when upload non-image file without thumbUrl in onChange', done => { it('should not render <img /> when upload non-image file without thumbUrl in onChange', done => {
global.testName =
'should not render <img /> when upload non-image file without thumbUrl in onChange';
let wrapper; let wrapper;
const onChange = async ({ fileList: files }) => { const onChange = async ({ fileList: files }) => {
wrapper.setProps({ fileList: files }); wrapper.setProps({ fileList: files });
@ -907,6 +977,7 @@ describe('Upload List', () => {
await sleep(); await sleep();
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img');
expect(imgNode.length).toBe(0); expect(imgNode.length).toBe(0);
done(); done();
}; };
wrapper = mount( wrapper = mount(
@ -927,14 +998,17 @@ describe('Upload List', () => {
}); });
it('[deprecated] should support transformFile', done => { it('[deprecated] should support transformFile', done => {
let wrapper;
const handleTransformFile = jest.fn(); const handleTransformFile = jest.fn();
const onChange = ({ file }) => { const onChange = ({ file }) => {
if (file.status === 'done') { if (file.status === 'done') {
expect(handleTransformFile).toHaveBeenCalled(); expect(handleTransformFile).toHaveBeenCalled();
wrapper.unmount();
done(); done();
} }
}; };
const wrapper = mount( wrapper = mount(
<Upload <Upload
action="http://jsonplaceholder.typicode.com/posts/" action="http://jsonplaceholder.typicode.com/posts/"
transformFile={handleTransformFile} transformFile={handleTransformFile}
@ -972,6 +1046,8 @@ describe('Upload List', () => {
expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(true); expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(true);
wrapper.setProps({ showUploadList: false }); wrapper.setProps({ showUploadList: false });
expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(false); expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(false);
wrapper.unmount();
}); });
// https://github.com/ant-design/ant-design/issues/26536 // https://github.com/ant-design/ant-design/issues/26536
@ -998,7 +1074,7 @@ describe('Upload List', () => {
); );
}; };
mount(<MyUpload />); const wrapper = mount(<MyUpload />);
// Mock async update in a frame // Mock async update in a frame
const files = ['light', 'bamboo', 'little']; const files = ['light', 'bamboo', 'little'];
@ -1018,6 +1094,8 @@ describe('Upload List', () => {
jest.runAllTimers(); jest.runAllTimers();
expect(uploadRef.current.fileList).toHaveLength(files.length); expect(uploadRef.current.fileList).toHaveLength(files.length);
wrapper.unmount();
jest.useRealTimers(); jest.useRealTimers();
}); });
@ -1035,5 +1113,7 @@ describe('Upload List', () => {
}; };
const wrapper = mount(<UploadList locale={{}} items={fileList} itemRender={itemRender} />); const wrapper = mount(<UploadList locale={{}} items={fileList} itemRender={itemRender} />);
expect(wrapper.render()).toMatchSnapshot(); expect(wrapper.render()).toMatchSnapshot();
wrapper.unmount();
}); });
}); });

View File

@ -7,11 +7,11 @@ title:
## zh-CN ## zh-CN
`beforeUpload` 返回 `false``Promise.reject` 时,只用于拦截上传行为,不会阻止文件进入上传列表([原因](https://github.com/ant-design/ant-design/issues/15561#issuecomment-475108235))。如果需要阻止列表展现,可以参照此例配合 `onChange` 进行实现。 `beforeUpload` 返回 `false``Promise.reject` 时,只用于拦截上传行为,不会阻止文件进入上传列表([原因](https://github.com/ant-design/ant-design/issues/15561#issuecomment-475108235))。如果需要阻止列表展现,可以通过返回 `Upload.LIST_IGNORE` 实现。
## en-US ## en-US
`beforeUpload` only prevent upload behavior when return false or reject promise, the prevented file would still show in file list. Here is the example you can keep prevented files out of list by using `onChange`. `beforeUpload` only prevent upload behavior when return false or reject promise, the prevented file would still show in file list. Here is the example you can keep prevented files out of list by return `UPLOAD.LIST_IGNORE`.
```jsx ```jsx
import React, { useState } from 'react'; import React, { useState } from 'react';
@ -19,19 +19,15 @@ import { Upload, Button, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
const Uploader = () => { const Uploader = () => {
const [fileList, updateFileList] = useState([]);
const props = { const props = {
fileList,
beforeUpload: file => { beforeUpload: file => {
if (file.type !== 'image/png') { if (file.type !== 'image/png') {
message.error(`${file.name} is not a png file`); message.error(`${file.name} is not a png file`);
} }
return file.type === 'image/png'; return file.type === 'image/png' ? true : Upload.LIST_IGNORE;
}, },
onChange: info => { onChange: info => {
console.log(info.fileList); console.log(info.fileList);
// file.status is empty when beforeUpload return false
updateFileList(info.fileList.filter(file => !!file.status));
}, },
}; };
return ( return (

View File

@ -37,7 +37,7 @@ export interface UploadFile<T = any> {
export interface UploadChangeParam<T extends object = UploadFile> { export interface UploadChangeParam<T extends object = UploadFile> {
// https://github.com/ant-design/ant-design/issues/14420 // https://github.com/ant-design/ant-design/issues/14420
file: T; file: T;
fileList: Array<UploadFile>; fileList: UploadFile[];
event?: { percent: number }; event?: { percent: number };
} }

View File

@ -1,56 +0,0 @@
import { useRef, useEffect } from 'react';
import raf from 'rc-util/lib/raf';
import useForceUpdate from '../_util/hooks/useForceUpdate';
// Note. Only for upload usage. Do not export to global util hooks
export default function useFreshState<T>(
defaultValue: T,
propValue?: T,
): [(displayValue?: boolean) => T, (newValue: T) => void] {
const valueRef = useRef(defaultValue);
const forceUpdate = useForceUpdate();
const rafRef = useRef<number>();
// Set value
function setValue(newValue: T) {
valueRef.current = newValue;
forceUpdate();
}
function cleanUp() {
raf.cancel(rafRef.current!);
}
function rafSyncValue(newValue: T) {
cleanUp();
rafRef.current = raf(() => {
setValue(newValue);
});
}
// Get value
function getValue(displayValue = false) {
if (displayValue) {
return propValue || valueRef.current;
}
return valueRef.current;
}
// Effect will always update in a next frame to avoid sync state overwrite current processing state
useEffect(() => {
if (propValue) {
rafSyncValue(propValue);
}
}, [propValue]);
// Clean up
useEffect(
() => () => {
cleanUp();
},
[],
);
return [getValue, setValue];
}

View File

@ -4,11 +4,13 @@ export function T() {
return true; return true;
} }
// Fix IE file.status problem /**
// via coping a new Object * Wrap file with Proxy to provides more info. Will fallback to object if Proxy not support.
export function fileToObject(file: RcFile): UploadFile { *
return { * Origin comment: Fix IE file.status problem via coping a new Object
...file, */
export function wrapFile(file: RcFile | UploadFile): UploadFile {
const filledProps = {
lastModified: file.lastModified, lastModified: file.lastModified,
lastModifiedDate: file.lastModifiedDate, lastModifiedDate: file.lastModifiedDate,
name: file.name, name: file.name,
@ -17,6 +19,41 @@ export function fileToObject(file: RcFile): UploadFile {
uid: file.uid, uid: file.uid,
percent: 0, percent: 0,
originFileObj: file, originFileObj: file,
};
if (typeof Proxy !== 'undefined') {
const data = new Map<string | symbol, any>(Object.entries(filledProps));
return new Proxy(file, {
get(target, key) {
if (data.has(key)) {
return data.get(key);
}
return (target as any)[key];
},
set(_, key, value) {
data.set(key, value);
return true;
},
has(target, prop) {
return data.has(prop) || prop in target;
},
ownKeys(target) {
const keys = [...Object.keys(target), ...data.keys()];
return [...new Set(keys)];
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
});
}
return {
...file,
...filledProps,
} as UploadFile; } as UploadFile;
} }

View File

@ -147,7 +147,7 @@
"rc-tree": "~4.1.0", "rc-tree": "~4.1.0",
"rc-tree-select": "~4.3.0", "rc-tree-select": "~4.3.0",
"rc-trigger": "^5.2.1", "rc-trigger": "^5.2.1",
"rc-upload": "~3.3.4", "rc-upload": "~4.0.0-alpha.6",
"rc-util": "^5.8.1", "rc-util": "^5.8.1",
"scroll-into-view-if-needed": "^2.2.25", "scroll-into-view-if-needed": "^2.2.25",
"warning": "^4.0.3" "warning": "^4.0.3"