From d07f68ba05b97219561e4466e25287b58492ed25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Fri, 31 Jan 2025 12:44:23 +0800 Subject: [PATCH 01/10] chore: update @rc-component/upload" --- .gitignore | 1 + docs/examples/beforeUpload.tsx | 2 +- docs/examples/customRequest.tsx | 2 +- package.json | 22 +- src/AjaxUploader.tsx | 418 +++++++++++++++----------------- src/Upload.tsx | 76 +++--- src/attr-accept.ts | 2 +- src/interface.tsx | 6 +- src/request.ts | 6 +- src/traverseFileTree.ts | 11 +- src/uid.ts | 4 +- tests/uploader.spec.tsx | 42 ++-- 12 files changed, 290 insertions(+), 302 deletions(-) diff --git a/.gitignore b/.gitignore index 54a517ba..6c9b17e8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ coverage yarn.lock es package-lock.json +pnpm-lock.yaml tmp/ .history .storybook diff --git a/docs/examples/beforeUpload.tsx b/docs/examples/beforeUpload.tsx index cefd7752..c4527c53 100644 --- a/docs/examples/beforeUpload.tsx +++ b/docs/examples/beforeUpload.tsx @@ -1,6 +1,6 @@ /* eslint no-console:0 */ -import { Action } from '@/interface'; +import type { Action } from '@/interface'; import Upload from 'rc-upload'; const props = { diff --git a/docs/examples/customRequest.tsx b/docs/examples/customRequest.tsx index afe58fd5..8fb7cfd4 100644 --- a/docs/examples/customRequest.tsx +++ b/docs/examples/customRequest.tsx @@ -2,7 +2,7 @@ import React from 'react'; import axios from 'axios'; import Upload from 'rc-upload'; -import { UploadRequestOption } from '@/interface'; +import type { UploadRequestOption } from '@/interface'; const uploadProps = { action: '/upload.do', diff --git a/package.json b/package.json index bed6ad99..53a5fc08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "rc-upload", - "version": "4.8.1", + "name": "@rc-component/upload", + "version": "1.0.0", "description": "upload ui component for react", "keywords": [ "react", @@ -38,16 +38,18 @@ }, "dependencies": { "@babel/runtime": "^7.18.3", - "classnames": "^2.2.5", - "rc-util": "^5.2.0" + "@rc-component/util": "^1.2.0", + "classnames": "^2.2.5" }, "devDependencies": { "@rc-component/father-plugin": "^1.0.0", "@testing-library/jest-dom": "^6.1.5", "@testing-library/react": "^14.1.2", "@types/jest": "^29.5.11", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", + "@types/node": "^22.12.0", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@types/sinon": "^17.0.3", "@umijs/fabric": "^4.0.1", "axios": "^1.7.2", "co-busboy": "^1.3.0", @@ -61,15 +63,11 @@ "np": "^10.0.7", "raf": "^3.4.0", "rc-test": "^7.0.13", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "regenerator-runtime": "^0.14.1", "sinon": "^9.0.2", "typescript": "^5.3.3", "vinyl-fs": "^3.0.3" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" } } diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 383d10f3..16f7ed8f 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -1,7 +1,7 @@ /* eslint react/no-is-mounted:0,react/sort-comp:0,react/prop-types:0 */ -import clsx from 'classnames'; -import pickAttrs from 'rc-util/lib/pickAttrs'; -import React, { Component } from 'react'; +import classnames from 'classnames'; +import pickAttrs from '@rc-component/util/lib/pickAttrs'; +import React from 'react'; import attrAccept from './attr-accept'; import type { BeforeUploadFileType, @@ -9,6 +9,7 @@ import type { UploadProgressEvent, UploadProps, UploadRequestError, + UploadRequestOption, } from './interface'; import defaultRequest from './request'; import traverseFileTree from './traverseFileTree'; @@ -21,121 +22,79 @@ interface ParsedFileInfo { parsedFile: RcFile; } -class AjaxUploader extends Component { - state = { uid: getUid() }; - - reqs: Record = {}; - - private fileInput: HTMLInputElement; - - private _isMounted: boolean; - - onChange = (e: React.ChangeEvent) => { - const { accept, directory } = this.props; - const { files } = e.target; - const acceptedFiles = [...files].filter( - (file: RcFile) => !directory || attrAccept(file, accept), - ); - this.uploadFiles(acceptedFiles); - this.reset(); - }; - - onClick = (event: React.MouseEvent | React.KeyboardEvent) => { - const el = this.fileInput; - if (!el) { - return; - } - - const target = event.target as HTMLElement; - const { onClick } = this.props; - - if (target && target.tagName === 'BUTTON') { - const parent = el.parentNode as HTMLInputElement; - parent.focus(); - target.blur(); - } - el.click(); - if (onClick) { - onClick(event); - } - }; - - onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - this.onClick(e); - } - }; - - onFileDrop = async (e: React.DragEvent) => { - const { multiple } = this.props; - - e.preventDefault(); - - if (e.type === 'dragover') { - return; - } - - if (this.props.directory) { - const files = await traverseFileTree( - Array.prototype.slice.call(e.dataTransfer.items), - (_file: RcFile) => attrAccept(_file, this.props.accept), - ); - this.uploadFiles(files); - } else { - let files = [...e.dataTransfer.files].filter((file: RcFile) => - attrAccept(file, this.props.accept), - ); - - if (multiple === false) { - files = files.slice(0, 1); - } - - this.uploadFiles(files); - } - }; - - componentDidMount() { - this._isMounted = true; - } - - componentWillUnmount() { - this._isMounted = false; - this.abort(); - } - - uploadFiles = (files: File[]) => { - const originFiles = [...files] as RcFile[]; - const postFiles = originFiles.map((file: RcFile & { uid?: string }) => { - // eslint-disable-next-line no-param-reassign - file.uid = getUid(); - return this.processFile(file, originFiles); - }); - - // 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); +const AjaxUploader: React.FC>> = props => { + const { + component: Tag, + prefixCls, + className, + classNames = {}, + disabled, + id, + name, + style, + styles = {}, + multiple, + accept, + capture, + children, + directory, + openFileDialogOnClick, + onMouseEnter, + onMouseLeave, + hasControlInside, + ...otherProps + } = props; + + const [uid, setUid] = React.useState(getUid()); + const [reqs, setReqs] = React.useState>({}); + + const isMountedRef = React.useRef(false); + const inputRef = React.useRef(null); + + const abort = React.useCallback( + (file?: any) => { + if (file) { + const internalUid = file.uid ? file.uid : file; + if (reqs[internalUid]?.abort) { + reqs[internalUid].abort(); + } + setReqs(prev => { + const { [internalUid]: _, ...rest } = prev; + return rest; }); - }); - }; + } else { + Object.keys(reqs).forEach(key => { + if (reqs[key]?.abort) { + reqs[key].abort(); + } + setReqs(prev => { + const { [key]: _, ...rest } = prev; + return rest; + }); + }); + } + }, + [reqs], + ); + + React.useEffect(() => { + isMountedRef.current = true; + return () => { + isMountedRef.current = false; + abort(); + }; + }, [abort]); /** * Process file before upload. When all the file is ready, we start upload. */ - processFile = async (file: RcFile, fileList: RcFile[]): Promise => { - const { beforeUpload } = this.props; - + const processFile = async (file: RcFile, fileList: RcFile[]): Promise => { + const { beforeUpload } = props; let transformedFile: BeforeUploadFileType | void = file; if (beforeUpload) { try { transformedFile = await beforeUpload(file, fileList); - } catch (e) { + } catch { // Rejection will also trade as false transformedFile = false; } @@ -150,7 +109,7 @@ class AjaxUploader extends Component { } // Get latest action - const { action } = this.props; + const { action } = props; let mergedAction: string; if (typeof action === 'function') { mergedAction = await action(file); @@ -159,7 +118,7 @@ class AjaxUploader extends Component { } // Get latest data - const { data } = this.props; + const { data } = props; let mergedData: Record; if (typeof data === 'function') { mergedData = await data(file); @@ -193,17 +152,16 @@ class AjaxUploader extends Component { }; }; - post({ data, origin, action, parsedFile }: ParsedFileInfo) { - if (!this._isMounted) { + const post = ({ data, origin, action, parsedFile }: ParsedFileInfo) => { + if (!isMountedRef.current) { return; } - const { onStart, customRequest, name, headers, withCredentials, method } = this.props; + const { onStart, customRequest, headers, withCredentials, method } = props; - const { uid } = origin; const request = customRequest || defaultRequest; - const requestOption = { + const requestOption: UploadRequestOption = { action, filename: name, data, @@ -212,124 +170,142 @@ class AjaxUploader extends Component { withCredentials, method: method || 'post', onProgress: (e: UploadProgressEvent) => { - const { onProgress } = this.props; - onProgress?.(e, parsedFile); + props.onProgress?.(e, parsedFile); }, onSuccess: (ret: any, xhr: XMLHttpRequest) => { - const { onSuccess } = this.props; - onSuccess?.(ret, parsedFile, xhr); - - delete this.reqs[uid]; + props.onSuccess?.(ret, parsedFile, xhr); + setReqs(prev => { + const { [origin.uid]: _, ...rest } = prev; + return rest; + }); }, onError: (err: UploadRequestError, ret: any) => { - const { onError } = this.props; - onError?.(err, ret, parsedFile); - - delete this.reqs[uid]; + props.onError?.(err, ret, parsedFile); + setReqs(prev => { + const { [origin.uid]: _, ...rest } = prev; + return rest; + }); }, }; - onStart(origin); - this.reqs[uid] = request(requestOption); - } + setReqs(prev => ({ ...prev, [origin.uid]: request(requestOption) })); + }; - reset() { - this.setState({ - uid: getUid(), + const uploadFiles = (files: File[]) => { + const originFiles = [...files] as RcFile[]; + const postFiles = originFiles.map((file: RcFile & { uid?: string }) => { + // eslint-disable-next-line no-param-reassign + file.uid = getUid(); + return processFile(file, originFiles); }); - } - abort(file?: any) { - const { reqs } = this; - if (file) { - const uid = file.uid ? file.uid : file; - if (reqs[uid] && reqs[uid].abort) { - reqs[uid].abort(); - } - delete reqs[uid]; - } else { - Object.keys(reqs).forEach(uid => { - if (reqs[uid] && reqs[uid].abort) { - reqs[uid].abort(); - } - delete reqs[uid]; - }); + // Batch upload files + Promise.all(postFiles).then(fileList => { + props.onBatchStart?.( + fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })), + ); + fileList.filter(file => file.parsedFile !== null).forEach(file => post(file)); + }); + }; + + const onChange = (e: React.ChangeEvent) => { + const { files } = e.target; + const acceptedFiles = [...files].filter( + (file: RcFile) => !directory || attrAccept(file, accept), + ); + uploadFiles(acceptedFiles); + setUid(getUid()); + }; + + const onClick = ( + event: React.MouseEvent | React.KeyboardEvent, + ) => { + if (!inputRef.current) { + return; + } + const target = event.target as HTMLElement; + if (target?.tagName === 'BUTTON') { + const parent = inputRef.current.parentNode as HTMLInputElement; + parent.focus(); + target.blur(); } - } + inputRef.current.click(); + props.onClick?.(event); + }; - saveFileInput = (node: HTMLInputElement) => { - this.fileInput = node; + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onClick(e); + } }; - render() { - const { - component: Tag, - prefixCls, - className, - classNames = {}, - disabled, - id, - name, - style, - styles = {}, - multiple, - accept, - capture, - children, - directory, - openFileDialogOnClick, - onMouseEnter, - onMouseLeave, - hasControlInside, - ...otherProps - } = this.props; - const cls = clsx({ - [prefixCls]: true, - [`${prefixCls}-disabled`]: disabled, - [className]: className, - }); - // because input don't have directory/webkitdirectory type declaration - const dirProps: any = directory - ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } - : {}; - const events = disabled - ? {} - : { - onClick: openFileDialogOnClick ? this.onClick : () => {}, - onKeyDown: openFileDialogOnClick ? this.onKeyDown : () => {}, - onMouseEnter, - onMouseLeave, - onDrop: this.onFileDrop, - onDragOver: this.onFileDrop, - tabIndex: hasControlInside ? undefined : '0', - }; - return ( - - e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948 - key={this.state.uid} - style={{ display: 'none', ...styles.input }} - className={classNames.input} - accept={accept} - {...dirProps} - multiple={multiple} - onChange={this.onChange} - {...(capture != null ? { capture } : {})} - /> - {children} - - ); - } -} + const onFileDrop = async (e: React.DragEvent) => { + e.preventDefault(); + if (e.type === 'dragover') { + return; + } + if (directory) { + const files = await traverseFileTree( + Array.prototype.slice.call(e.dataTransfer.items), + (_file: RcFile) => attrAccept(_file, accept), + ); + uploadFiles(files); + } else { + let files = [...e.dataTransfer.files].filter((file: RcFile) => attrAccept(file, accept)); + if (multiple === false) { + files = files.slice(0, 1); + } + uploadFiles(files); + } + }; + + const cls = classnames(prefixCls, className, { + [`${prefixCls}-disabled`]: disabled, + }); + + // because input don't have directory/webkitdirectory type declaration + const dirProps: any = directory + ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } + : {}; + + const events = disabled + ? {} + : { + onClick: openFileDialogOnClick ? onClick : () => {}, + onKeyDown: openFileDialogOnClick ? onKeyDown : () => {}, + onMouseEnter, + onMouseLeave, + onDrop: onFileDrop, + onDragOver: onFileDrop, + tabIndex: hasControlInside ? undefined : 0, + }; + + return ( + + e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948 + key={uid} + style={{ display: 'none', ...styles.input }} + className={classNames.input} + accept={accept} + {...dirProps} + multiple={multiple} + onChange={onChange} + {...(capture != null ? { capture } : {})} + /> + {children} + + ); +}; export default AjaxUploader; diff --git a/src/Upload.tsx b/src/Upload.tsx index 23541e31..2024f023 100644 --- a/src/Upload.tsx +++ b/src/Upload.tsx @@ -1,42 +1,46 @@ -/* eslint react/prop-types:0 */ -import React, { Component } from 'react'; +import React from 'react'; import AjaxUpload from './AjaxUploader'; -import type { UploadProps, RcFile } from './interface'; +import type { UploadProps } from './interface'; function empty() {} -class Upload extends Component { - static defaultProps = { - component: 'span', - prefixCls: 'rc-upload', - data: {}, - headers: {}, - name: 'file', - multipart: false, - onStart: empty, - onError: empty, - onSuccess: empty, - multiple: false, - beforeUpload: null, - customRequest: null, - withCredentials: false, - openFileDialogOnClick: true, - hasControlInside: false, - }; - - private uploader: AjaxUpload; - - abort(file: RcFile) { - this.uploader.abort(file); - } - - saveUploader = (node: AjaxUpload) => { - this.uploader = node; - }; - - render() { - return ; - } -} +const Upload: React.FC> = props => { + const { + component = 'span', + prefixCls = 'rc-upload', + data = {}, + headers = {}, + name = 'file', + onStart = empty, + onError = empty, + onSuccess = empty, + multiple = false, + beforeUpload = null, + customRequest = null, + withCredentials = false, + openFileDialogOnClick = true, + hasControlInside = false, + ...rest + } = props; + return ( + + ); +}; export default Upload; diff --git a/src/attr-accept.ts b/src/attr-accept.ts index 7d1caec4..c9da0266 100644 --- a/src/attr-accept.ts +++ b/src/attr-accept.ts @@ -1,4 +1,4 @@ -import warning from 'rc-util/lib/warning'; +import warning from '@rc-component/util/lib/warning'; import type { RcFile } from './interface'; export default (file: RcFile, acceptedFiles: string | string[]) => { diff --git a/src/interface.tsx b/src/interface.tsx index 1c6e1d5e..4765c47b 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -34,9 +34,9 @@ export interface UploadProps openFileDialogOnClick?: boolean; prefixCls?: string; id?: string; - onMouseEnter?: (e: React.MouseEvent) => void; - onMouseLeave?: (e: React.MouseEvent) => void; - onClick?: (e: React.MouseEvent | React.KeyboardEvent) => void; + onMouseEnter?: (e: React.MouseEvent) => void; + onMouseLeave?: (e: React.MouseEvent) => void; + onClick?: (e: React.MouseEvent | React.KeyboardEvent) => void; classNames?: { input?: string; }; diff --git a/src/request.ts b/src/request.ts index 898847d0..96d175ca 100644 --- a/src/request.ts +++ b/src/request.ts @@ -17,12 +17,12 @@ function getBody(xhr: XMLHttpRequest) { try { return JSON.parse(text); - } catch (e) { + } catch { return text; } } -export default function upload(option: UploadRequestOption) { +function upload(option: UploadRequestOption) { // eslint-disable-next-line no-undef const xhr = new XMLHttpRequest(); @@ -105,3 +105,5 @@ export default function upload(option: UploadRequestOption) { }, }; } + +export default upload; diff --git a/src/traverseFileTree.ts b/src/traverseFileTree.ts index 6544ac58..7ae668f6 100644 --- a/src/traverseFileTree.ts +++ b/src/traverseFileTree.ts @@ -14,18 +14,21 @@ interface InternalDataTransferItem extends DataTransferItem { const traverseFileTree = async (files: InternalDataTransferItem[], isAccepted) => { const flattenFileList = []; const progressFileList = []; - files.forEach(file => progressFileList.push(file.webkitGetAsEntry() as any)); + + files.forEach(file => { + progressFileList.push(file.webkitGetAsEntry() as any); + }); async function readDirectory(directory: InternalDataTransferItem) { const dirReader = directory.createReader(); - const entries = []; + const entries: InternalDataTransferItem[] = []; while (true) { - const results = await new Promise((resolve) => { + const results = await new Promise(resolve => { dirReader.readEntries(resolve, () => resolve([])); }); const n = results.length; - + if (!n) { break; } diff --git a/src/uid.ts b/src/uid.ts index 3ad3f8e3..e57fcf19 100644 --- a/src/uid.ts +++ b/src/uid.ts @@ -1,7 +1,9 @@ const now = +new Date(); let index = 0; -export default function uid() { +function uid() { // eslint-disable-next-line no-plusplus return `rc-upload-${now}-${++index}`; } + +export default uid; diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index 344a61d2..4d7a129e 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -1,18 +1,18 @@ import { fireEvent, render } from '@testing-library/react'; -import { resetWarned } from 'rc-util/lib/warning'; +import { resetWarned } from '@rc-component/util/lib/warning'; import React from 'react'; import sinon from 'sinon'; import { format } from 'util'; import Upload, { type UploadProps } from '../src'; -const sleep = (timeout = 500) => new Promise(resolve => setTimeout(resolve, timeout)); +const sleep = (timeout = 500) => new Promise(resolve => setTimeout(resolve, timeout)); -function Item(name) { +function Item(name: string) { this.name = name; this.toString = () => this.name; } -const makeFileSystemEntry = item => { +const makeFileSystemEntry = (item: any) => { const isDirectory = Array.isArray(item.children); const ret = { isDirectory, @@ -50,13 +50,13 @@ const makeFileSystemEntryAsync = item => { return { async readEntries(handle, error) { await sleep(100); - + if (!first) { return handle([]); } if (item.error && first) { - return error && error(new Error('read file error')) + return error && error(new Error('read file error')); } first = false; @@ -377,16 +377,18 @@ describe('uploader', () => { }); it('should pass file to request', done => { - const fakeRequest = jest.fn((file) => { - expect(file).toEqual(expect.objectContaining({ - filename: 'file', // <= https://github.com/react-component/upload/pull/574 - file: expect.any(File), - method: 'post', - onError: expect.any(Function), - onProgress: expect.any(Function), - onSuccess: expect.any(Function), - data: expect.anything(), - })); + const fakeRequest = jest.fn(file => { + expect(file).toEqual( + expect.objectContaining({ + filename: 'file', // <= https://github.com/react-component/upload/pull/574 + file: expect.any(File), + method: 'post', + onError: expect.any(Function), + onProgress: expect.any(Function), + onSuccess: expect.any(Function), + data: expect.anything(), + }), + ); done(); }); @@ -563,14 +565,14 @@ describe('uploader', () => { fireEvent.drop(input, { dataTransfer: { items: [makeDataTransferItemAsync(files)] } }); const mockStart = jest.fn(); handlers.onStart = mockStart; - + setTimeout(() => { expect(mockStart.mock.calls.length).toBe(2); done(); }, 1000); }); - it('dragging and dropping files to upload through asynchronous file reading with some readEntries method throw error', (done) => { + it('dragging and dropping files to upload through asynchronous file reading with some readEntries method throw error', done => { const input = uploader.container.querySelector('input')!; const files = { @@ -593,7 +595,7 @@ describe('uploader', () => { name: '8.png', }, ], - } + }, ], }, { @@ -612,7 +614,7 @@ describe('uploader', () => { fireEvent.drop(input, { dataTransfer: { items: [makeDataTransferItemAsync(files)] } }); const mockStart = jest.fn(); handlers.onStart = mockStart; - + setTimeout(() => { expect(mockStart.mock.calls.length).toBe(1); done(); From ea157c0df953971c171a160bbcc2cf7d85b4942a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Fri, 31 Jan 2025 12:45:38 +0800 Subject: [PATCH 02/10] chore: update @rc-component/upload" --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 53a5fc08..65e8139c 100644 --- a/package.json +++ b/package.json @@ -69,5 +69,9 @@ "sinon": "^9.0.2", "typescript": "^5.3.3", "vinyl-fs": "^3.0.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } } From 0968ea4af85e4e2eadcfa9ef6d16930319105d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Sat, 1 Feb 2025 10:28:15 +0800 Subject: [PATCH 03/10] fix: fix --- src/AjaxUploader.tsx | 29 +++++++++++++++++++---------- src/Upload.tsx | 4 ++++ src/uid.ts | 4 ++-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 16f7ed8f..71c98135 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -204,7 +204,11 @@ const AjaxUploader: React.FC>> = p props.onBatchStart?.( fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })), ); - fileList.filter(file => file.parsedFile !== null).forEach(file => post(file)); + fileList + .filter(file => file.parsedFile !== null) + .forEach(file => { + post(file); + }); }); }; @@ -259,14 +263,8 @@ const AjaxUploader: React.FC>> = p } }; - const cls = classnames(prefixCls, className, { - [`${prefixCls}-disabled`]: disabled, - }); - // because input don't have directory/webkitdirectory type declaration - const dirProps: any = directory - ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } - : {}; + const dirProps = directory ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {}; const events = disabled ? {} @@ -277,11 +275,18 @@ const AjaxUploader: React.FC>> = p onMouseLeave, onDrop: onFileDrop, onDragOver: onFileDrop, - tabIndex: hasControlInside ? undefined : 0, }; return ( - + >> = p ); }; +if (process.env.NODE_ENV !== 'production') { + AjaxUploader.displayName = 'AjaxUploader'; +} + export default AjaxUploader; diff --git a/src/Upload.tsx b/src/Upload.tsx index 2024f023..a009f071 100644 --- a/src/Upload.tsx +++ b/src/Upload.tsx @@ -43,4 +43,8 @@ const Upload: React.FC> = props => { ); }; +if (process.env.NODE_ENV !== 'production') { + Upload.displayName = 'Upload'; +} + export default Upload; diff --git a/src/uid.ts b/src/uid.ts index e57fcf19..28523eea 100644 --- a/src/uid.ts +++ b/src/uid.ts @@ -1,8 +1,8 @@ -const now = +new Date(); +const now = Date.now(); + let index = 0; function uid() { - // eslint-disable-next-line no-plusplus return `rc-upload-${now}-${++index}`; } From 2bd4c6188034b94c9b7a624a6c95a561ab8e6003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Sat, 1 Feb 2025 11:20:04 +0800 Subject: [PATCH 04/10] fix: fix --- src/AjaxUploader.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 71c98135..e585732f 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ /* eslint react/no-is-mounted:0,react/sort-comp:0,react/prop-types:0 */ import classnames from 'classnames'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; @@ -80,10 +81,10 @@ const AjaxUploader: React.FC>> = p React.useEffect(() => { isMountedRef.current = true; return () => { - isMountedRef.current = false; abort(); + isMountedRef.current = false; }; - }, [abort]); + }, []); /** * Process file before upload. When all the file is ready, we start upload. @@ -251,15 +252,12 @@ const AjaxUploader: React.FC>> = p if (directory) { const files = await traverseFileTree( Array.prototype.slice.call(e.dataTransfer.items), - (_file: RcFile) => attrAccept(_file, accept), + (f: RcFile) => attrAccept(f, accept), ); uploadFiles(files); } else { - let files = [...e.dataTransfer.files].filter((file: RcFile) => attrAccept(file, accept)); - if (multiple === false) { - files = files.slice(0, 1); - } - uploadFiles(files); + const allFiles = [...e.dataTransfer.files].filter((file: RcFile) => attrAccept(file, accept)); + uploadFiles(multiple === false ? allFiles.slice(0, 1) : allFiles); } }; From 8cbe4c2569a7ff59f28d8262cae2b134d2b17a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Sat, 1 Feb 2025 11:40:22 +0800 Subject: [PATCH 05/10] fix: fix --- src/AjaxUploader.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index e585732f..a78dc523 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -46,7 +46,7 @@ const AjaxUploader: React.FC>> = p ...otherProps } = props; - const [uid, setUid] = React.useState(getUid()); + const [uid, setUid] = React.useState(getUid); const [reqs, setReqs] = React.useState>({}); const isMountedRef = React.useRef(false); @@ -68,11 +68,8 @@ const AjaxUploader: React.FC>> = p if (reqs[key]?.abort) { reqs[key].abort(); } - setReqs(prev => { - const { [key]: _, ...rest } = prev; - return rest; - }); }); + setReqs({}); } }, [reqs], From c6451d45e79a1621b31a9f3b5f6568dc0232e294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Sat, 1 Feb 2025 11:55:33 +0800 Subject: [PATCH 06/10] fix: fix --- src/AjaxUploader.tsx | 50 +++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index a78dc523..2e6b6692 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -47,33 +47,27 @@ const AjaxUploader: React.FC>> = p } = props; const [uid, setUid] = React.useState(getUid); - const [reqs, setReqs] = React.useState>({}); const isMountedRef = React.useRef(false); const inputRef = React.useRef(null); + const reqsRef = React.useRef>>({}); - const abort = React.useCallback( - (file?: any) => { - if (file) { - const internalUid = file.uid ? file.uid : file; - if (reqs[internalUid]?.abort) { - reqs[internalUid].abort(); - } - setReqs(prev => { - const { [internalUid]: _, ...rest } = prev; - return rest; - }); - } else { - Object.keys(reqs).forEach(key => { - if (reqs[key]?.abort) { - reqs[key].abort(); - } - }); - setReqs({}); + const abort = React.useCallback((file?: any) => { + if (file) { + const internalUid = file.uid ? file.uid : file; + if (reqsRef.current[internalUid]?.abort) { + reqsRef.current[internalUid].abort(); } - }, - [reqs], - ); + reqsRef.current[internalUid] = undefined; + } else { + Object.keys(reqsRef.current).forEach(key => { + if (reqsRef.current[key]?.abort) { + reqsRef.current[key].abort(); + } + }); + reqsRef.current = {}; + } + }, []); React.useEffect(() => { isMountedRef.current = true; @@ -172,21 +166,15 @@ const AjaxUploader: React.FC>> = p }, onSuccess: (ret: any, xhr: XMLHttpRequest) => { props.onSuccess?.(ret, parsedFile, xhr); - setReqs(prev => { - const { [origin.uid]: _, ...rest } = prev; - return rest; - }); + reqsRef.current[origin.uid] = undefined; }, onError: (err: UploadRequestError, ret: any) => { props.onError?.(err, ret, parsedFile); - setReqs(prev => { - const { [origin.uid]: _, ...rest } = prev; - return rest; - }); + reqsRef.current[origin.uid] = undefined; }, }; onStart(origin); - setReqs(prev => ({ ...prev, [origin.uid]: request(requestOption) })); + reqsRef.current[origin.uid] = request(requestOption); }; const uploadFiles = (files: File[]) => { From a79aed54f32a04366ae195afa3178bfeef2555ff Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Sun, 6 Apr 2025 20:58:36 +0800 Subject: [PATCH 07/10] fix: fix --- src/AjaxUploader.tsx | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 2e6b6692..a738a0d1 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -40,9 +40,17 @@ const AjaxUploader: React.FC>> = p children, directory, openFileDialogOnClick, + hasControlInside, + action, + headers, + withCredentials, + method, onMouseEnter, onMouseLeave, - hasControlInside, + data, + beforeUpload, + onStart, + customRequest, ...otherProps } = props; @@ -81,7 +89,6 @@ const AjaxUploader: React.FC>> = p * Process file before upload. When all the file is ready, we start upload. */ const processFile = async (file: RcFile, fileList: RcFile[]): Promise => { - const { beforeUpload } = props; let transformedFile: BeforeUploadFileType | void = file; if (beforeUpload) { try { @@ -100,8 +107,6 @@ const AjaxUploader: React.FC>> = p } } - // Get latest action - const { action } = props; let mergedAction: string; if (typeof action === 'function') { mergedAction = await action(file); @@ -109,8 +114,6 @@ const AjaxUploader: React.FC>> = p mergedAction = action; } - // Get latest data - const { data } = props; let mergedData: Record; if (typeof data === 'function') { mergedData = await data(file); @@ -144,19 +147,19 @@ const AjaxUploader: React.FC>> = p }; }; - const post = ({ data, origin, action, parsedFile }: ParsedFileInfo) => { + const post = (info: ParsedFileInfo) => { if (!isMountedRef.current) { return; } - const { onStart, customRequest, headers, withCredentials, method } = props; + const { origin, parsedFile } = info; const request = customRequest || defaultRequest; const requestOption: UploadRequestOption = { - action, + action: info.action, filename: name, - data, + data: info.data, file: parsedFile, headers, withCredentials, @@ -266,9 +269,7 @@ const AjaxUploader: React.FC>> = p style={style} role={disabled || hasControlInside ? undefined : 'button'} tabIndex={disabled || hasControlInside ? undefined : 0} - className={classnames(prefixCls, className, { - [`${prefixCls}-disabled`]: disabled, - })} + className={classnames(prefixCls, className, { [`${prefixCls}-disabled`]: disabled })} > Date: Sun, 6 Apr 2025 21:36:10 +0800 Subject: [PATCH 08/10] fix: fix --- src/AjaxUploader.tsx | 17 ++++++----------- src/Upload.tsx | 4 ++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index a738a0d1..fb4925b9 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -54,7 +54,7 @@ const AjaxUploader: React.FC>> = p ...otherProps } = props; - const [uid, setUid] = React.useState(getUid); + const [uid, setUid] = React.useState(getUid()); const isMountedRef = React.useRef(false); const inputRef = React.useRef(null); @@ -80,8 +80,8 @@ const AjaxUploader: React.FC>> = p React.useEffect(() => { isMountedRef.current = true; return () => { - abort(); isMountedRef.current = false; + abort(); }; }, []); @@ -91,10 +91,9 @@ const AjaxUploader: React.FC>> = p const processFile = async (file: RcFile, fileList: RcFile[]): Promise => { let transformedFile: BeforeUploadFileType | void = file; if (beforeUpload) { - try { + if (typeof beforeUpload === 'function') { transformedFile = await beforeUpload(file, fileList); - } catch { - // Rejection will also trade as false + } else { transformedFile = false; } if (transformedFile === false) { @@ -193,11 +192,7 @@ const AjaxUploader: React.FC>> = p props.onBatchStart?.( fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })), ); - fileList - .filter(file => file.parsedFile !== null) - .forEach(file => { - post(file); - }); + fileList.filter(file => file.parsedFile !== null).forEach(file => post(file)); }); }; @@ -217,7 +212,7 @@ const AjaxUploader: React.FC>> = p return; } const target = event.target as HTMLElement; - if (target?.tagName === 'BUTTON') { + if (target?.tagName.toUpperCase() === 'BUTTON') { const parent = inputRef.current.parentNode as HTMLInputElement; parent.focus(); target.blur(); diff --git a/src/Upload.tsx b/src/Upload.tsx index a009f071..44caf5a0 100644 --- a/src/Upload.tsx +++ b/src/Upload.tsx @@ -15,8 +15,8 @@ const Upload: React.FC> = props => { onError = empty, onSuccess = empty, multiple = false, - beforeUpload = null, - customRequest = null, + beforeUpload, + customRequest, withCredentials = false, openFileDialogOnClick = true, hasControlInside = false, From 0e4a93519d358ea5296463ee92bef424aada3c51 Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Sun, 6 Apr 2025 21:42:00 +0800 Subject: [PATCH 09/10] fix: fix --- src/AjaxUploader.tsx | 1 - src/Upload.tsx | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index fb4925b9..2518eee4 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -1,5 +1,4 @@ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint react/no-is-mounted:0,react/sort-comp:0,react/prop-types:0 */ import classnames from 'classnames'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; import React from 'react'; diff --git a/src/Upload.tsx b/src/Upload.tsx index 44caf5a0..36275390 100644 --- a/src/Upload.tsx +++ b/src/Upload.tsx @@ -20,7 +20,8 @@ const Upload: React.FC> = props => { withCredentials = false, openFileDialogOnClick = true, hasControlInside = false, - ...rest + children, + ...otherProps } = props; return ( > = props => { withCredentials={withCredentials} openFileDialogOnClick={openFileDialogOnClick} hasControlInside={hasControlInside} - {...rest} - /> + {...otherProps} + > + {children} + ); }; From fa1888de5171cdde8386acbd2339e10891eac9bd Mon Sep 17 00:00:00 2001 From: lijianan <574980606@qq.com> Date: Mon, 7 Apr 2025 01:06:28 +0800 Subject: [PATCH 10/10] fix: fix --- package.json | 4 ++-- tests/setup.js | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index dcb5c9df..e488acf9 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "vinyl-fs": "^4.0.0" }, "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } } diff --git a/tests/setup.js b/tests/setup.js index 154da504..6036b841 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,7 +1,2 @@ global.requestAnimationFrame = cb => setTimeout(cb, 0); require('regenerator-runtime'); - -const Enzyme = require('enzyme'); -const Adapter = require('enzyme-adapter-react-16'); - -Enzyme.configure({ adapter: new Adapter() });