From d3bab0d19644f1a41cc9ff5753ef41587ceee0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 30 Oct 2025 17:33:29 +0800 Subject: [PATCH 1/5] Revert "feat: support folder and deprecated directory (#643)" This reverts commit c1ea49f104a45ab468bc3cbedde4453dc072ad53. --- src/AjaxUploader.tsx | 6 +++--- src/interface.tsx | 2 -- tests/uploader.spec.tsx | 12 ------------ 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 3f596eb..8165551 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -322,7 +322,6 @@ class AjaxUploader extends Component { capture, children, directory, - folder, openFileDialogOnClick, onMouseEnter, onMouseLeave, @@ -331,8 +330,9 @@ class AjaxUploader extends Component { } = this.props; const cls = clsx(prefixCls, { [`${prefixCls}-disabled`]: disabled, [className]: className }); // because input don't have directory/webkitdirectory type declaration - const dirProps: any = - directory || folder ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {}; + const dirProps: any = directory + ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } + : {}; const events = disabled ? {} : { diff --git a/src/interface.tsx b/src/interface.tsx index f2906e8..768981e 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -13,9 +13,7 @@ export interface UploadProps component?: React.ComponentType | string; action?: Action; method?: UploadRequestMethod; - /** @deprecated Please use `folder` instead */ directory?: boolean; - folder?: boolean; data?: Record | ((file: RcFile | string | Blob) => Record); headers?: UploadRequestHeader; accept?: string; diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index c688368..ef6e856 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -1038,18 +1038,6 @@ describe('uploader', () => { directory: false, }, ); - - it('should trigger beforeUpload when uploading non-accepted files in folder mode', () => { - const beforeUpload = jest.fn(); - const { container } = render(); - - fireEvent.change(container.querySelector('input')!, { - target: { - files: [new File([], 'bamboo.png'), new File([], 'light.jpg')], - }, - }); - expect(beforeUpload).toHaveBeenCalledTimes(2); - }); }); describe('transform file before request', () => { From 4c0d24ae421bd5c5c7c3b417622f94bfb1653ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 30 Oct 2025 17:41:05 +0800 Subject: [PATCH 2/5] chore: add get filter func --- src/AjaxUploader.tsx | 25 ++++++++++++++++++++++++- src/interface.tsx | 7 ++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 8165551..b2c7459 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -1,9 +1,10 @@ /* eslint react/no-is-mounted:0,react/sort-comp:0,react/prop-types:0 */ -import { clsx } from 'clsx'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; +import { clsx } from 'clsx'; import React, { Component } from 'react'; import attrAccept from './attr-accept'; import type { + AcceptConfig, BeforeUploadFileType, RcFile, UploadProgressEvent, @@ -30,6 +31,28 @@ class AjaxUploader extends Component { private _isMounted: boolean; + private getFilterFn = () => { + const { accept, directory } = this.props; + + let filterFn: AcceptConfig['filter']; + let acceptFormat: string | undefined; + + if (typeof accept === 'string') { + acceptFormat = accept; + } else { + const { filter, format } = accept || {}; + + acceptFormat = format; + if (filter === 'native') { + filterFn = () => true; + } else { + filterFn = filter; + } + } + + return filterFn || (directory ? (file: RcFile) => attrAccept(file, acceptFormat) : () => true); + }; + onChange = (e: React.ChangeEvent) => { const { accept, directory } = this.props; const { files } = e.target; diff --git a/src/interface.tsx b/src/interface.tsx index 768981e..41fac82 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -4,6 +4,11 @@ export type BeforeUploadFileType = File | Blob | boolean | string; export type Action = string | ((file: RcFile) => string | PromiseLike); +export type AcceptConfig = { + format: string; + filter?: 'native' | ((file: RcFile) => boolean); +}; + export interface UploadProps extends Omit, 'onError' | 'onProgress'> { name?: string; @@ -16,7 +21,7 @@ export interface UploadProps directory?: boolean; data?: Record | ((file: RcFile | string | Blob) => Record); headers?: UploadRequestHeader; - accept?: string; + accept?: string | AcceptConfig; multiple?: boolean; onBatchStart?: ( fileList: { file: RcFile; parsedFile: Exclude }[], From 27cd976e23679390a1883f2f11c7a8bc7795c0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 30 Oct 2025 18:07:49 +0800 Subject: [PATCH 3/5] chore: compatible --- src/AjaxUploader.tsx | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index b2c7459..9477b99 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -31,10 +31,10 @@ class AjaxUploader extends Component { private _isMounted: boolean; - private getFilterFn = () => { + private filterFile = (file: RcFile | File, force = false) => { const { accept, directory } = this.props; - let filterFn: AcceptConfig['filter']; + let filterFn: Exclude; let acceptFormat: string | undefined; if (typeof accept === 'string') { @@ -50,15 +50,17 @@ class AjaxUploader extends Component { } } - return filterFn || (directory ? (file: RcFile) => attrAccept(file, acceptFormat) : () => true); + const mergedFilter = + filterFn || + (directory || force + ? (currentFile: RcFile) => attrAccept(currentFile, acceptFormat) + : () => true); + return mergedFilter(file as RcFile); }; onChange = (e: React.ChangeEvent) => { - const { accept, directory } = this.props; const { files } = e.target; - const acceptedFiles = [...files].filter( - (file: RcFile) => !directory || attrAccept(file, accept), - ); + const acceptedFiles = [...files].filter(file => this.filterFile(file)); this.uploadFiles(acceptedFiles); this.reset(); }; @@ -90,7 +92,7 @@ class AjaxUploader extends Component { }; onDataTransferFiles = async (dataTransfer: DataTransfer, existFileCallback?: () => void) => { - const { multiple, accept, directory } = this.props; + const { multiple, directory } = this.props; const items: DataTransferItem[] = [...(dataTransfer.items || [])]; let files: File[] = [...(dataTransfer.files || [])]; @@ -100,12 +102,10 @@ class AjaxUploader extends Component { } if (directory) { - files = await traverseFileTree(Array.prototype.slice.call(items), (_file: RcFile) => - attrAccept(_file, this.props.accept), - ); + files = await traverseFileTree(Array.prototype.slice.call(items), this.filterFile); this.uploadFiles(files); } else { - let acceptFiles = [...files].filter((file: RcFile) => attrAccept(file, accept)); + let acceptFiles = [...files].filter(file => this.filterFile(file, true)); if (multiple === false) { acceptFiles = files.slice(0, 1); @@ -351,6 +351,9 @@ class AjaxUploader extends Component { hasControlInside, ...otherProps } = this.props; + + // Extract accept format for input element + const acceptFormat = typeof accept === 'string' ? accept : accept?.format; const cls = clsx(prefixCls, { [`${prefixCls}-disabled`]: disabled, [className]: className }); // because input don't have directory/webkitdirectory type declaration const dirProps: any = directory @@ -384,7 +387,7 @@ class AjaxUploader extends Component { key={this.state.uid} style={{ display: 'none', ...styles.input }} className={classNames.input} - accept={accept} + accept={acceptFormat} {...dirProps} multiple={multiple} onChange={this.onChange} From 6b0ae0d3711f30dd37b4f7e13b744cc764e906b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 30 Oct 2025 18:11:13 +0800 Subject: [PATCH 4/5] chore: fix lint --- src/interface.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interface.tsx b/src/interface.tsx index 41fac82..27f28b5 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -10,7 +10,7 @@ export type AcceptConfig = { }; export interface UploadProps - extends Omit, 'onError' | 'onProgress'> { + extends Omit, 'onError' | 'onProgress' | 'accept'> { name?: string; style?: React.CSSProperties; className?: string; From f3ccf21dee7b04e6f36e05a571f5b4dd3dae18a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 30 Oct 2025 18:18:37 +0800 Subject: [PATCH 5/5] test: add test case --- tests/uploader.spec.tsx | 67 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index ef6e856..5b3040c 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -1,5 +1,5 @@ -import { fireEvent, render } from '@testing-library/react'; import { resetWarned } from '@rc-component/util/lib/warning'; +import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import sinon from 'sinon'; import { format } from 'util'; @@ -1040,6 +1040,71 @@ describe('uploader', () => { ); }); + describe('AcceptConfig', () => { + let uploader: ReturnType; + const handlers: UploadProps = {}; + + const props: UploadProps = { + action: '/test', + data: { a: 1, b: 2 }, + directory: true, // Enable format filtering + onStart(file) { + if (handlers.onStart) { + handlers.onStart(file); + } + }, + }; + + function testAcceptConfig(desc: string, accept: any, files: object[], expectCallTimes: number) { + it(desc, done => { + uploader = render(); + const input = uploader.container.querySelector('input')!; + fireEvent.change(input, { target: { files } }); + const mockStart = jest.fn(); + handlers.onStart = mockStart; + + setTimeout(() => { + expect(mockStart.mock.calls.length).toBe(expectCallTimes); + done(); + }, 100); + }); + } + + testAcceptConfig( + 'should work with format only', + { format: '.png' }, + [{ name: 'test.png' }, { name: 'test.jpg' }], + 1, + ); + + testAcceptConfig( + 'should work with filter: native', + { format: '.png', filter: 'native' }, + [{ name: 'test.png' }, { name: 'test.jpg' }], + 2, // native filter bypasses format check + ); + + testAcceptConfig( + 'should work with custom filter function', + { + format: '.png', + filter: (file: any) => file.name.includes('custom'), + }, + [{ name: 'custom.jpg' }, { name: 'test.png' }], + 1, // only custom.jpg passes custom filter + ); + + testAcceptConfig( + 'should work with MIME type format', + { format: 'image/*' }, + [ + { name: 'test.png', type: 'image/png' }, + { name: 'doc.txt', type: 'text/plain' }, + ], + 1, // only image file passes + ); + }); + describe('transform file before request', () => { let uploader: ReturnType; beforeEach(() => {