@availity/mui-file-selector 1.6.6 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +84 -1
- package/dist/index.d.mts +81 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.js +558 -112
- package/dist/index.mjs +531 -94
- package/package.json +6 -6
- package/src/index.ts +3 -0
- package/src/lib/Dropzone.tsx +4 -4
- package/src/lib/Dropzone2.test.tsx +28 -0
- package/src/lib/Dropzone2.tsx +244 -0
- package/src/lib/FileList2.test.tsx +73 -0
- package/src/lib/FileList2.tsx +118 -0
- package/src/lib/FileSelector.tsx +1 -1
- package/src/lib/FileSelector2.stories.tsx +253 -0
- package/src/lib/FileSelector2.test.tsx +23 -0
- package/src/lib/FileSelector2.tsx +209 -0
- package/src/lib/util.test.ts +16 -0
- package/src/lib/util.ts +15 -4
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Each exported component in the package should have its own stories file
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
5
|
+
import { Paper } from '@availity/mui-paper';
|
|
6
|
+
import { Alert, AlertProps, AlertTitle } from '@availity/mui-alert';
|
|
7
|
+
import { Grid } from '@availity/mui-layout';
|
|
8
|
+
import { Button } from '@availity/mui-button';
|
|
9
|
+
import Collapse from '@mui/material/Collapse';
|
|
10
|
+
|
|
11
|
+
import { FileSelector2, FileSelector2Props } from './FileSelector2';
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof FileSelector2> = {
|
|
14
|
+
title: 'Components/File Selector/FileSelector2',
|
|
15
|
+
component: FileSelector2,
|
|
16
|
+
tags: ['autodocs'],
|
|
17
|
+
args: {
|
|
18
|
+
name: 'file-selector-2',
|
|
19
|
+
allowedFileTypes: ['.txt', '.png', '.pdf'],
|
|
20
|
+
clientId: '123',
|
|
21
|
+
customerId: '456',
|
|
22
|
+
bucketId: '789',
|
|
23
|
+
uploadOptions: {
|
|
24
|
+
retryDelays: [],
|
|
25
|
+
},
|
|
26
|
+
maxFiles: 2,
|
|
27
|
+
maxSize: 1 * 1024 * 1024, // 1MB
|
|
28
|
+
enableDropArea: true,
|
|
29
|
+
isCloud: true,
|
|
30
|
+
multiple: true,
|
|
31
|
+
},
|
|
32
|
+
argTypes: {
|
|
33
|
+
customSizeMessage: {
|
|
34
|
+
control: 'text',
|
|
35
|
+
},
|
|
36
|
+
customTypesMessage: {
|
|
37
|
+
control: 'text',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
|
|
44
|
+
const DismissableAlert = (props: AlertProps) => {
|
|
45
|
+
const [visible, setVisible] = useState(true);
|
|
46
|
+
|
|
47
|
+
const onClose = () => {
|
|
48
|
+
setVisible(false);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Collapse in={visible}>
|
|
53
|
+
<Alert onClose={onClose} {...props} />
|
|
54
|
+
</Collapse>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const _FileSelector2: StoryObj<typeof FileSelector2> = {
|
|
59
|
+
render: (props: FileSelector2Props) => {
|
|
60
|
+
const methods = useForm({
|
|
61
|
+
defaultValues: {
|
|
62
|
+
[props.name]: [] as File[],
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const uploads = methods.watch(props.name);
|
|
67
|
+
|
|
68
|
+
const handleOnSubmit = (values: Record<string, File[]>) => {
|
|
69
|
+
if (values[props.name].length === 0) return;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Paper sx={{ padding: '2rem' }}>
|
|
74
|
+
<FormProvider {...methods}>
|
|
75
|
+
<form onSubmit={methods.handleSubmit(handleOnSubmit)}>
|
|
76
|
+
<FileSelector2 {...props}>
|
|
77
|
+
<DismissableAlert severity="warning">
|
|
78
|
+
<AlertTitle>Make an Appeal</AlertTitle>
|
|
79
|
+
This is an example alert. It is not part of the component. `children` you pass to the component will
|
|
80
|
+
show up here.
|
|
81
|
+
</DismissableAlert>
|
|
82
|
+
</FileSelector2>
|
|
83
|
+
{uploads.length > 0 && (
|
|
84
|
+
<Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
|
|
85
|
+
<Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
|
86
|
+
Submit
|
|
87
|
+
</Button>
|
|
88
|
+
</Grid>
|
|
89
|
+
)}
|
|
90
|
+
</form>
|
|
91
|
+
</FormProvider>
|
|
92
|
+
</Paper>
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Set `enableDropzone` to `false` for a button only file selector. */
|
|
98
|
+
export const _FileSelector2ButtonOnly: StoryObj<typeof FileSelector2> = {
|
|
99
|
+
render: (props: FileSelector2Props) => {
|
|
100
|
+
const methods = useForm({
|
|
101
|
+
defaultValues: {
|
|
102
|
+
[props.name]: [] as File[],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const uploads = methods.watch(props.name);
|
|
107
|
+
|
|
108
|
+
const handleOnSubmit = (values: Record<string, File[]>) => {
|
|
109
|
+
if (values[props.name].length === 0) return;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Paper sx={{ padding: '2rem' }}>
|
|
114
|
+
<FormProvider {...methods}>
|
|
115
|
+
<form onSubmit={methods.handleSubmit(handleOnSubmit)}>
|
|
116
|
+
<FileSelector2 {...props} enableDropArea={false} />
|
|
117
|
+
{uploads.length > 0 && (
|
|
118
|
+
<Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
|
|
119
|
+
<Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
|
120
|
+
Submit
|
|
121
|
+
</Button>
|
|
122
|
+
</Grid>
|
|
123
|
+
)}
|
|
124
|
+
</form>
|
|
125
|
+
</FormProvider>
|
|
126
|
+
</Paper>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Upload password protected files. _For this example, the password for any file is '1234'_ */
|
|
132
|
+
export const _FileSelector2Encrypted: StoryObj<typeof FileSelector2> = {
|
|
133
|
+
render: (props: Omit<FileSelector2Props, 'bucketId'>) => {
|
|
134
|
+
const methods = useForm({
|
|
135
|
+
defaultValues: {
|
|
136
|
+
[props.name]: [] as File[],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const uploads = methods.watch(props.name);
|
|
141
|
+
|
|
142
|
+
const handleOnSubmit = (values: Record<string, File[]>) => {
|
|
143
|
+
if (values[props.name].length === 0) return;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Paper sx={{ padding: '2rem' }}>
|
|
148
|
+
<FormProvider {...methods}>
|
|
149
|
+
<form onSubmit={methods.handleSubmit(handleOnSubmit)}>
|
|
150
|
+
<FileSelector2 {...props} bucketId="enc" />
|
|
151
|
+
{uploads.length > 0 && (
|
|
152
|
+
<Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
|
|
153
|
+
<Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
|
154
|
+
Submit
|
|
155
|
+
</Button>
|
|
156
|
+
</Grid>
|
|
157
|
+
)}
|
|
158
|
+
</form>
|
|
159
|
+
</FormProvider>
|
|
160
|
+
</Paper>
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
args: {
|
|
164
|
+
bucketId: 'enc',
|
|
165
|
+
},
|
|
166
|
+
argTypes: {
|
|
167
|
+
bucketId: { control: false },
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const _FileSelector2CustomTypesMessage: StoryObj<typeof FileSelector2> = {
|
|
172
|
+
render: (props: FileSelector2Props) => {
|
|
173
|
+
const methods = useForm({
|
|
174
|
+
defaultValues: {
|
|
175
|
+
[props.name]: [] as File[],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const uploads = methods.watch(props.name);
|
|
180
|
+
|
|
181
|
+
const handleOnSubmit = (values: Record<string, File[]>) => {
|
|
182
|
+
if (values[props.name].length === 0) return;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<Paper sx={{ padding: '2rem' }}>
|
|
187
|
+
<FormProvider {...methods}>
|
|
188
|
+
<form onSubmit={methods.handleSubmit(handleOnSubmit)}>
|
|
189
|
+
<FileSelector2 {...props}>
|
|
190
|
+
<DismissableAlert severity="warning">
|
|
191
|
+
<AlertTitle>Make an Appeal</AlertTitle>
|
|
192
|
+
This is an example alert. It is not part of the component. `children` you pass to the component will
|
|
193
|
+
show up here.
|
|
194
|
+
</DismissableAlert>
|
|
195
|
+
</FileSelector2>
|
|
196
|
+
{uploads.length > 0 && (
|
|
197
|
+
<Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
|
|
198
|
+
<Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
|
199
|
+
Submit
|
|
200
|
+
</Button>
|
|
201
|
+
</Grid>
|
|
202
|
+
)}
|
|
203
|
+
</form>
|
|
204
|
+
</FormProvider>
|
|
205
|
+
</Paper>
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
args: {
|
|
209
|
+
customTypesMessage: 'Only cool file types allowed',
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const _FileSelector2CustomSizeMessage: StoryObj<typeof FileSelector2> = {
|
|
214
|
+
render: (props: FileSelector2Props) => {
|
|
215
|
+
const methods = useForm({
|
|
216
|
+
defaultValues: {
|
|
217
|
+
[props.name]: [] as File[],
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const uploads = methods.watch(props.name);
|
|
222
|
+
|
|
223
|
+
const handleOnSubmit = (values: Record<string, File[]>) => {
|
|
224
|
+
if (values[props.name].length === 0) return;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Paper sx={{ padding: '2rem' }}>
|
|
229
|
+
<FormProvider {...methods}>
|
|
230
|
+
<form onSubmit={methods.handleSubmit(handleOnSubmit)}>
|
|
231
|
+
<FileSelector2 {...props}>
|
|
232
|
+
<DismissableAlert severity="warning">
|
|
233
|
+
<AlertTitle>Make an Appeal</AlertTitle>
|
|
234
|
+
This is an example alert. It is not part of the component. `children` you pass to the component will
|
|
235
|
+
show up here.
|
|
236
|
+
</DismissableAlert>
|
|
237
|
+
</FileSelector2>
|
|
238
|
+
{uploads.length > 0 && (
|
|
239
|
+
<Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
|
|
240
|
+
<Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
|
241
|
+
Submit
|
|
242
|
+
</Button>
|
|
243
|
+
</Grid>
|
|
244
|
+
)}
|
|
245
|
+
</form>
|
|
246
|
+
</FormProvider>
|
|
247
|
+
</Paper>
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
args: {
|
|
251
|
+
customSizeMessage: 'Only huge files allowed. ',
|
|
252
|
+
},
|
|
253
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { useForm, FormProvider } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
import { FileSelector2 } from './FileSelector2';
|
|
6
|
+
|
|
7
|
+
const TestForm = ({ children }: { children: ReactNode }) => {
|
|
8
|
+
const methods = useForm();
|
|
9
|
+
|
|
10
|
+
return <FormProvider {...methods}>{children}</FormProvider>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('FileSelector2', () => {
|
|
14
|
+
test('should render successfully', () => {
|
|
15
|
+
const { getByText } = render(
|
|
16
|
+
<TestForm>
|
|
17
|
+
<FileSelector2 name="test" bucketId="test" customerId="123" clientId="test" maxSize={1000} maxFiles={1} />
|
|
18
|
+
</TestForm>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(getByText('Upload file')).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
|
3
|
+
import type { FileRejection } from 'react-dropzone/typings/react-dropzone';
|
|
4
|
+
import type { default as Upload } from '@availity/upload-core';
|
|
5
|
+
import { Grid } from '@availity/mui-layout';
|
|
6
|
+
import { Typography } from '@availity/mui-typography';
|
|
7
|
+
import { Alert, AlertTitle } from '@availity/mui-alert';
|
|
8
|
+
|
|
9
|
+
import { Dropzone2 } from './Dropzone2';
|
|
10
|
+
import { ErrorAlert } from './ErrorAlert';
|
|
11
|
+
import { FileList2 } from './FileList2';
|
|
12
|
+
import { FileTypesMessage } from './FileTypesMessage';
|
|
13
|
+
import { HeaderMessage } from './HeaderMessage';
|
|
14
|
+
import type { Options } from './useUploadCore';
|
|
15
|
+
|
|
16
|
+
import { CLOUD_URL } from './FileSelector';
|
|
17
|
+
import type { FileSelectorProps } from './FileSelector';
|
|
18
|
+
|
|
19
|
+
export type FileSelector2Props = Omit<FileSelectorProps, 'onUploadRemove' | 'queryOptions'> & {
|
|
20
|
+
onUploadRemove?: (uploads: Upload[], removedUploadId: string) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `<FileSelector2 />` is the future of the the `<FileSelector />`
|
|
25
|
+
* component. In a future major release, the `<FileSelector />` and
|
|
26
|
+
* `<FileSelector2 />` components will be consolidated into a single
|
|
27
|
+
* component.
|
|
28
|
+
*
|
|
29
|
+
* `<FileSelector2 />` removes the reliance on `@tanstack/react-query`. The
|
|
30
|
+
* `Upload` object can now be accessed from the form state.
|
|
31
|
+
*/
|
|
32
|
+
export const FileSelector2 = ({
|
|
33
|
+
name,
|
|
34
|
+
allowedFileNameCharacters,
|
|
35
|
+
allowedFileTypes = [],
|
|
36
|
+
bucketId,
|
|
37
|
+
clientId,
|
|
38
|
+
children,
|
|
39
|
+
customSizeMessage,
|
|
40
|
+
customTypesMessage,
|
|
41
|
+
customerId,
|
|
42
|
+
customFileRow,
|
|
43
|
+
disabled = false,
|
|
44
|
+
enableDropArea = true,
|
|
45
|
+
endpoint,
|
|
46
|
+
isCloud,
|
|
47
|
+
label = 'Upload file',
|
|
48
|
+
maxFiles,
|
|
49
|
+
maxSize,
|
|
50
|
+
multiple = true,
|
|
51
|
+
onChange,
|
|
52
|
+
onDrop,
|
|
53
|
+
onUploadRemove,
|
|
54
|
+
uploadOptions,
|
|
55
|
+
validator,
|
|
56
|
+
disableRemove,
|
|
57
|
+
}: FileSelector2Props) => {
|
|
58
|
+
const [totalSize, setTotalSize] = useState(0);
|
|
59
|
+
const [fileRejections, setFileRejections] = useState<(FileRejection & { id: number })[]>([]);
|
|
60
|
+
|
|
61
|
+
const formMethods = useFormContext();
|
|
62
|
+
|
|
63
|
+
const options: Options = {
|
|
64
|
+
...uploadOptions,
|
|
65
|
+
bucketId,
|
|
66
|
+
customerId,
|
|
67
|
+
clientId,
|
|
68
|
+
fileTypes: allowedFileTypes,
|
|
69
|
+
maxSize,
|
|
70
|
+
allowedFileNameCharacters,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Endpoint is set by default in upload-core so check if it exists before passing `undefined`
|
|
74
|
+
if (endpoint) options.endpoint = endpoint;
|
|
75
|
+
// Override endpoint if using the cloud
|
|
76
|
+
if (isCloud) options.endpoint = CLOUD_URL;
|
|
77
|
+
|
|
78
|
+
const handleOnRemoveFile = (uploadId: string, upload: Upload) => {
|
|
79
|
+
const prevFiles: Upload[] = formMethods.getValues(name);
|
|
80
|
+
const newFiles = prevFiles.filter((prev) => prev.file.name !== upload.file.name);
|
|
81
|
+
|
|
82
|
+
if (newFiles.length !== prevFiles.length) {
|
|
83
|
+
const removedFile = prevFiles.find((prev) => prev.file.name === upload.file.name);
|
|
84
|
+
|
|
85
|
+
// Stop upload
|
|
86
|
+
try {
|
|
87
|
+
upload.abort();
|
|
88
|
+
} catch {
|
|
89
|
+
console.error('Encountered an issue stopping the file upload');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Remove from context and cache
|
|
93
|
+
formMethods.setValue(name, newFiles);
|
|
94
|
+
|
|
95
|
+
if (removedFile?.file.size) setTotalSize(totalSize - removedFile.file.size);
|
|
96
|
+
|
|
97
|
+
if (onUploadRemove) onUploadRemove(newFiles, uploadId);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const uploads = (formMethods.watch(name) as Upload[]) || [];
|
|
102
|
+
|
|
103
|
+
const handleRemoveRejection = (id: number) => {
|
|
104
|
+
const rejections = fileRejections.filter((value) => value.id !== id);
|
|
105
|
+
setFileRejections(rejections);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const TOO_MANY_FILES_CODE = 'too-many-files';
|
|
109
|
+
|
|
110
|
+
// Extract too-many-files rejections
|
|
111
|
+
const tooManyFilesRejections = fileRejections.filter((rejection) =>
|
|
112
|
+
rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Extract other rejections
|
|
116
|
+
const otherRejections = fileRejections.filter(
|
|
117
|
+
(rejection) => !rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
{enableDropArea ? (
|
|
123
|
+
<>
|
|
124
|
+
{label ? <Typography marginBottom="4px">{label}</Typography> : null}
|
|
125
|
+
<Dropzone2
|
|
126
|
+
name={name}
|
|
127
|
+
allowedFileTypes={allowedFileTypes}
|
|
128
|
+
disabled={disabled}
|
|
129
|
+
enableDropArea={enableDropArea}
|
|
130
|
+
maxFiles={maxFiles}
|
|
131
|
+
maxSize={maxSize}
|
|
132
|
+
multiple={multiple}
|
|
133
|
+
onChange={onChange}
|
|
134
|
+
onDrop={onDrop}
|
|
135
|
+
setFileRejections={setFileRejections}
|
|
136
|
+
setTotalSize={setTotalSize}
|
|
137
|
+
uploadOptions={options}
|
|
138
|
+
validator={validator}
|
|
139
|
+
/>
|
|
140
|
+
<FileTypesMessage
|
|
141
|
+
allowedFileTypes={allowedFileTypes}
|
|
142
|
+
maxFileSize={maxSize}
|
|
143
|
+
customSizeMessage={customSizeMessage}
|
|
144
|
+
customTypesMessage={customTypesMessage}
|
|
145
|
+
variant="caption"
|
|
146
|
+
/>
|
|
147
|
+
{children}
|
|
148
|
+
</>
|
|
149
|
+
) : (
|
|
150
|
+
<Grid container rowSpacing={3} flexDirection="column">
|
|
151
|
+
<Grid>
|
|
152
|
+
<HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
|
|
153
|
+
<FileTypesMessage
|
|
154
|
+
allowedFileTypes={allowedFileTypes}
|
|
155
|
+
customSizeMessage={customSizeMessage}
|
|
156
|
+
customTypesMessage={customTypesMessage}
|
|
157
|
+
variant="body2"
|
|
158
|
+
/>
|
|
159
|
+
</Grid>
|
|
160
|
+
{children ? <Grid>{children}</Grid> : null}
|
|
161
|
+
<Grid>
|
|
162
|
+
<Dropzone2
|
|
163
|
+
name={name}
|
|
164
|
+
allowedFileTypes={allowedFileTypes}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
enableDropArea={enableDropArea}
|
|
167
|
+
maxFiles={maxFiles}
|
|
168
|
+
maxSize={maxSize}
|
|
169
|
+
multiple={multiple}
|
|
170
|
+
onChange={onChange}
|
|
171
|
+
onDrop={onDrop}
|
|
172
|
+
setFileRejections={setFileRejections}
|
|
173
|
+
setTotalSize={setTotalSize}
|
|
174
|
+
uploadOptions={options}
|
|
175
|
+
validator={validator}
|
|
176
|
+
/>
|
|
177
|
+
</Grid>
|
|
178
|
+
</Grid>
|
|
179
|
+
)}
|
|
180
|
+
{tooManyFilesRejections.length > 0 && (
|
|
181
|
+
<Alert
|
|
182
|
+
severity="error"
|
|
183
|
+
onClose={() => tooManyFilesRejections.forEach((rejection) => handleRemoveRejection(rejection.id))}
|
|
184
|
+
>
|
|
185
|
+
<AlertTitle>Items not allowed.</AlertTitle>
|
|
186
|
+
Too many files are selected for upload, maximum {maxFiles} allowed.
|
|
187
|
+
</Alert>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{otherRejections.length > 0 &&
|
|
191
|
+
otherRejections.map((rejection) => (
|
|
192
|
+
<ErrorAlert
|
|
193
|
+
key={rejection.id}
|
|
194
|
+
errors={rejection.errors}
|
|
195
|
+
fileName={rejection.file.name}
|
|
196
|
+
id={rejection.id}
|
|
197
|
+
onClose={() => handleRemoveRejection(rejection.id)}
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
200
|
+
<FileList2
|
|
201
|
+
uploads={uploads || []}
|
|
202
|
+
options={options}
|
|
203
|
+
onRemoveFile={handleOnRemoveFile}
|
|
204
|
+
customFileRow={customFileRow}
|
|
205
|
+
disableRemove={disableRemove}
|
|
206
|
+
/>
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { dedupeErrors } from './util';
|
|
2
|
+
|
|
3
|
+
describe('util', () => {
|
|
4
|
+
it('should remove duplicate errors', () => {
|
|
5
|
+
expect(
|
|
6
|
+
dedupeErrors([
|
|
7
|
+
{ message: 'error 1', code: '123' },
|
|
8
|
+
{ message: 'error 2', code: '1234' },
|
|
9
|
+
{ message: 'error 1', code: '123' },
|
|
10
|
+
])
|
|
11
|
+
).toEqual([
|
|
12
|
+
{ message: 'error 1', code: '123' },
|
|
13
|
+
{ message: 'error 2', code: '1234' },
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
});
|
package/src/lib/util.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
FilePowerpointIcon,
|
|
11
11
|
FileWordIcon,
|
|
12
12
|
} from '@availity/mui-icon';
|
|
13
|
+
import type { FileError } from 'react-dropzone';
|
|
13
14
|
|
|
14
15
|
export function formatBytes(bytes: number, decimals = 2) {
|
|
15
16
|
if (!+bytes) return '0 Bytes';
|
|
@@ -22,7 +23,6 @@ export function formatBytes(bytes: number, decimals = 2) {
|
|
|
22
23
|
|
|
23
24
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
26
|
export const FILE_EXT_ICONS = {
|
|
27
27
|
png: FileImageIcon,
|
|
28
28
|
jpg: FileImageIcon,
|
|
@@ -48,9 +48,20 @@ export type FileExtensionKey = keyof typeof FILE_EXT_ICONS;
|
|
|
48
48
|
|
|
49
49
|
export const isValidKey = (key: string): key is FileExtensionKey => (key ? key in FILE_EXT_ICONS : false);
|
|
50
50
|
|
|
51
|
+
export const getFileExtension = (fileName: string) => fileName.split('.').pop()?.toLowerCase() || '';
|
|
52
|
+
|
|
51
53
|
export const getFileExtIcon = (fileName: string) => {
|
|
52
|
-
const ext = fileName
|
|
53
|
-
|
|
54
|
+
const ext = getFileExtension(fileName);
|
|
55
|
+
return isValidKey(ext) ? FILE_EXT_ICONS[ext] : FileIcon;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const dedupeErrors = (errors: FileError[]) => {
|
|
59
|
+
const dedupedErrors = errors.reduce((acc, error) => {
|
|
60
|
+
if (!acc.find((err) => err.code === error.code)) {
|
|
61
|
+
acc.push(error);
|
|
62
|
+
}
|
|
63
|
+
return acc;
|
|
64
|
+
}, [] as FileError[]);
|
|
54
65
|
|
|
55
|
-
return
|
|
66
|
+
return dedupedErrors;
|
|
56
67
|
};
|