From 33a1d04deab9615f01f59e2ce7af6667a9e1d573 Mon Sep 17 00:00:00 2001 From: lh01217311 Date: Mon, 27 Jul 2026 17:38:12 +0800 Subject: [PATCH 1/2] feat: add retryUpload method for upload - Add retryUpload method to AjaxUploader - Add retryUpload method to Upload component - Add unit test for retryUpload - Update README docs Co-Authored-By: Claude --- README.md | 8 ++++---- README.zh-CN.md | 8 ++++---- src/AjaxUploader.tsx | 8 ++++++++ src/Upload.tsx | 4 ++++ tests/uploader.spec.tsx | 23 +++++++++++++++++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) 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..d90532a8 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -301,6 +301,14 @@ class AjaxUploader extends Component { this.reqs[uid] = request(requestOption, { defaultRequest }); } + retryUpload = (originFile: RcFile) => { + this.processFile(originFile, [originFile]).then(fileInfo => { + if (fileInfo.parsedFile !== null) { + this.post(fileInfo); + } + }); + }; + 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..172ee3fe 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -253,6 +253,29 @@ describe('uploader', () => { }, 100); }); + it('retryUpload should make new request', done => { + const uploadRef = React.createRef(); + render(); + + const file = { + 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 any); + + setTimeout(() => { + expect(requests.length).toBe(initialRequestCount + 1); + done(); + }, 100); + }); + it('drag to upload', done => { const input = uploader.container.querySelector('input')!; From 9c1860257f26ca6e5af0263f310ce8073af66ec8 Mon Sep 17 00:00:00 2001 From: lh01217311 Date: Tue, 28 Jul 2026 14:47:12 +0800 Subject: [PATCH 2/2] fix: handle processFile rejection and improve test fixture --- src/AjaxUploader.tsx | 72 ++++++++++++++++++++++++++++++----------- tests/uploader.spec.tsx | 68 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 20 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index d90532a8..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 @@ -302,11 +334,15 @@ class AjaxUploader extends Component { } retryUpload = (originFile: RcFile) => { - this.processFile(originFile, [originFile]).then(fileInfo => { - if (fileInfo.parsedFile !== null) { - this.post(fileInfo); - } - }); + this.processFile(originFile, [originFile]) + .then(fileInfo => { + if (fileInfo.parsedFile) { + this.post(fileInfo); + } + }) + .catch(() => { + // Error already handled in processFile for action/data rejection + }); }; reset() { diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index 172ee3fe..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)); @@ -258,6 +258,7 @@ describe('uploader', () => { render(); const file = { + uid: 'rc-upload-retry-test', name: 'retry.png', toString() { return this.name; @@ -268,10 +269,73 @@ describe('uploader', () => { const initialRequestCount = requests.length; - uploadRef.current.retryUpload(file as any); + 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); });