@availity/mui-file-selector 1.0.0 → 1.1.0
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 +25 -0
- package/README.md +107 -15
- package/dist/index.d.mts +208 -22
- package/dist/index.d.ts +208 -22
- package/dist/index.js +234 -97
- package/dist/index.mjs +226 -97
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/lib/Dropzone.test.tsx +2 -2
- package/src/lib/Dropzone.tsx +92 -32
- package/src/lib/FileList.test.tsx +25 -1
- package/src/lib/FileList.tsx +66 -28
- package/src/lib/FilePickerBtn.test.tsx +3 -1
- package/src/lib/FilePickerBtn.tsx +23 -21
- package/src/lib/FileSelector.stories.tsx +66 -4
- package/src/lib/FileSelector.test.tsx +11 -1
- package/src/lib/FileSelector.tsx +109 -90
- package/src/lib/FileTypesMessage.test.tsx +2 -2
- package/src/lib/FileTypesMessage.tsx +11 -6
- package/src/lib/HeaderMessage.tsx +1 -1
- package/src/lib/useFileDelivery.tsx +3 -1
- package/src/lib/useUploadCore.tsx +10 -3
package/src/lib/FileSelector.tsx
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { ChangeEvent, ElementType, ReactNode } from 'react';
|
|
3
|
+
import { useFormContext } from 'react-hook-form';
|
|
4
|
+
import type { DropEvent, FileError, FileRejection } from 'react-dropzone/typings/react-dropzone';
|
|
4
5
|
import { useQueryClient } from '@tanstack/react-query';
|
|
5
|
-
import
|
|
6
|
-
import { Button } from '@availity/mui-button';
|
|
6
|
+
import type { default as Upload } from '@availity/upload-core';
|
|
7
7
|
import { Grid } from '@availity/mui-layout';
|
|
8
8
|
import { Typography } from '@availity/mui-typography';
|
|
9
9
|
|
|
10
10
|
import { Dropzone } from './Dropzone';
|
|
11
11
|
import { ErrorAlert } from './ErrorAlert';
|
|
12
12
|
import { FileList } from './FileList';
|
|
13
|
+
import type { FileListProps } from './FileList';
|
|
13
14
|
import { FileTypesMessage } from './FileTypesMessage';
|
|
14
|
-
import {
|
|
15
|
+
import { HeaderMessage } from './HeaderMessage';
|
|
16
|
+
import type { Options, UploadQueryOptions } from './useUploadCore';
|
|
15
17
|
|
|
16
18
|
const CLOUD_URL = '/cloud/web/appl/vault/upload/v1/resumable';
|
|
17
19
|
|
|
@@ -53,10 +55,18 @@ export type FileSelectorProps = {
|
|
|
53
55
|
* @default false
|
|
54
56
|
*/
|
|
55
57
|
disabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether to enable the dropzone area
|
|
60
|
+
*/
|
|
61
|
+
enableDropArea?: boolean;
|
|
56
62
|
/**
|
|
57
63
|
* Custom endpoint URL for file uploads. If not provided, default endpoint will be used
|
|
58
64
|
*/
|
|
59
65
|
endpoint?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Componet to render the File information. This should return a `ListItem`
|
|
68
|
+
*/
|
|
69
|
+
customFileRow?: ElementType<FileListProps>;
|
|
60
70
|
/**
|
|
61
71
|
* Whether to use the cloud upload endpoint
|
|
62
72
|
* When true, uses '/cloud/web/appl/vault/upload/v1/resumable'
|
|
@@ -70,7 +80,7 @@ export type FileSelectorProps = {
|
|
|
70
80
|
/**
|
|
71
81
|
* Maximum number of files that can be uploaded simultaneously
|
|
72
82
|
*/
|
|
73
|
-
maxFiles
|
|
83
|
+
maxFiles: number;
|
|
74
84
|
/**
|
|
75
85
|
* Maximum file size allowed per file in bytes
|
|
76
86
|
* Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
|
|
@@ -87,25 +97,9 @@ export type FileSelectorProps = {
|
|
|
87
97
|
*/
|
|
88
98
|
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
89
99
|
/**
|
|
90
|
-
*
|
|
91
|
-
* @param uploads - Array of Upload instances for the submitted files
|
|
92
|
-
* @param values - Object containing the form values, with files indexed by the name prop
|
|
100
|
+
*
|
|
93
101
|
*/
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Callback fired when a file is successfully uploaded
|
|
97
|
-
*/
|
|
98
|
-
onSuccess?: () => void;
|
|
99
|
-
/**
|
|
100
|
-
* Callback fired when an error occurs during upload
|
|
101
|
-
*/
|
|
102
|
-
onError?: (error: Error) => void;
|
|
103
|
-
/**
|
|
104
|
-
* Array of functions to execute before file upload begins.
|
|
105
|
-
* Each function should return a boolean indicating whether to proceed with the upload.
|
|
106
|
-
* @default []
|
|
107
|
-
*/
|
|
108
|
-
onFilePreUpload?: (() => boolean)[];
|
|
102
|
+
onDrop?: (acceptedFiles: File[], fileRejections: (FileRejection & { id: number })[], event: DropEvent) => void;
|
|
109
103
|
/**
|
|
110
104
|
* Callback fired when a file is removed from the upload list
|
|
111
105
|
* @param files - Array of remaining files
|
|
@@ -113,19 +107,24 @@ export type FileSelectorProps = {
|
|
|
113
107
|
*/
|
|
114
108
|
onUploadRemove?: (files: File[], removedUploadId: string) => void;
|
|
115
109
|
/**
|
|
116
|
-
*
|
|
110
|
+
* Query options from `react-query` for the upload call
|
|
111
|
+
* */
|
|
112
|
+
queryOptions?: UploadQueryOptions;
|
|
113
|
+
/**
|
|
114
|
+
* Options that are passed to the Upload class from `@availity/upload-core`
|
|
117
115
|
*/
|
|
118
|
-
|
|
116
|
+
uploadOptions?: Partial<Options>;
|
|
117
|
+
/**
|
|
118
|
+
* Validation function used for custom validation that is not covered with the other props
|
|
119
|
+
* */
|
|
120
|
+
validator?: (file: File) => FileError | FileError[] | null;
|
|
121
|
+
/**
|
|
122
|
+
* Whether the remove button is disabled
|
|
123
|
+
* @default false
|
|
124
|
+
*/
|
|
125
|
+
disableRemove?: boolean;
|
|
119
126
|
};
|
|
120
127
|
|
|
121
|
-
// Below props were removed from availity-react version. Perserving here in case needed later
|
|
122
|
-
// deliverFileOnSubmit?: boolean;
|
|
123
|
-
// deliveryChannel?: string;
|
|
124
|
-
// fileDeliveryMetadata?: Record<string, unknown> | ((file: Upload) => Record<string, unknown>);
|
|
125
|
-
// onDeliveryError?: (error: unknown) => void;
|
|
126
|
-
// onDeliverySuccess?: () => void;
|
|
127
|
-
// onFileDelivery?: (upload: Upload) => void;
|
|
128
|
-
|
|
129
128
|
export const FileSelector = ({
|
|
130
129
|
name,
|
|
131
130
|
allowedFileNameCharacters,
|
|
@@ -134,7 +133,9 @@ export const FileSelector = ({
|
|
|
134
133
|
clientId,
|
|
135
134
|
children,
|
|
136
135
|
customerId,
|
|
136
|
+
customFileRow,
|
|
137
137
|
disabled = false,
|
|
138
|
+
enableDropArea = true,
|
|
138
139
|
endpoint,
|
|
139
140
|
isCloud,
|
|
140
141
|
label = 'Upload file',
|
|
@@ -142,48 +143,51 @@ export const FileSelector = ({
|
|
|
142
143
|
maxSize,
|
|
143
144
|
multiple = true,
|
|
144
145
|
onChange,
|
|
145
|
-
|
|
146
|
-
onSuccess,
|
|
147
|
-
onError,
|
|
148
|
-
onFilePreUpload = [],
|
|
146
|
+
onDrop,
|
|
149
147
|
onUploadRemove,
|
|
150
|
-
|
|
148
|
+
queryOptions,
|
|
149
|
+
uploadOptions,
|
|
150
|
+
validator,
|
|
151
|
+
disableRemove,
|
|
151
152
|
}: FileSelectorProps) => {
|
|
152
153
|
const [totalSize, setTotalSize] = useState(0);
|
|
153
154
|
const [fileRejections, setFileRejections] = useState<(FileRejection & { id: number })[]>([]);
|
|
154
155
|
|
|
155
156
|
const client = useQueryClient();
|
|
156
|
-
|
|
157
|
-
const methods = useForm({
|
|
158
|
-
defaultValues: {
|
|
159
|
-
[name]: [] as File[],
|
|
160
|
-
},
|
|
161
|
-
});
|
|
157
|
+
const formMethods = useFormContext();
|
|
162
158
|
|
|
163
159
|
const options: Options = {
|
|
160
|
+
...uploadOptions,
|
|
164
161
|
bucketId,
|
|
165
162
|
customerId,
|
|
166
163
|
clientId,
|
|
167
164
|
fileTypes: allowedFileTypes,
|
|
168
165
|
maxSize,
|
|
169
166
|
allowedFileNameCharacters,
|
|
170
|
-
onError,
|
|
171
|
-
onSuccess,
|
|
172
|
-
retryDelays,
|
|
173
167
|
};
|
|
174
168
|
|
|
175
|
-
if
|
|
169
|
+
// Endpoint is set by default in upload-core so check if it exists before passing `undefined`
|
|
176
170
|
if (endpoint) options.endpoint = endpoint;
|
|
171
|
+
// Override endpoint if using the cloud
|
|
177
172
|
if (isCloud) options.endpoint = CLOUD_URL;
|
|
178
173
|
|
|
179
174
|
const handleOnRemoveFile = (uploadId: string, upload: Upload) => {
|
|
180
|
-
const prevFiles =
|
|
175
|
+
const prevFiles: File[] = formMethods.getValues(name);
|
|
181
176
|
const newFiles = prevFiles.filter((file) => file.name !== upload.file.name);
|
|
182
177
|
|
|
183
178
|
if (newFiles.length !== prevFiles.length) {
|
|
184
179
|
const removedFile = prevFiles.find((file) => file.name === upload.file.name);
|
|
185
180
|
|
|
186
|
-
|
|
181
|
+
// Stop upload
|
|
182
|
+
try {
|
|
183
|
+
upload.abort();
|
|
184
|
+
} catch {
|
|
185
|
+
console.error('Encountered an issue stopping the file upload');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Remove from context and cache
|
|
189
|
+
formMethods.setValue(name, newFiles);
|
|
190
|
+
client.removeQueries(['upload', upload.file.name]);
|
|
187
191
|
|
|
188
192
|
if (removedFile?.size) setTotalSize(totalSize - removedFile.size);
|
|
189
193
|
|
|
@@ -191,19 +195,7 @@ export const FileSelector = ({
|
|
|
191
195
|
}
|
|
192
196
|
};
|
|
193
197
|
|
|
194
|
-
const files =
|
|
195
|
-
|
|
196
|
-
const handleOnSubmit = (values: Record<string, File[]>) => {
|
|
197
|
-
if (values[name].length === 0) return;
|
|
198
|
-
|
|
199
|
-
const queries = client.getQueriesData<Upload>(['upload']);
|
|
200
|
-
const uploads = [];
|
|
201
|
-
for (const [, data] of queries) {
|
|
202
|
-
if (data) uploads.push(data);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (onSubmit) onSubmit(uploads, values);
|
|
206
|
-
};
|
|
198
|
+
const files = formMethods.watch(name);
|
|
207
199
|
|
|
208
200
|
const handleRemoveRejection = (id: number) => {
|
|
209
201
|
const rejections = fileRejections.filter((value) => value.id !== id);
|
|
@@ -211,44 +203,71 @@ export const FileSelector = ({
|
|
|
211
203
|
};
|
|
212
204
|
|
|
213
205
|
return (
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
<>
|
|
207
|
+
{enableDropArea ? (
|
|
216
208
|
<>
|
|
217
|
-
<Typography marginBottom="4px">{label}</Typography>
|
|
209
|
+
{label ? <Typography marginBottom="4px">{label}</Typography> : null}
|
|
218
210
|
<Dropzone
|
|
219
211
|
name={name}
|
|
220
212
|
allowedFileTypes={allowedFileTypes}
|
|
221
213
|
disabled={disabled}
|
|
214
|
+
enableDropArea={enableDropArea}
|
|
222
215
|
maxFiles={maxFiles}
|
|
223
216
|
maxSize={maxSize}
|
|
224
217
|
multiple={multiple}
|
|
225
218
|
onChange={onChange}
|
|
219
|
+
onDrop={onDrop}
|
|
226
220
|
setFileRejections={setFileRejections}
|
|
227
221
|
setTotalSize={setTotalSize}
|
|
222
|
+
validator={validator}
|
|
228
223
|
/>
|
|
229
|
-
<FileTypesMessage allowedFileTypes={allowedFileTypes} maxFileSize={maxSize} />
|
|
224
|
+
<FileTypesMessage allowedFileTypes={allowedFileTypes} maxFileSize={maxSize} variant="caption" />
|
|
225
|
+
{children}
|
|
230
226
|
</>
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
227
|
+
) : (
|
|
228
|
+
<Grid container rowSpacing={3} flexDirection="column">
|
|
229
|
+
<Grid>
|
|
230
|
+
<HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
|
|
231
|
+
<FileTypesMessage allowedFileTypes={allowedFileTypes} variant="body2" />
|
|
232
|
+
</Grid>
|
|
233
|
+
{children ? <Grid>{children}</Grid> : null}
|
|
234
|
+
<Grid>
|
|
235
|
+
<Dropzone
|
|
236
|
+
name={name}
|
|
237
|
+
allowedFileTypes={allowedFileTypes}
|
|
238
|
+
disabled={disabled}
|
|
239
|
+
enableDropArea={enableDropArea}
|
|
240
|
+
maxFiles={maxFiles}
|
|
241
|
+
maxSize={maxSize}
|
|
242
|
+
multiple={multiple}
|
|
243
|
+
onChange={onChange}
|
|
244
|
+
onDrop={onDrop}
|
|
245
|
+
setFileRejections={setFileRejections}
|
|
246
|
+
setTotalSize={setTotalSize}
|
|
247
|
+
validator={validator}
|
|
248
|
+
/>
|
|
249
249
|
</Grid>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
</Grid>
|
|
251
|
+
)}
|
|
252
|
+
{fileRejections.length > 0
|
|
253
|
+
? fileRejections.map((rejection) => (
|
|
254
|
+
<ErrorAlert
|
|
255
|
+
key={rejection.id}
|
|
256
|
+
errors={rejection.errors}
|
|
257
|
+
fileName={rejection.file.name}
|
|
258
|
+
id={rejection.id}
|
|
259
|
+
onClose={() => handleRemoveRejection(rejection.id)}
|
|
260
|
+
/>
|
|
261
|
+
))
|
|
262
|
+
: null}
|
|
263
|
+
<FileList
|
|
264
|
+
files={files || []}
|
|
265
|
+
options={options}
|
|
266
|
+
onRemoveFile={handleOnRemoveFile}
|
|
267
|
+
queryOptions={queryOptions}
|
|
268
|
+
customFileRow={customFileRow}
|
|
269
|
+
disableRemove={disableRemove}
|
|
270
|
+
/>
|
|
271
|
+
</>
|
|
253
272
|
);
|
|
254
273
|
};
|
|
@@ -4,13 +4,13 @@ import { FileTypesMessage } from './FileTypesMessage';
|
|
|
4
4
|
|
|
5
5
|
describe('FileTypesMessage', () => {
|
|
6
6
|
test('should render successfully', () => {
|
|
7
|
-
render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} />);
|
|
7
|
+
render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} variant="caption" />);
|
|
8
8
|
|
|
9
9
|
expect(screen.getByText(/All file types allowed/)).toBeTruthy();
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
test('should show file size', () => {
|
|
13
|
-
render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} />);
|
|
13
|
+
render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} variant="body2" />);
|
|
14
14
|
|
|
15
15
|
expect(screen.getByText(/Maximum file size is/)).toBeTruthy();
|
|
16
16
|
});
|
|
@@ -2,26 +2,31 @@ import { Typography } from '@availity/mui-typography';
|
|
|
2
2
|
|
|
3
3
|
import { formatBytes } from './util';
|
|
4
4
|
|
|
5
|
-
type FileTypesMessageProps = {
|
|
5
|
+
export type FileTypesMessageProps = {
|
|
6
6
|
/**
|
|
7
7
|
* Allowed file type extensions. Each extension should be prefixed with a ".". eg: .txt, .pdf, .png
|
|
8
8
|
*/
|
|
9
|
-
allowedFileTypes
|
|
9
|
+
allowedFileTypes?: `.${string}`[];
|
|
10
10
|
/**
|
|
11
11
|
* Maximum size per file in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
|
|
12
12
|
*/
|
|
13
|
-
maxFileSize
|
|
13
|
+
maxFileSize?: number;
|
|
14
|
+
variant?: 'caption' | 'body2';
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
export const FileTypesMessage = ({
|
|
17
|
+
export const FileTypesMessage = ({
|
|
18
|
+
allowedFileTypes = [],
|
|
19
|
+
maxFileSize,
|
|
20
|
+
variant = 'caption',
|
|
21
|
+
}: FileTypesMessageProps) => {
|
|
17
22
|
const fileSizeMsg = typeof maxFileSize === 'number' ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null;
|
|
18
23
|
const fileTypesMsg =
|
|
19
24
|
allowedFileTypes.length > 0
|
|
20
|
-
? `Supported file types include: ${allowedFileTypes.join(', ')}
|
|
25
|
+
? `Supported file types include: ${allowedFileTypes.join(', ')}`
|
|
21
26
|
: 'All file types allowed.';
|
|
22
27
|
|
|
23
28
|
return (
|
|
24
|
-
<Typography variant=
|
|
29
|
+
<Typography variant={variant}>
|
|
25
30
|
{fileSizeMsg}
|
|
26
31
|
{fileTypesMsg}
|
|
27
32
|
</Typography>
|
|
@@ -16,7 +16,7 @@ export type HeaderMessageProps = {
|
|
|
16
16
|
export const HeaderMessage = ({ maxFiles, maxSize }: HeaderMessageProps) => {
|
|
17
17
|
return (
|
|
18
18
|
<Typography variant="h6">
|
|
19
|
-
Attach up to {maxFiles} file(s), with a maximum of {formatBytes(maxSize)}
|
|
19
|
+
Attach up to {maxFiles} file(s), with a maximum individual size of {formatBytes(maxSize)}
|
|
20
20
|
</Typography>
|
|
21
21
|
);
|
|
22
22
|
};
|
|
@@ -77,7 +77,9 @@ export function useFileDelivery({
|
|
|
77
77
|
uploads.length > 0 &&
|
|
78
78
|
validate(errors);
|
|
79
79
|
|
|
80
|
-
return useQuery(
|
|
80
|
+
return useQuery({
|
|
81
|
+
queryKey: ['file-delivery', customerId, clientId, bucketId],
|
|
82
|
+
queryFn: () => callFileDelivery(uploads),
|
|
81
83
|
enabled: isQueryEnabled,
|
|
82
84
|
retry: false,
|
|
83
85
|
});
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query';
|
|
2
|
-
import
|
|
2
|
+
import type { UseQueryOptions } from '@tanstack/react-query';
|
|
3
|
+
import Upload from '@availity/upload-core';
|
|
4
|
+
import type { UploadOptions } from '@availity/upload-core';
|
|
3
5
|
|
|
4
6
|
export type Options = {
|
|
5
7
|
onError?: (error: Error) => void;
|
|
6
8
|
onSuccess?: () => void;
|
|
7
9
|
} & UploadOptions;
|
|
8
10
|
|
|
11
|
+
export type UploadQueryOptions = UseQueryOptions<Upload, Error, Upload, [string, string, Options]>;
|
|
12
|
+
|
|
9
13
|
async function startUpload(file: File, options: Options) {
|
|
10
14
|
const { onSuccess, onError, ...uploadOptions } = options;
|
|
11
15
|
const upload = new Upload(file, uploadOptions);
|
|
@@ -20,12 +24,15 @@ async function startUpload(file: File, options: Options) {
|
|
|
20
24
|
return upload;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
export function useUploadCore(file: File, options: Options) {
|
|
27
|
+
export function useUploadCore(file: File, options: Options, queryOptions?: UploadQueryOptions) {
|
|
24
28
|
const isQueryEnabled = !!file;
|
|
25
29
|
|
|
26
|
-
return useQuery(
|
|
30
|
+
return useQuery({
|
|
31
|
+
queryKey: ['upload', file.name, options],
|
|
32
|
+
queryFn: () => startUpload(file, options),
|
|
27
33
|
enabled: isQueryEnabled,
|
|
28
34
|
retry: false,
|
|
29
35
|
refetchOnWindowFocus: false,
|
|
36
|
+
...queryOptions,
|
|
30
37
|
});
|
|
31
38
|
}
|