@availity/mui-file-selector 1.7.0 → 1.8.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 +21 -0
- package/dist/index.d.mts +32 -7
- package/dist/index.d.ts +32 -7
- package/dist/index.js +168 -1163
- package/dist/index.mjs +180 -1201
- package/package.json +2 -1
- package/src/lib/Dropzone.tsx +63 -1
- package/src/lib/Dropzone2.tsx +60 -2
- package/src/lib/ErrorAlert.tsx +1 -0
- package/src/lib/FileSelector.tsx +19 -1
- package/src/lib/FileSelector2.tsx +8 -1
- package/src/lib/FileTypesMessage.tsx +14 -0
- package/src/lib/HeaderMessage.tsx +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@availity/mui-file-selector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Availity MUI file-selector Component - part of the @availity/element design system",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@availity/mui-progress": "^1.0.3",
|
|
52
52
|
"@availity/mui-typography": "^1.0.2",
|
|
53
53
|
"@availity/upload-core": "^8.0.0",
|
|
54
|
+
"@tanstack/react-query": "^4.36.1",
|
|
54
55
|
"react-dropzone": "^11.7.1",
|
|
55
56
|
"react-hook-form": "^7.55.0",
|
|
56
57
|
"tus-js-client": "4.3.1",
|
package/src/lib/Dropzone.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { Box, Stack } from '@availity/mui-layout';
|
|
|
11
11
|
import { Typography } from '@availity/mui-typography';
|
|
12
12
|
|
|
13
13
|
import { FilePickerBtn } from './FilePickerBtn';
|
|
14
|
+
import { formatBytes } from './util';
|
|
14
15
|
|
|
15
16
|
export const outerBoxStyles = {
|
|
16
17
|
backgroundColor: 'background.secondary',
|
|
@@ -66,6 +67,10 @@ export type DropzoneProps = {
|
|
|
66
67
|
* Maximum size of each file in bytes
|
|
67
68
|
*/
|
|
68
69
|
maxSize?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Maximum size of total upload in bytes
|
|
72
|
+
*/
|
|
73
|
+
maxTotalSize?: number;
|
|
69
74
|
/**
|
|
70
75
|
* Whether multiple file selection is allowed
|
|
71
76
|
*/
|
|
@@ -110,6 +115,7 @@ export const Dropzone = ({
|
|
|
110
115
|
enableDropArea = true,
|
|
111
116
|
maxFiles,
|
|
112
117
|
maxSize,
|
|
118
|
+
maxTotalSize,
|
|
113
119
|
multiple,
|
|
114
120
|
name,
|
|
115
121
|
onChange,
|
|
@@ -169,8 +175,64 @@ export const Dropzone = ({
|
|
|
169
175
|
|
|
170
176
|
const previous = watch(name) ?? [];
|
|
171
177
|
|
|
178
|
+
if (maxTotalSize) {
|
|
179
|
+
// Calculate current total size
|
|
180
|
+
const currentTotalSize = previous.reduce((sum: number, file: File) => sum + file.size, 0);
|
|
181
|
+
let newSize = 0;
|
|
182
|
+
|
|
183
|
+
const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
|
|
184
|
+
let sizeCounter = 0;
|
|
185
|
+
|
|
186
|
+
// Find the index where we exceed the total size limit
|
|
187
|
+
const cutoffIndex = acceptedFiles.findIndex((file) => {
|
|
188
|
+
sizeCounter += file.size;
|
|
189
|
+
return sizeCounter > availableSize;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// If we found files that exceed the limit
|
|
193
|
+
if (cutoffIndex !== -1) {
|
|
194
|
+
// Files that fit within the size limit
|
|
195
|
+
const filesToAdd = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
|
|
196
|
+
|
|
197
|
+
// Create rejection for excess files
|
|
198
|
+
fileRejections.push({
|
|
199
|
+
file: acceptedFiles[cutoffIndex],
|
|
200
|
+
errors: [
|
|
201
|
+
{
|
|
202
|
+
code: 'upload-too-large',
|
|
203
|
+
message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
id: counter.increment(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Update acceptedFiles to only include files that fit
|
|
210
|
+
acceptedFiles = filesToAdd;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Calculate size of accepted files for the state update
|
|
214
|
+
newSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
215
|
+
setTotalSize((prev) => prev + newSize);
|
|
216
|
+
}
|
|
217
|
+
|
|
172
218
|
// Set accepted files to form context
|
|
173
|
-
|
|
219
|
+
const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
|
|
220
|
+
const filesToAdd = acceptedFiles.slice(0, remainingSlots);
|
|
221
|
+
setValue(name, previous.concat(filesToAdd));
|
|
222
|
+
|
|
223
|
+
// Add rejections for excess files if needed
|
|
224
|
+
if (maxFiles && acceptedFiles.length > remainingSlots) {
|
|
225
|
+
fileRejections.push({
|
|
226
|
+
file: acceptedFiles[remainingSlots], // Use the first excess file
|
|
227
|
+
errors: [
|
|
228
|
+
{
|
|
229
|
+
code: 'too-many-files',
|
|
230
|
+
message: `Too many files. You may only upload ${maxFiles} file(s).`,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
id: counter.increment(),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
174
236
|
|
|
175
237
|
if (fileRejections.length > 0) {
|
|
176
238
|
const TOO_MANY_FILES_CODE = 'too-many-files';
|
package/src/lib/Dropzone2.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import type { UploadOptions } from '@availity/upload-core';
|
|
|
12
12
|
import type { OnSuccessPayload } from 'tus-js-client';
|
|
13
13
|
|
|
14
14
|
import { FilePickerBtn } from './FilePickerBtn';
|
|
15
|
-
import { dedupeErrors } from './util';
|
|
15
|
+
import { dedupeErrors, formatBytes } from './util';
|
|
16
16
|
import { createCounter, DropzoneContainer, innerBoxStyles, outerBoxStyles } from './Dropzone';
|
|
17
17
|
import type { DropzoneProps } from './Dropzone';
|
|
18
18
|
|
|
@@ -59,6 +59,7 @@ export const Dropzone2 = ({
|
|
|
59
59
|
enableDropArea = true,
|
|
60
60
|
maxFiles,
|
|
61
61
|
maxSize,
|
|
62
|
+
maxTotalSize,
|
|
62
63
|
multiple,
|
|
63
64
|
name,
|
|
64
65
|
onChange,
|
|
@@ -121,10 +122,67 @@ export const Dropzone2 = ({
|
|
|
121
122
|
|
|
122
123
|
const previous = watch(name) ?? [];
|
|
123
124
|
|
|
125
|
+
if (maxTotalSize) {
|
|
126
|
+
// Calculate current total size
|
|
127
|
+
const currentTotalSize = previous.reduce((sum: number, upload: Upload) => sum + upload.file.size, 0);
|
|
128
|
+
console.log({ previous });
|
|
129
|
+
let newSize = 0;
|
|
130
|
+
|
|
131
|
+
const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
|
|
132
|
+
let sizeCounter = 0;
|
|
133
|
+
|
|
134
|
+
// Find the index where we exceed the total size limit
|
|
135
|
+
const cutoffIndex = acceptedFiles.findIndex((file) => {
|
|
136
|
+
sizeCounter += file.size;
|
|
137
|
+
return sizeCounter > availableSize;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// If we found files that exceed the limit
|
|
141
|
+
if (cutoffIndex !== -1) {
|
|
142
|
+
// Files that fit within the size limit
|
|
143
|
+
const filesToAdd = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
|
|
144
|
+
|
|
145
|
+
// Create rejection for excess files
|
|
146
|
+
fileRejections.push({
|
|
147
|
+
file: acceptedFiles[cutoffIndex],
|
|
148
|
+
errors: [
|
|
149
|
+
{
|
|
150
|
+
code: 'upload-too-large',
|
|
151
|
+
message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
id: counter.increment(),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Update acceptedFiles to only include files that fit
|
|
158
|
+
acceptedFiles = filesToAdd;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Calculate size of accepted files for the state update
|
|
162
|
+
newSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
163
|
+
setTotalSize((prev) => prev + newSize);
|
|
164
|
+
}
|
|
165
|
+
|
|
124
166
|
// Set accepted files to form context
|
|
125
|
-
const
|
|
167
|
+
const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
|
|
168
|
+
const filesToAdd = acceptedFiles.slice(0, remainingSlots);
|
|
169
|
+
const uploads = filesToAdd.map((file) => startUpload(file, uploadOptions));
|
|
126
170
|
setValue(name, previous.concat(await Promise.all(uploads)));
|
|
127
171
|
|
|
172
|
+
// Add rejections for excess files if needed
|
|
173
|
+
if (maxFiles && acceptedFiles.length > remainingSlots) {
|
|
174
|
+
fileRejections.push({
|
|
175
|
+
file: acceptedFiles[remainingSlots], // Use the first excess file
|
|
176
|
+
errors: [
|
|
177
|
+
{
|
|
178
|
+
code: 'too-many-files',
|
|
179
|
+
message: `Too many files. You may only upload ${maxFiles} file(s).`,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
id: counter.increment(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
128
186
|
if (fileRejections.length > 0) {
|
|
129
187
|
const TOO_MANY_FILES_CODE = 'too-many-files';
|
|
130
188
|
let hasTooManyFiles = false;
|
package/src/lib/ErrorAlert.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { formatBytes } from './util';
|
|
|
5
5
|
|
|
6
6
|
const codes: Record<string, string> = {
|
|
7
7
|
'file-too-large': 'File exceeds maximum size',
|
|
8
|
+
'upload-too-large': 'File causes maximum total upload size to be exceeded',
|
|
8
9
|
'file-invalid-type': 'File has an invalid type',
|
|
9
10
|
'file-too-small': 'File is smaller than minimum size',
|
|
10
11
|
'too-many-file': 'Too many files',
|
package/src/lib/FileSelector.tsx
CHANGED
|
@@ -59,6 +59,11 @@ export type FileSelectorProps = {
|
|
|
59
59
|
* Overrides the standard file size message
|
|
60
60
|
*/
|
|
61
61
|
customSizeMessage?: React.ReactNode;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Overrides the standard total upload size message
|
|
65
|
+
*/
|
|
66
|
+
customTotalSizeMessage?: React.ReactNode;
|
|
62
67
|
/**
|
|
63
68
|
* Overrides the standard file types message
|
|
64
69
|
*/
|
|
@@ -98,10 +103,16 @@ export type FileSelectorProps = {
|
|
|
98
103
|
* Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
|
|
99
104
|
*/
|
|
100
105
|
maxSize: number;
|
|
106
|
+
/**
|
|
107
|
+
* Maximum size allowed for total upload in bytes
|
|
108
|
+
* Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
|
|
109
|
+
*/
|
|
110
|
+
maxTotalSize?: number;
|
|
101
111
|
/**
|
|
102
112
|
* Whether multiple file selection is allowed
|
|
103
113
|
* @default true
|
|
104
114
|
*/
|
|
115
|
+
|
|
105
116
|
multiple?: boolean;
|
|
106
117
|
/**
|
|
107
118
|
* Callback fired when files are selected
|
|
@@ -145,6 +156,7 @@ export const FileSelector = ({
|
|
|
145
156
|
clientId,
|
|
146
157
|
children,
|
|
147
158
|
customSizeMessage,
|
|
159
|
+
customTotalSizeMessage,
|
|
148
160
|
customTypesMessage,
|
|
149
161
|
customerId,
|
|
150
162
|
customFileRow,
|
|
@@ -155,6 +167,7 @@ export const FileSelector = ({
|
|
|
155
167
|
label = 'Upload file',
|
|
156
168
|
maxFiles,
|
|
157
169
|
maxSize,
|
|
170
|
+
maxTotalSize,
|
|
158
171
|
multiple = true,
|
|
159
172
|
onChange,
|
|
160
173
|
onDrop,
|
|
@@ -240,6 +253,7 @@ export const FileSelector = ({
|
|
|
240
253
|
enableDropArea={enableDropArea}
|
|
241
254
|
maxFiles={maxFiles}
|
|
242
255
|
maxSize={maxSize}
|
|
256
|
+
maxTotalSize={maxTotalSize}
|
|
243
257
|
multiple={multiple}
|
|
244
258
|
onChange={onChange}
|
|
245
259
|
onDrop={onDrop}
|
|
@@ -250,7 +264,9 @@ export const FileSelector = ({
|
|
|
250
264
|
<FileTypesMessage
|
|
251
265
|
allowedFileTypes={allowedFileTypes}
|
|
252
266
|
maxFileSize={maxSize}
|
|
267
|
+
maxTotalSize={maxTotalSize}
|
|
253
268
|
customSizeMessage={customSizeMessage}
|
|
269
|
+
customTotalSizeMessage={customTotalSizeMessage}
|
|
254
270
|
customTypesMessage={customTypesMessage}
|
|
255
271
|
variant="caption"
|
|
256
272
|
/>
|
|
@@ -259,9 +275,10 @@ export const FileSelector = ({
|
|
|
259
275
|
) : (
|
|
260
276
|
<Grid container rowSpacing={3} flexDirection="column">
|
|
261
277
|
<Grid>
|
|
262
|
-
<HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
|
|
278
|
+
<HeaderMessage maxFiles={maxFiles} maxSize={maxSize} maxTotalSize={maxTotalSize} />
|
|
263
279
|
<FileTypesMessage
|
|
264
280
|
allowedFileTypes={allowedFileTypes}
|
|
281
|
+
customTotalSizeMessage={customTotalSizeMessage}
|
|
265
282
|
customSizeMessage={customSizeMessage}
|
|
266
283
|
customTypesMessage={customTypesMessage}
|
|
267
284
|
variant="body2"
|
|
@@ -276,6 +293,7 @@ export const FileSelector = ({
|
|
|
276
293
|
enableDropArea={enableDropArea}
|
|
277
294
|
maxFiles={maxFiles}
|
|
278
295
|
maxSize={maxSize}
|
|
296
|
+
maxTotalSize={maxTotalSize}
|
|
279
297
|
multiple={multiple}
|
|
280
298
|
onChange={onChange}
|
|
281
299
|
onDrop={onDrop}
|
|
@@ -37,6 +37,7 @@ export const FileSelector2 = ({
|
|
|
37
37
|
clientId,
|
|
38
38
|
children,
|
|
39
39
|
customSizeMessage,
|
|
40
|
+
customTotalSizeMessage,
|
|
40
41
|
customTypesMessage,
|
|
41
42
|
customerId,
|
|
42
43
|
customFileRow,
|
|
@@ -47,6 +48,7 @@ export const FileSelector2 = ({
|
|
|
47
48
|
label = 'Upload file',
|
|
48
49
|
maxFiles,
|
|
49
50
|
maxSize,
|
|
51
|
+
maxTotalSize,
|
|
50
52
|
multiple = true,
|
|
51
53
|
onChange,
|
|
52
54
|
onDrop,
|
|
@@ -129,6 +131,7 @@ export const FileSelector2 = ({
|
|
|
129
131
|
enableDropArea={enableDropArea}
|
|
130
132
|
maxFiles={maxFiles}
|
|
131
133
|
maxSize={maxSize}
|
|
134
|
+
maxTotalSize={maxTotalSize}
|
|
132
135
|
multiple={multiple}
|
|
133
136
|
onChange={onChange}
|
|
134
137
|
onDrop={onDrop}
|
|
@@ -140,7 +143,9 @@ export const FileSelector2 = ({
|
|
|
140
143
|
<FileTypesMessage
|
|
141
144
|
allowedFileTypes={allowedFileTypes}
|
|
142
145
|
maxFileSize={maxSize}
|
|
146
|
+
maxTotalSize={maxTotalSize}
|
|
143
147
|
customSizeMessage={customSizeMessage}
|
|
148
|
+
customTotalSizeMessage={customTotalSizeMessage}
|
|
144
149
|
customTypesMessage={customTypesMessage}
|
|
145
150
|
variant="caption"
|
|
146
151
|
/>
|
|
@@ -149,10 +154,11 @@ export const FileSelector2 = ({
|
|
|
149
154
|
) : (
|
|
150
155
|
<Grid container rowSpacing={3} flexDirection="column">
|
|
151
156
|
<Grid>
|
|
152
|
-
<HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
|
|
157
|
+
<HeaderMessage maxFiles={maxFiles} maxSize={maxSize} maxTotalSize={maxTotalSize} />
|
|
153
158
|
<FileTypesMessage
|
|
154
159
|
allowedFileTypes={allowedFileTypes}
|
|
155
160
|
customSizeMessage={customSizeMessage}
|
|
161
|
+
customTotalSizeMessage={customTotalSizeMessage}
|
|
156
162
|
customTypesMessage={customTypesMessage}
|
|
157
163
|
variant="body2"
|
|
158
164
|
/>
|
|
@@ -166,6 +172,7 @@ export const FileSelector2 = ({
|
|
|
166
172
|
enableDropArea={enableDropArea}
|
|
167
173
|
maxFiles={maxFiles}
|
|
168
174
|
maxSize={maxSize}
|
|
175
|
+
maxTotalSize={maxTotalSize}
|
|
169
176
|
multiple={multiple}
|
|
170
177
|
onChange={onChange}
|
|
171
178
|
onDrop={onDrop}
|
|
@@ -11,6 +11,10 @@ export type FileTypesMessageProps = {
|
|
|
11
11
|
* Overrides the standard file size message
|
|
12
12
|
*/
|
|
13
13
|
customSizeMessage?: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Overrides the standard total upload size message
|
|
16
|
+
*/
|
|
17
|
+
customTotalSizeMessage?: React.ReactNode;
|
|
14
18
|
/**
|
|
15
19
|
* Overrides the standard file types message
|
|
16
20
|
*/
|
|
@@ -19,19 +23,28 @@ export type FileTypesMessageProps = {
|
|
|
19
23
|
* Maximum size per file in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
|
|
20
24
|
*/
|
|
21
25
|
maxFileSize?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum size of the total upload in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
|
|
28
|
+
*/
|
|
29
|
+
maxTotalSize?: number;
|
|
22
30
|
variant?: 'caption' | 'body2';
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
export const FileTypesMessage = ({
|
|
26
34
|
allowedFileTypes = [],
|
|
27
35
|
customSizeMessage,
|
|
36
|
+
customTotalSizeMessage,
|
|
28
37
|
customTypesMessage,
|
|
29
38
|
maxFileSize,
|
|
39
|
+
maxTotalSize,
|
|
30
40
|
variant = 'caption',
|
|
31
41
|
}: FileTypesMessageProps) => {
|
|
32
42
|
const fileSizeMsg =
|
|
33
43
|
customSizeMessage ||
|
|
34
44
|
(typeof maxFileSize === 'number' ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null);
|
|
45
|
+
const totalFileSizeMsg =
|
|
46
|
+
customTotalSizeMessage ||
|
|
47
|
+
(typeof maxTotalSize === 'number' ? `Maximum total upload size is ${formatBytes(maxTotalSize)}. ` : null);
|
|
35
48
|
const fileTypesMsg =
|
|
36
49
|
customTypesMessage ||
|
|
37
50
|
(allowedFileTypes.length > 0
|
|
@@ -40,6 +53,7 @@ export const FileTypesMessage = ({
|
|
|
40
53
|
return (
|
|
41
54
|
<Typography variant={variant}>
|
|
42
55
|
{fileSizeMsg}
|
|
56
|
+
{totalFileSizeMsg}
|
|
43
57
|
{fileTypesMsg}
|
|
44
58
|
</Typography>
|
|
45
59
|
);
|
|
@@ -8,15 +8,20 @@ export type HeaderMessageProps = {
|
|
|
8
8
|
*/
|
|
9
9
|
maxFiles: number;
|
|
10
10
|
/**
|
|
11
|
-
* Maximum
|
|
11
|
+
* Maximum size of each file
|
|
12
12
|
*/
|
|
13
13
|
maxSize: number;
|
|
14
|
+
/**
|
|
15
|
+
* Maximum combined total size of all files
|
|
16
|
+
*/
|
|
17
|
+
maxTotalSize?: number;
|
|
14
18
|
};
|
|
15
19
|
|
|
16
|
-
export const HeaderMessage = ({ maxFiles, maxSize }: HeaderMessageProps) => {
|
|
20
|
+
export const HeaderMessage = ({ maxFiles, maxSize, maxTotalSize }: HeaderMessageProps) => {
|
|
17
21
|
return (
|
|
18
22
|
<Typography variant="h6">
|
|
19
|
-
Attach up to {maxFiles} file(s), with a maximum individual size of {formatBytes(maxSize)}
|
|
23
|
+
Attach up to {maxFiles} file(s), with a maximum individual size of {formatBytes(maxSize)}{' '}
|
|
24
|
+
{maxTotalSize && `and a maximum total size of ${formatBytes(maxTotalSize)}`}
|
|
20
25
|
</Typography>
|
|
21
26
|
);
|
|
22
27
|
};
|