diff --git a/.prettierrc b/.prettierrc index 895b8bdc..77290ab3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,6 @@ "singleQuote": true, "trailingComma": "all", "proseWrap": "never", - "printWidth": 100 + "printWidth": 100, + "arrowParens": "avoid" } diff --git a/package.json b/package.json index 9a5de4e1..acf11971 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "now-build": "npm run docs:build" }, "devDependencies": { - "@types/jest": "^26.0.0", + "@types/enzyme": "^3.10.8", + "@types/jest": "^26.0.19", "@types/react": "^16.9.2", "@types/react-dom": "^16.9.0", "@umijs/fabric": "^2.0.0", @@ -67,18 +68,6 @@ "classnames": "^2.2.5", "rc-util": "^5.2.0" }, - "jest": { - "collectCoverageFrom": [ - "src/*" - ], - "coveragePathIgnorePatterns": [ - "src/IframeUploader.jsx" - ], - "transform": { - "\\.tsx?$": "./node_modules/rc-tools/scripts/jestPreprocessor.js", - "\\.jsx?$": "./node_modules/rc-tools/scripts/jestPreprocessor.js" - } - }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index cbc08e30..a49b5aba 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -84,104 +84,115 @@ class AjaxUploader extends Component { } uploadFiles = (files: FileList) => { + const { onBatchUpload } = this.props; const postFiles: Array = Array.prototype.slice.call(files); - postFiles + const startPromiseList = postFiles .map((file: RcFile & { uid?: string }) => { // eslint-disable-next-line no-param-reassign file.uid = getUid(); return file; }) - .forEach(file => { - this.upload(file, postFiles); - }); + .map(file => this.upload(file, postFiles)); + + // Trigger when all files has started + Promise.all(startPromiseList).then(parsedFiles => { + onBatchUpload?.(parsedFiles.filter(f => f)); + }); }; - upload(file: RcFile, fileList: Array) { + upload(file: RcFile, fileList: Array): Promise { const { props } = this; - if (!props.beforeUpload) { + const { beforeUpload } = this.props; + if (!beforeUpload) { // always async in case use react state to keep fileList - Promise.resolve().then(() => { - this.post(file); - }); - return; + return Promise.resolve().then(() => this.post(file)); } - const before = props.beforeUpload(file, fileList); + const before = beforeUpload(file, fileList); if (before && typeof before !== 'boolean' && before.then) { - before + return before .then(processedFile => { const processedFileType = Object.prototype.toString.call(processedFile); if (processedFileType === '[object File]' || processedFileType === '[object Blob]') { - this.post(processedFile); - return; + return this.post(processedFile); } - this.post(file); + return this.post(file); }) .catch(e => { // eslint-disable-next-line no-console console.log(e); + + return null; }); - } else if (before !== false) { - Promise.resolve().then(() => { - this.post(file); - }); } + + if (before !== false) { + return Promise.resolve().then(() => this.post(file)); + } + + return Promise.resolve(file); } - post(file: RcFile) { + post(file: RcFile): Promise { if (!this._isMounted) { - return; + return null; } const { props } = this; const { onStart, onProgress, transformFile = originFile => originFile } = props; - new Promise(resolve => { - let { action } = props; - if (typeof action === 'function') { - action = action(file); - } - return resolve(action); - }).then((action: string) => { - const { uid } = file; - const request = props.customRequest || defaultRequest; - const transform = Promise.resolve(transformFile(file)) - .then(transformedFile => { - let { data } = props; - if (typeof data === 'function') { - data = data(transformedFile); - } - return Promise.all([transformedFile, data]); - }) - .catch(e => { - console.error(e); // eslint-disable-line no-console - }); + return new Promise(resolveStartFile => { + new Promise(resolveAction => { + let { action } = props; + if (typeof action === 'function') { + action = action(file); + } + return resolveAction(action); + }).then((action: string) => { + const { uid } = file; + const request = props.customRequest || defaultRequest; + const transform = Promise.resolve(transformFile(file)) + .then(transformedFile => { + let { data } = props; + if (typeof data === 'function') { + data = data(transformedFile); + } + return Promise.all([transformedFile, data]); + }) + .catch(e => { + console.error(e); // eslint-disable-line no-console + }); - transform.then(([transformedFile, data]: [RcFile, object]) => { - const requestOption = { - action, - filename: props.name, - data, - file: transformedFile, - headers: props.headers, - withCredentials: props.withCredentials, - method: props.method || 'post', - onProgress: onProgress - ? (e: UploadProgressEvent) => { - onProgress(e, file); - } - : null, - onSuccess: (ret: any, xhr: XMLHttpRequest) => { - delete this.reqs[uid]; - props.onSuccess(ret, file, xhr); - }, - onError: (err: UploadRequestError, ret: any) => { - delete this.reqs[uid]; - props.onError(err, ret, file); - }, - }; + transform.then(([transformedFile, data]: [RcFile, object]) => { + const requestOption = { + action, + filename: props.name, + data, + file: transformedFile, + headers: props.headers, + withCredentials: props.withCredentials, + method: props.method || 'post', + onProgress: onProgress + ? (e: UploadProgressEvent) => { + onProgress(e, file); + } + : null, + onSuccess: (ret: any, xhr: XMLHttpRequest) => { + delete this.reqs[uid]; + props.onSuccess(ret, file, xhr); + }, + onError: (err: UploadRequestError, ret: any) => { + delete this.reqs[uid]; + props.onError(err, ret, file); + }, + }; + + onStart(file); - onStart(file); - this.reqs[uid] = request(requestOption); + this.reqs[uid] = request(requestOption); + + // Tell root we have finish start + resolveStartFile(file); + }); }); }); } diff --git a/src/interface.tsx b/src/interface.tsx index a9196808..959f6418 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -14,6 +14,8 @@ export interface UploadProps headers?: UploadRequestHeader; accept?: string; multiple?: boolean; + /** @private Trigger when a batch of files upload. Internal usage, do not use in your production! */ + onBatchUpload?: (files: RcFile[]) => void; onStart?: (file: RcFile) => void; onError?: (error: Error, ret: object, file: RcFile) => void; onSuccess?: (response: object, file: RcFile, xhr: object) => void; diff --git a/tests/batch.spec.tsx b/tests/batch.spec.tsx new file mode 100644 index 00000000..7f9815e6 --- /dev/null +++ b/tests/batch.spec.tsx @@ -0,0 +1,150 @@ +/* eslint no-console:0 */ +import React from 'react'; +import { format } from 'util'; +import { mount, ReactWrapper } from 'enzyme'; +import sinon from 'sinon'; +import Upload from '../src'; +import { UploadProps, RcFile } from '../src/interface'; + +const delay = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout)); + +describe('Upload.Batch', () => { + function getFile(name: string): RcFile { + return { + name, + toString: () => name, + } as RcFile; + } + + function genProps(props?: { + onStart: UploadProps['onStart']; + onProgress: UploadProps['onProgress']; + onSuccess: UploadProps['onSuccess']; + onError: UploadProps['onError']; + }) { + return { + action: '/test', + data: { a: 1, b: 2 }, + multiple: true, + accept: '.png', + onStart(file) { + props?.onStart?.(file); + }, + onSuccess(res, file, xhr) { + props?.onSuccess?.(res, file, xhr); + }, + onProgress(step, file) { + props?.onProgress?.(step, file); + }, + onError(err, ret, file) { + props?.onError?.(err, ret, file); + }, + }; + } + + describe('onBatchUpload', () => { + const firstFile = getFile('first.png'); + const secondFile = getFile('second.png'); + + function triggerUpload(wrapper: ReactWrapper) { + const files: RcFile[] = [firstFile, secondFile]; + wrapper.find('input').first().simulate('change', { target: { files } }); + } + + it('should trigger', done => { + const onBatchUpload = jest.fn(); + + const wrapper = mount(); + triggerUpload(wrapper); + + setTimeout(() => { + expect(onBatchUpload).toHaveBeenCalledWith([ + expect.objectContaining(firstFile), + expect.objectContaining(secondFile), + ]); + done(); + }, 10); + }); + + describe('beforeUpload', () => { + it('return false', done => { + const onBatchUpload = jest.fn(); + + const wrapper = mount( + false} {...genProps()} />, + ); + triggerUpload(wrapper); + + setTimeout(() => { + expect(onBatchUpload).toHaveBeenCalledWith([ + expect.objectContaining(firstFile), + expect.objectContaining(secondFile), + ]); + done(); + }, 10); + }); + + it('return promise file', done => { + const onBatchUpload = jest.fn(); + + const wrapper = mount( + Promise.resolve(file)} + {...genProps()} + />, + ); + triggerUpload(wrapper); + + setTimeout(() => { + expect(onBatchUpload).toHaveBeenCalledWith([ + expect.objectContaining(firstFile), + expect.objectContaining(secondFile), + ]); + done(); + }, 10); + }); + + it('return promise rejection', done => { + const onBatchUpload = jest.fn(); + + const wrapper = mount( + Promise.reject()} + {...genProps()} + />, + ); + triggerUpload(wrapper); + + setTimeout(() => { + expect(onBatchUpload).toHaveBeenCalledWith([]); + done(); + }, 10); + }); + + it('beforeUpload delay for the first', done => { + const onBatchUpload = jest.fn(); + + const wrapper = mount( + { + if (file === firstFile) { + await delay(100); + } + return file; + }} + {...genProps()} + />, + ); + triggerUpload(wrapper); + + setTimeout(() => { + expect(onBatchUpload).toHaveBeenCalledWith([]); + done(); + }, 1000); + }); + }); + }); +});