diff --git a/README.md b/README.md
index 440e9659..a40f79a7 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,6 @@
English | 简体中文
-
## Highlights
- Supports Ajax uploads with progress, headers, credentials, and custom request overrides.
@@ -98,9 +97,10 @@ Then open `http://localhost:8000`.
### Methods
-| Name | Type | Description |
-| ------- | ------------------------- | ----------------------- |
-| `abort` | `(file: RcFile) => void` | Abort an active upload. |
+| Name | Type | Description |
+| ------------- | ------------------------ | ------------------------------------ |
+| `abort` | `(file: RcFile) => void` | Abort an active upload. |
+| `retryUpload` | `(file: RcFile) => void` | Retry an upload for a specific file. |
## Development
diff --git a/README.zh-CN.md b/README.zh-CN.md
index cc0e8dc8..04e23474 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -15,7 +15,6 @@
English | 简体中文
-
## 特性
- 支持带进度、请求头、凭证和自定义请求覆盖的 Ajax 上传。
@@ -98,9 +97,10 @@ npm start
### 方法
-| 名称 | 类型 | 说明 |
-| ------- | ------------------------- | ----------------------- |
-| `abort` | `(file: RcFile) => void` | 中止进行中的上传。 |
+| 名称 | 类型 | 说明 |
+| ------------- | ------------------------ | -------------------- |
+| `abort` | `(file: RcFile) => void` | 中止进行中的上传。 |
+| `retryUpload` | `(file: RcFile) => void` | 重试特定文件的上传。 |
## 本地开发
diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx
index 0244c3be..5eecc8b3 100644
--- a/src/AjaxUploader.tsx
+++ b/src/AjaxUploader.tsx
@@ -179,17 +179,21 @@ class AjaxUploader extends Component {
});
// Batch upload files
- Promise.all(postFiles).then(fileList => {
- const { onBatchStart } = this.props;
-
- onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })));
-
- fileList
- .filter(file => file.parsedFile !== null)
- .forEach(file => {
- this.post(file);
- });
- });
+ Promise.all(postFiles)
+ .then(fileList => {
+ const { onBatchStart } = this.props;
+
+ onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })));
+
+ fileList
+ .filter(file => file.parsedFile !== null)
+ .forEach(file => {
+ this.post(file);
+ });
+ })
+ .catch(() => {
+ // Error already handled in processFile, this catch is to prevent unhandled rejection
+ });
};
/**
@@ -220,20 +224,48 @@ class AjaxUploader extends Component {
const { action } = this.props;
let mergedAction: string;
if (typeof action === 'function') {
- mergedAction = await action(file);
+ try {
+ mergedAction = await action(file);
+ } catch {
+ transformedFile = false;
+ }
} else {
mergedAction = action;
}
+ // Early return if action rejected
+ if (transformedFile === false) {
+ return {
+ origin: file,
+ parsedFile: null,
+ action: null,
+ data: null,
+ };
+ }
+
// Get latest data
const { data } = this.props;
let mergedData: Record;
if (typeof data === 'function') {
- mergedData = await data(file);
+ try {
+ mergedData = await data(file);
+ } catch {
+ transformedFile = false;
+ }
} else {
mergedData = data;
}
+ // Early return if data rejected
+ if (transformedFile === false) {
+ return {
+ origin: file,
+ parsedFile: null,
+ action: null,
+ data: null,
+ };
+ }
+
const parsedData =
// string type is from legacy `transformFile`.
// Not sure if this will work since no related test case works with it
@@ -301,6 +333,18 @@ class AjaxUploader extends Component {
this.reqs[uid] = request(requestOption, { defaultRequest });
}
+ retryUpload = (originFile: RcFile) => {
+ this.processFile(originFile, [originFile])
+ .then(fileInfo => {
+ if (fileInfo.parsedFile) {
+ this.post(fileInfo);
+ }
+ })
+ .catch(() => {
+ // Error already handled in processFile for action/data rejection
+ });
+ };
+
reset() {
this.setState({
uid: getUid(),
diff --git a/src/Upload.tsx b/src/Upload.tsx
index 23541e31..6fe755ec 100644
--- a/src/Upload.tsx
+++ b/src/Upload.tsx
@@ -30,6 +30,10 @@ class Upload extends Component {
this.uploader.abort(file);
}
+ retryUpload(file: RcFile) {
+ this.uploader.retryUpload(file);
+ }
+
saveUploader = (node: AjaxUpload) => {
this.uploader = node;
};
diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx
index ef835041..a810ad5f 100644
--- a/tests/uploader.spec.tsx
+++ b/tests/uploader.spec.tsx
@@ -3,7 +3,7 @@ import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import sinon from 'sinon';
import { format } from 'util';
-import Upload, { type UploadProps } from '../src';
+import Upload, { type UploadProps, type RcFile } from '../src';
const sleep = (timeout = 500) => new Promise(resolve => setTimeout(resolve, timeout));
@@ -253,6 +253,93 @@ describe('uploader', () => {
}, 100);
});
+ it('retryUpload should make new request', done => {
+ const uploadRef = React.createRef();
+ render();
+
+ const file = {
+ uid: 'rc-upload-retry-test',
+ name: 'retry.png',
+ toString() {
+ return this.name;
+ },
+ };
+ const files = [file];
+ (files as any).item = (i: number) => files[i];
+
+ const initialRequestCount = requests.length;
+
+ uploadRef.current.retryUpload(file as RcFile);
+
+ setTimeout(() => {
+ expect(requests.length).toBe(initialRequestCount + 1);
+ // Verify the request uses the correct file uid
+ expect(requests[initialRequestCount].url).toBe('/test');
+ done();
+ }, 100);
+ });
+
+ it('retryUpload should not make request when async action rejects', done => {
+ const uploadRef = React.createRef();
+ render(
+ {
+ throw new Error('action error');
+ }}
+ />,
+ );
+
+ const file = {
+ uid: 'rc-upload-retry-action-reject',
+ name: 'retry.png',
+ toString() {
+ return this.name;
+ },
+ };
+
+ const initialRequestCount = requests.length;
+
+ uploadRef.current.retryUpload(file as RcFile);
+
+ setTimeout(() => {
+ // Should not make a new request when action rejects
+ expect(requests.length).toBe(initialRequestCount);
+ done();
+ }, 100);
+ });
+
+ it('retryUpload should not make request when async data rejects', done => {
+ const uploadRef = React.createRef();
+ render(
+ {
+ throw new Error('data error');
+ }}
+ />,
+ );
+
+ const file = {
+ uid: 'rc-upload-retry-data-reject',
+ name: 'retry.png',
+ toString() {
+ return this.name;
+ },
+ };
+
+ const initialRequestCount = requests.length;
+
+ uploadRef.current.retryUpload(file as RcFile);
+
+ setTimeout(() => {
+ // Should not make a new request when data rejects
+ expect(requests.length).toBe(initialRequestCount);
+ done();
+ }, 100);
+ });
+
it('drag to upload', done => {
const input = uploader.container.querySelector('input')!;