mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-11 11:32:52 +08:00
fix: Controlled multiple files upload list sync (#26612)
* init hooks * Use raf to sync list * clean up
This commit is contained in:
parent
7041af92fb
commit
a26517f9ab
@ -19,6 +19,7 @@ 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 useForceUpdate from '../_util/hooks/useForceUpdate';
|
||||||
|
import useFreshState from './useFreshState';
|
||||||
|
|
||||||
export { UploadProps };
|
export { UploadProps };
|
||||||
|
|
||||||
@ -47,11 +48,11 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
const [dragState, setDragState] = React.useState<string>('drop');
|
const [dragState, setDragState] = React.useState<string>('drop');
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
// `fileListRef` used for internal state sync to avoid control mode set it back when sync update.
|
// Refresh always use fresh data
|
||||||
// `visibleFileList` used for display in UploadList instead.
|
const [getFileList, setFileList] = useFreshState<UploadFile<any>[]>(
|
||||||
// It's a workaround and not the best solution.
|
fileListProp || defaultFileList || [],
|
||||||
const fileListRef = React.useRef(fileListProp || defaultFileList || []);
|
fileListProp,
|
||||||
const visibleFileList = fileListProp || fileListRef.current;
|
);
|
||||||
|
|
||||||
const upload = React.useRef<any>();
|
const upload = React.useRef<any>();
|
||||||
|
|
||||||
@ -63,16 +64,8 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (fileListProp !== undefined && fileListProp !== fileListRef.current) {
|
|
||||||
fileListRef.current = fileListProp;
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
}, [fileListProp]);
|
|
||||||
|
|
||||||
const onChange = (info: UploadChangeParam) => {
|
const onChange = (info: UploadChangeParam) => {
|
||||||
fileListRef.current = info.fileList;
|
setFileList(info.fileList);
|
||||||
forceUpdate();
|
|
||||||
|
|
||||||
const { onChange: onChangeProp } = props;
|
const { onChange: onChangeProp } = props;
|
||||||
if (onChangeProp) {
|
if (onChangeProp) {
|
||||||
@ -87,7 +80,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
const targetItem = fileToObject(file);
|
const targetItem = fileToObject(file);
|
||||||
targetItem.status = 'uploading';
|
targetItem.status = 'uploading';
|
||||||
|
|
||||||
const nextFileList = fileListRef.current.concat();
|
const nextFileList = getFileList().concat();
|
||||||
|
|
||||||
const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid);
|
const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === targetItem.uid);
|
||||||
if (fileIndex === -1) {
|
if (fileIndex === -1) {
|
||||||
@ -110,7 +103,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* do nothing */
|
/* do nothing */
|
||||||
}
|
}
|
||||||
const targetItem = getFileItem(file, fileListRef.current);
|
const targetItem = getFileItem(file, getFileList());
|
||||||
// removed
|
// removed
|
||||||
if (!targetItem) {
|
if (!targetItem) {
|
||||||
return;
|
return;
|
||||||
@ -120,12 +113,12 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
targetItem.xhr = xhr;
|
targetItem.xhr = xhr;
|
||||||
onChange({
|
onChange({
|
||||||
file: { ...targetItem },
|
file: { ...targetItem },
|
||||||
fileList: fileListRef.current.concat(),
|
fileList: getFileList().concat(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onProgress = (e: { percent: number }, file: UploadFile) => {
|
const onProgress = (e: { percent: number }, file: UploadFile) => {
|
||||||
const targetItem = getFileItem(file, fileListRef.current);
|
const targetItem = getFileItem(file, getFileList());
|
||||||
// removed
|
// removed
|
||||||
if (!targetItem) {
|
if (!targetItem) {
|
||||||
return;
|
return;
|
||||||
@ -134,12 +127,12 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
onChange({
|
onChange({
|
||||||
event: e,
|
event: e,
|
||||||
file: { ...targetItem },
|
file: { ...targetItem },
|
||||||
fileList: fileListRef.current.concat(),
|
fileList: getFileList().concat(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onError = (error: Error, response: any, file: UploadFile) => {
|
const onError = (error: Error, response: any, file: UploadFile) => {
|
||||||
const targetItem = getFileItem(file, fileListRef.current);
|
const targetItem = getFileItem(file, getFileList());
|
||||||
// removed
|
// removed
|
||||||
if (!targetItem) {
|
if (!targetItem) {
|
||||||
return;
|
return;
|
||||||
@ -149,7 +142,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
targetItem.status = 'error';
|
targetItem.status = 'error';
|
||||||
onChange({
|
onChange({
|
||||||
file: { ...targetItem },
|
file: { ...targetItem },
|
||||||
fileList: fileListRef.current.concat(),
|
fileList: getFileList().concat(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,7 +153,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removedFileList = removeFileItem(file, fileListRef.current);
|
const removedFileList = removeFileItem(file, getFileList());
|
||||||
|
|
||||||
if (removedFileList) {
|
if (removedFileList) {
|
||||||
file.status = 'removed';
|
file.status = 'removed';
|
||||||
@ -189,11 +182,13 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
if (result === false) {
|
if (result === false) {
|
||||||
// Get unique file list
|
// Get unique file list
|
||||||
const uniqueList: UploadFile<any>[] = [];
|
const uniqueList: UploadFile<any>[] = [];
|
||||||
fileListRef.current.concat(fileListArgs.map(fileToObject)).forEach(f => {
|
getFileList()
|
||||||
if (uniqueList.every(uf => uf.uid !== f.uid)) {
|
.concat(fileListArgs.map(fileToObject))
|
||||||
uniqueList.push(f);
|
.forEach(f => {
|
||||||
}
|
if (uniqueList.every(uf => uf.uid !== f.uid)) {
|
||||||
});
|
uniqueList.push(f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
file,
|
file,
|
||||||
@ -212,7 +207,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
onSuccess,
|
onSuccess,
|
||||||
onProgress,
|
onProgress,
|
||||||
onError,
|
onError,
|
||||||
fileList: fileListRef.current,
|
fileList: getFileList(),
|
||||||
upload: upload.current,
|
upload: upload.current,
|
||||||
forceUpdate,
|
forceUpdate,
|
||||||
}));
|
}));
|
||||||
@ -251,7 +246,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
return (
|
return (
|
||||||
<UploadList
|
<UploadList
|
||||||
listType={listType}
|
listType={listType}
|
||||||
items={visibleFileList}
|
items={getFileList(true)}
|
||||||
previewFile={previewFile}
|
previewFile={previewFile}
|
||||||
onPreview={onPreview}
|
onPreview={onPreview}
|
||||||
onDownload={onDownload}
|
onDownload={onDownload}
|
||||||
@ -279,9 +274,7 @@ const InternalUpload: React.ForwardRefRenderFunction<unknown, UploadProps> = (pr
|
|||||||
prefixCls,
|
prefixCls,
|
||||||
{
|
{
|
||||||
[`${prefixCls}-drag`]: true,
|
[`${prefixCls}-drag`]: true,
|
||||||
[`${prefixCls}-drag-uploading`]: fileListRef.current.some(
|
[`${prefixCls}-drag-uploading`]: getFileList().some(file => file.status === 'uploading'),
|
||||||
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',
|
||||||
|
@ -17,6 +17,15 @@ describe('Upload', () => {
|
|||||||
beforeEach(() => setup());
|
beforeEach(() => setup());
|
||||||
afterEach(() => teardown());
|
afterEach(() => teardown());
|
||||||
|
|
||||||
|
// Mock for rc-util raf
|
||||||
|
window.requestAnimationFrame = (callback) => {
|
||||||
|
window.setTimeout(callback, 16);
|
||||||
|
};
|
||||||
|
window.cancelAnimationFrame = (id) => {
|
||||||
|
window.clearTimeout(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// https://github.com/react-component/upload/issues/36
|
// https://github.com/react-component/upload/issues/36
|
||||||
it('should get refs inside Upload in componentDidMount', () => {
|
it('should get refs inside Upload in componentDidMount', () => {
|
||||||
let ref;
|
let ref;
|
||||||
@ -255,6 +264,7 @@ describe('Upload', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be controlled by fileList', () => {
|
it('should be controlled by fileList', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
const fileList = [
|
const fileList = [
|
||||||
{
|
{
|
||||||
uid: '-1',
|
uid: '-1',
|
||||||
@ -266,8 +276,11 @@ describe('Upload', () => {
|
|||||||
const ref = React.createRef();
|
const ref = React.createRef();
|
||||||
const wrapper = mount(<Upload ref={ref} />);
|
const wrapper = mount(<Upload ref={ref} />);
|
||||||
expect(ref.current.fileList).toEqual([]);
|
expect(ref.current.fileList).toEqual([]);
|
||||||
|
|
||||||
wrapper.setProps({ fileList });
|
wrapper.setProps({ fileList });
|
||||||
|
jest.runAllTimers();
|
||||||
expect(ref.current.fileList).toEqual(fileList);
|
expect(ref.current.fileList).toEqual(fileList);
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('util', () => {
|
describe('util', () => {
|
||||||
|
@ -26,6 +26,14 @@ const fileList = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe('Upload List', () => {
|
describe('Upload List', () => {
|
||||||
|
// Mock for rc-util raf
|
||||||
|
window.requestAnimationFrame = callback => {
|
||||||
|
window.setTimeout(callback, 16);
|
||||||
|
};
|
||||||
|
window.cancelAnimationFrame = id => {
|
||||||
|
window.clearTimeout(id);
|
||||||
|
};
|
||||||
|
|
||||||
// jsdom not support `createObjectURL` yet. Let's handle this.
|
// jsdom not support `createObjectURL` yet. Let's handle this.
|
||||||
const originCreateObjectURL = window.URL.createObjectURL;
|
const originCreateObjectURL = window.URL.createObjectURL;
|
||||||
window.URL.createObjectURL = jest.fn(() => '');
|
window.URL.createObjectURL = jest.fn(() => '');
|
||||||
@ -913,4 +921,51 @@ describe('Upload List', () => {
|
|||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://github.com/ant-design/ant-design/issues/26536
|
||||||
|
it('multiple file upload should keep the internal fileList async', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const uploadRef = React.createRef();
|
||||||
|
|
||||||
|
const MyUpload = () => {
|
||||||
|
const [testFileList, setTestFileList] = React.useState([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Upload
|
||||||
|
ref={uploadRef}
|
||||||
|
fileList={testFileList}
|
||||||
|
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
||||||
|
multiple
|
||||||
|
onChange={info => {
|
||||||
|
setTestFileList([...info.fileList]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="button">Upload</button>
|
||||||
|
</Upload>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
mount(<MyUpload />);
|
||||||
|
|
||||||
|
// Mock async update in a frame
|
||||||
|
const files = ['light', 'bamboo', 'little'];
|
||||||
|
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
for (let i = 0; i < files.length; i += 1) {
|
||||||
|
await Promise.resolve();
|
||||||
|
uploadRef.current.onStart({
|
||||||
|
uid: files[i],
|
||||||
|
name: files[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
expect(uploadRef.current.fileList).toHaveLength(files.length);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(uploadRef.current.fileList).toHaveLength(files.length);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
56
components/upload/useFreshState.ts
Normal file
56
components/upload/useFreshState.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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];
|
||||||
|
}
|
@ -147,7 +147,7 @@
|
|||||||
"rc-tree": "~3.9.0",
|
"rc-tree": "~3.9.0",
|
||||||
"rc-tree-select": "~4.1.1",
|
"rc-tree-select": "~4.1.1",
|
||||||
"rc-trigger": "~4.4.0",
|
"rc-trigger": "~4.4.0",
|
||||||
"rc-upload": "~3.3.0",
|
"rc-upload": "~3.3.1",
|
||||||
"rc-util": "^5.1.0",
|
"rc-util": "^5.1.0",
|
||||||
"scroll-into-view-if-needed": "^2.2.25",
|
"scroll-into-view-if-needed": "^2.2.25",
|
||||||
"warning": "^4.0.3"
|
"warning": "^4.0.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user