Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: onStart always in sync #291

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"printWidth": 100
"printWidth": 100,
"arrowParens": "avoid"
}
15 changes: 2 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
143 changes: 77 additions & 66 deletions src/AjaxUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,104 +84,115 @@ class AjaxUploader extends Component<UploadProps> {
}

uploadFiles = (files: FileList) => {
const { onBatchUpload } = this.props;
const postFiles: Array<RcFile> = 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<RcFile>) {
upload(file: RcFile, fileList: Array<RcFile>): Promise<RcFile> {
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<RcFile> {
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);
});
});
});
}
Expand Down
2 changes: 2 additions & 0 deletions src/interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
150 changes: 150 additions & 0 deletions tests/batch.spec.tsx
Original file line number Diff line number Diff line change
@@ -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(<Upload onBatchUpload={onBatchUpload} {...genProps()} />);
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(
<Upload onBatchUpload={onBatchUpload} beforeUpload={() => 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(
<Upload
onBatchUpload={onBatchUpload}
beforeUpload={file => 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(
<Upload
onBatchUpload={onBatchUpload}
beforeUpload={() => 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(
<Upload
onBatchUpload={onBatchUpload}
beforeUpload={async file => {
if (file === firstFile) {
await delay(100);
}
return file;
}}
{...genProps()}
/>,
);
triggerUpload(wrapper);

setTimeout(() => {
expect(onBatchUpload).toHaveBeenCalledWith([]);
done();
}, 1000);
});
});
});
});