Skip to content
Merged
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
54 changes: 40 additions & 14 deletions src/AjaxUploader.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -30,12 +31,36 @@ class AjaxUploader extends Component<UploadProps> {

private _isMounted: boolean;

onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
private filterFile = (file: RcFile | File, force = false) => {
const { accept, directory } = this.props;

let filterFn: Exclude<AcceptConfig['filter'], 'native'>;
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;
}
}

const mergedFilter =
filterFn ||
(directory || force
? (currentFile: RcFile) => attrAccept(currentFile, acceptFormat)
: () => true);
return mergedFilter(file as RcFile);
};

onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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();
};
Expand Down Expand Up @@ -67,7 +92,7 @@ class AjaxUploader extends Component<UploadProps> {
};

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 || [])];
Expand All @@ -77,12 +102,10 @@ class AjaxUploader extends Component<UploadProps> {
}

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);
Expand Down Expand Up @@ -322,17 +345,20 @@ class AjaxUploader extends Component<UploadProps> {
capture,
children,
directory,
folder,
openFileDialogOnClick,
onMouseEnter,
onMouseLeave,
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 || folder ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {};
const dirProps: any = directory
? { directory: 'directory', webkitdirectory: 'webkitdirectory' }
: {};
const events = disabled
? {}
: {
Expand Down Expand Up @@ -361,7 +387,7 @@ class AjaxUploader extends Component<UploadProps> {
key={this.state.uid}
style={{ display: 'none', ...styles.input }}
className={classNames.input}
accept={accept}
accept={acceptFormat}
{...dirProps}
multiple={multiple}
onChange={this.onChange}
Expand Down
11 changes: 7 additions & 4 deletions src/interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@ export type BeforeUploadFileType = File | Blob | boolean | string;

export type Action = string | ((file: RcFile) => string | PromiseLike<string>);

export type AcceptConfig = {
format: string;
filter?: 'native' | ((file: RcFile) => boolean);
};

export interface UploadProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onError' | 'onProgress'> {
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onError' | 'onProgress' | 'accept'> {
name?: string;
style?: React.CSSProperties;
className?: string;
disabled?: boolean;
component?: React.ComponentType<any> | string;
action?: Action;
method?: UploadRequestMethod;
/** @deprecated Please use `folder` instead */
directory?: boolean;
folder?: boolean;
data?: Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>);
headers?: UploadRequestHeader;
accept?: string;
accept?: string | AcceptConfig;
multiple?: boolean;
onBatchStart?: (
fileList: { file: RcFile; parsedFile: Exclude<BeforeUploadFileType, boolean> }[],
Expand Down
73 changes: 63 additions & 10 deletions tests/uploader.spec.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -1038,18 +1038,71 @@ describe('uploader', () => {
directory: false,
},
);
});

it('should trigger beforeUpload when uploading non-accepted files in folder mode', () => {
const beforeUpload = jest.fn();
const { container } = render(<Upload accept=".png" folder beforeUpload={beforeUpload} />);
describe('AcceptConfig', () => {
let uploader: ReturnType<typeof render>;
const handlers: UploadProps = {};

fireEvent.change(container.querySelector('input')!, {
target: {
files: [new File([], 'bamboo.png'), new File([], 'light.jpg')],
},
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(<Upload {...props} accept={accept} />);
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);
});
expect(beforeUpload).toHaveBeenCalledTimes(2);
});
}

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', () => {
Expand Down