@availity/mui-file-selector 1.7.1 → 1.8.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/dist/index.d.mts +37 -7
- package/dist/index.d.ts +37 -7
- package/dist/index.js +214 -66
- package/dist/index.mjs +222 -74
- package/package.json +1 -1
- package/src/lib/Dropzone.tsx +101 -4
- package/src/lib/Dropzone2.tsx +91 -5
- package/src/lib/ErrorAlert.tsx +1 -0
- package/src/lib/FileSelector.tsx +21 -1
- package/src/lib/FileSelector2.tsx +8 -1
- package/src/lib/FileTypesMessage.tsx +14 -0
- package/src/lib/HeaderMessage.tsx +8 -3
package/dist/index.mjs
CHANGED
|
@@ -114,6 +114,66 @@ var FilePickerBtn = (_a) => {
|
|
|
114
114
|
] });
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
// src/lib/util.ts
|
|
118
|
+
import {
|
|
119
|
+
FileArchiveIcon,
|
|
120
|
+
FileCodeIcon,
|
|
121
|
+
FileCsvIcon,
|
|
122
|
+
FileExcelIcon,
|
|
123
|
+
FileIcon,
|
|
124
|
+
FileImageIcon,
|
|
125
|
+
FileLinesIcon,
|
|
126
|
+
FilePdfIcon,
|
|
127
|
+
FilePowerpointIcon,
|
|
128
|
+
FileWordIcon
|
|
129
|
+
} from "@availity/mui-icon";
|
|
130
|
+
function formatBytes(bytes, decimals = 2) {
|
|
131
|
+
if (!+bytes) return "0 Bytes";
|
|
132
|
+
const k = 1024;
|
|
133
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
134
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
135
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
136
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
137
|
+
}
|
|
138
|
+
var FILE_EXT_ICONS = {
|
|
139
|
+
png: FileImageIcon,
|
|
140
|
+
jpg: FileImageIcon,
|
|
141
|
+
jpeg: FileImageIcon,
|
|
142
|
+
gif: FileImageIcon,
|
|
143
|
+
csv: FileCsvIcon,
|
|
144
|
+
ppt: FilePowerpointIcon,
|
|
145
|
+
pptx: FilePowerpointIcon,
|
|
146
|
+
xls: FileExcelIcon,
|
|
147
|
+
xlsx: FileExcelIcon,
|
|
148
|
+
doc: FileWordIcon,
|
|
149
|
+
docx: FileWordIcon,
|
|
150
|
+
txt: FileLinesIcon,
|
|
151
|
+
text: FileLinesIcon,
|
|
152
|
+
zip: FileArchiveIcon,
|
|
153
|
+
"7zip": FileArchiveIcon,
|
|
154
|
+
xml: FileCodeIcon,
|
|
155
|
+
html: FileCodeIcon,
|
|
156
|
+
pdf: FilePdfIcon
|
|
157
|
+
};
|
|
158
|
+
var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
|
|
159
|
+
var getFileExtension = (fileName) => {
|
|
160
|
+
var _a;
|
|
161
|
+
return ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
|
|
162
|
+
};
|
|
163
|
+
var getFileExtIcon = (fileName) => {
|
|
164
|
+
const ext = getFileExtension(fileName);
|
|
165
|
+
return isValidKey(ext) ? FILE_EXT_ICONS[ext] : FileIcon;
|
|
166
|
+
};
|
|
167
|
+
var dedupeErrors = (errors) => {
|
|
168
|
+
const dedupedErrors = errors.reduce((acc, error) => {
|
|
169
|
+
if (!acc.find((err) => err.code === error.code)) {
|
|
170
|
+
acc.push(error);
|
|
171
|
+
}
|
|
172
|
+
return acc;
|
|
173
|
+
}, []);
|
|
174
|
+
return dedupedErrors;
|
|
175
|
+
};
|
|
176
|
+
|
|
117
177
|
// src/lib/Dropzone.tsx
|
|
118
178
|
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
119
179
|
var outerBoxStyles = {
|
|
@@ -149,10 +209,12 @@ var DropzoneContainer = styled(Box, { name: "AvDropzoneContainer", slot: "root"
|
|
|
149
209
|
});
|
|
150
210
|
var Dropzone = ({
|
|
151
211
|
allowedFileTypes = [],
|
|
212
|
+
allowedFileNameCharacters,
|
|
152
213
|
disabled,
|
|
153
214
|
enableDropArea = true,
|
|
154
215
|
maxFiles,
|
|
155
216
|
maxSize,
|
|
217
|
+
maxTotalSize,
|
|
156
218
|
multiple,
|
|
157
219
|
name,
|
|
158
220
|
onChange,
|
|
@@ -182,6 +244,27 @@ var Dropzone = ({
|
|
|
182
244
|
message: `Too many files. You may only upload ${maxFiles} file(s).`
|
|
183
245
|
});
|
|
184
246
|
}
|
|
247
|
+
if (allowedFileNameCharacters) {
|
|
248
|
+
const fileName = file.name.substring(0, file.name.lastIndexOf("."));
|
|
249
|
+
const regExp = new RegExp(`([^${allowedFileNameCharacters}])`, "g");
|
|
250
|
+
if (fileName.match(regExp) !== null) {
|
|
251
|
+
errors.push({
|
|
252
|
+
code: "invalid-file-name-characters",
|
|
253
|
+
message: "File name contains characters not allowed"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (allowedFileTypes.length > 0) {
|
|
258
|
+
const fileName = file.name;
|
|
259
|
+
const fileExt = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
|
|
260
|
+
const lowerCaseAllowedTypes = allowedFileTypes.map((type) => type.toLowerCase());
|
|
261
|
+
if (!lowerCaseAllowedTypes.includes(fileExt)) {
|
|
262
|
+
errors.push({
|
|
263
|
+
code: "file-invalid-type",
|
|
264
|
+
message: `File type ${fileExt} is not allowed`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
185
268
|
if (validator) {
|
|
186
269
|
const validatorErrors = validator(file);
|
|
187
270
|
if (validatorErrors) {
|
|
@@ -194,7 +277,7 @@ var Dropzone = ({
|
|
|
194
277
|
}
|
|
195
278
|
return errors.length > 0 ? errors : null;
|
|
196
279
|
},
|
|
197
|
-
[maxFiles, validator]
|
|
280
|
+
[maxFiles, validator, allowedFileNameCharacters, watch, name, allowedFileTypes]
|
|
198
281
|
);
|
|
199
282
|
const handleOnDrop = useCallback(
|
|
200
283
|
(acceptedFiles, fileRejections, event) => {
|
|
@@ -205,7 +288,48 @@ var Dropzone = ({
|
|
|
205
288
|
}
|
|
206
289
|
setTotalSize((prev) => prev + newSize);
|
|
207
290
|
const previous = (_a2 = watch(name)) != null ? _a2 : [];
|
|
208
|
-
|
|
291
|
+
if (maxTotalSize) {
|
|
292
|
+
const currentTotalSize = previous.reduce((sum, file) => sum + file.size, 0);
|
|
293
|
+
let newSize2 = 0;
|
|
294
|
+
const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
|
|
295
|
+
let sizeCounter = 0;
|
|
296
|
+
const cutoffIndex = acceptedFiles.findIndex((file) => {
|
|
297
|
+
sizeCounter += file.size;
|
|
298
|
+
return sizeCounter > availableSize;
|
|
299
|
+
});
|
|
300
|
+
if (cutoffIndex !== -1) {
|
|
301
|
+
const filesToAdd2 = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
|
|
302
|
+
fileRejections.push({
|
|
303
|
+
file: acceptedFiles[cutoffIndex],
|
|
304
|
+
errors: [
|
|
305
|
+
{
|
|
306
|
+
code: "upload-too-large",
|
|
307
|
+
message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`
|
|
308
|
+
}
|
|
309
|
+
],
|
|
310
|
+
id: counter.increment()
|
|
311
|
+
});
|
|
312
|
+
acceptedFiles = filesToAdd2;
|
|
313
|
+
}
|
|
314
|
+
newSize2 = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
315
|
+
setTotalSize((prev) => prev + newSize2);
|
|
316
|
+
}
|
|
317
|
+
const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
|
|
318
|
+
const filesToAdd = acceptedFiles.slice(0, remainingSlots);
|
|
319
|
+
setValue(name, previous.concat(filesToAdd));
|
|
320
|
+
if (maxFiles && acceptedFiles.length > remainingSlots) {
|
|
321
|
+
fileRejections.push({
|
|
322
|
+
file: acceptedFiles[remainingSlots],
|
|
323
|
+
// Use the first excess file
|
|
324
|
+
errors: [
|
|
325
|
+
{
|
|
326
|
+
code: "too-many-files",
|
|
327
|
+
message: `Too many files. You may only upload ${maxFiles} file(s).`
|
|
328
|
+
}
|
|
329
|
+
],
|
|
330
|
+
id: counter.increment()
|
|
331
|
+
});
|
|
332
|
+
}
|
|
209
333
|
if (fileRejections.length > 0) {
|
|
210
334
|
const TOO_MANY_FILES_CODE = "too-many-files";
|
|
211
335
|
let hasTooManyFiles = false;
|
|
@@ -232,7 +356,7 @@ var Dropzone = ({
|
|
|
232
356
|
if (setFileRejections) setFileRejections(fileRejections);
|
|
233
357
|
if (onDrop) onDrop(acceptedFiles, fileRejections, event);
|
|
234
358
|
},
|
|
235
|
-
[setFileRejections]
|
|
359
|
+
[setFileRejections, setTotalSize, watch, name, maxTotalSize, maxFiles, setValue, onDrop]
|
|
236
360
|
);
|
|
237
361
|
const accept = allowedFileTypes.join(",");
|
|
238
362
|
const { getRootProps, getInputProps } = useDropzone({
|
|
@@ -257,7 +381,7 @@ var Dropzone = ({
|
|
|
257
381
|
};
|
|
258
382
|
const handleOnClick = (event) => {
|
|
259
383
|
if (!enableDropArea && rootProps.onClick) rootProps.onClick(event);
|
|
260
|
-
if (onClick) onClick;
|
|
384
|
+
if (onClick) onClick(event);
|
|
261
385
|
};
|
|
262
386
|
const getFieldValue = () => {
|
|
263
387
|
const field = getValues();
|
|
@@ -306,68 +430,6 @@ import { CloudUploadIcon as CloudUploadIcon2, PlusIcon as PlusIcon2 } from "@ava
|
|
|
306
430
|
import { Box as Box2, Stack as Stack2 } from "@availity/mui-layout";
|
|
307
431
|
import { Typography as Typography2 } from "@availity/mui-typography";
|
|
308
432
|
import Upload from "@availity/upload-core";
|
|
309
|
-
|
|
310
|
-
// src/lib/util.ts
|
|
311
|
-
import {
|
|
312
|
-
FileArchiveIcon,
|
|
313
|
-
FileCodeIcon,
|
|
314
|
-
FileCsvIcon,
|
|
315
|
-
FileExcelIcon,
|
|
316
|
-
FileIcon,
|
|
317
|
-
FileImageIcon,
|
|
318
|
-
FileLinesIcon,
|
|
319
|
-
FilePdfIcon,
|
|
320
|
-
FilePowerpointIcon,
|
|
321
|
-
FileWordIcon
|
|
322
|
-
} from "@availity/mui-icon";
|
|
323
|
-
function formatBytes(bytes, decimals = 2) {
|
|
324
|
-
if (!+bytes) return "0 Bytes";
|
|
325
|
-
const k = 1024;
|
|
326
|
-
const dm = decimals < 0 ? 0 : decimals;
|
|
327
|
-
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
328
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
329
|
-
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
330
|
-
}
|
|
331
|
-
var FILE_EXT_ICONS = {
|
|
332
|
-
png: FileImageIcon,
|
|
333
|
-
jpg: FileImageIcon,
|
|
334
|
-
jpeg: FileImageIcon,
|
|
335
|
-
gif: FileImageIcon,
|
|
336
|
-
csv: FileCsvIcon,
|
|
337
|
-
ppt: FilePowerpointIcon,
|
|
338
|
-
pptx: FilePowerpointIcon,
|
|
339
|
-
xls: FileExcelIcon,
|
|
340
|
-
xlsx: FileExcelIcon,
|
|
341
|
-
doc: FileWordIcon,
|
|
342
|
-
docx: FileWordIcon,
|
|
343
|
-
txt: FileLinesIcon,
|
|
344
|
-
text: FileLinesIcon,
|
|
345
|
-
zip: FileArchiveIcon,
|
|
346
|
-
"7zip": FileArchiveIcon,
|
|
347
|
-
xml: FileCodeIcon,
|
|
348
|
-
html: FileCodeIcon,
|
|
349
|
-
pdf: FilePdfIcon
|
|
350
|
-
};
|
|
351
|
-
var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
|
|
352
|
-
var getFileExtension = (fileName) => {
|
|
353
|
-
var _a;
|
|
354
|
-
return ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
|
|
355
|
-
};
|
|
356
|
-
var getFileExtIcon = (fileName) => {
|
|
357
|
-
const ext = getFileExtension(fileName);
|
|
358
|
-
return isValidKey(ext) ? FILE_EXT_ICONS[ext] : FileIcon;
|
|
359
|
-
};
|
|
360
|
-
var dedupeErrors = (errors) => {
|
|
361
|
-
const dedupedErrors = errors.reduce((acc, error) => {
|
|
362
|
-
if (!acc.find((err) => err.code === error.code)) {
|
|
363
|
-
acc.push(error);
|
|
364
|
-
}
|
|
365
|
-
return acc;
|
|
366
|
-
}, []);
|
|
367
|
-
return dedupedErrors;
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
// src/lib/Dropzone2.tsx
|
|
371
433
|
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
372
434
|
var counter2 = createCounter();
|
|
373
435
|
function startUpload(file, options) {
|
|
@@ -389,6 +451,7 @@ var Dropzone2 = ({
|
|
|
389
451
|
enableDropArea = true,
|
|
390
452
|
maxFiles,
|
|
391
453
|
maxSize,
|
|
454
|
+
maxTotalSize,
|
|
392
455
|
multiple,
|
|
393
456
|
name,
|
|
394
457
|
onChange,
|
|
@@ -420,6 +483,27 @@ var Dropzone2 = ({
|
|
|
420
483
|
message: `Too many files. You may only upload ${maxFiles} file(s).`
|
|
421
484
|
});
|
|
422
485
|
}
|
|
486
|
+
if (uploadOptions.allowedFileNameCharacters) {
|
|
487
|
+
const fileName = file.name.substring(0, file.name.lastIndexOf("."));
|
|
488
|
+
const regExp = new RegExp(`([^${uploadOptions.allowedFileNameCharacters}])`, "g");
|
|
489
|
+
if (fileName.match(regExp) !== null) {
|
|
490
|
+
errors.push({
|
|
491
|
+
code: "invalid-file-name-characters",
|
|
492
|
+
message: "File name contains characters not allowed"
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (allowedFileTypes.length > 0) {
|
|
497
|
+
const fileName = file.name;
|
|
498
|
+
const fileExt = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
|
|
499
|
+
const lowerCaseAllowedTypes = allowedFileTypes.map((type) => type.toLowerCase());
|
|
500
|
+
if (!lowerCaseAllowedTypes.includes(fileExt)) {
|
|
501
|
+
errors.push({
|
|
502
|
+
code: "file-invalid-type",
|
|
503
|
+
message: `File type ${fileExt} is not allowed`
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
423
507
|
if (validator) {
|
|
424
508
|
const validatorErrors = validator(file);
|
|
425
509
|
if (validatorErrors) {
|
|
@@ -432,7 +516,7 @@ var Dropzone2 = ({
|
|
|
432
516
|
}
|
|
433
517
|
return errors.length > 0 ? dedupeErrors(errors) : null;
|
|
434
518
|
},
|
|
435
|
-
[maxFiles, validator]
|
|
519
|
+
[maxFiles, validator, uploadOptions.allowedFileNameCharacters, allowedFileTypes, watch, name]
|
|
436
520
|
);
|
|
437
521
|
const handleOnDrop = useCallback2(
|
|
438
522
|
(acceptedFiles, fileRejections, event) => __async(void 0, null, function* () {
|
|
@@ -443,8 +527,49 @@ var Dropzone2 = ({
|
|
|
443
527
|
}
|
|
444
528
|
setTotalSize((prev) => prev + newSize);
|
|
445
529
|
const previous = (_a2 = watch(name)) != null ? _a2 : [];
|
|
446
|
-
|
|
530
|
+
if (maxTotalSize) {
|
|
531
|
+
const currentTotalSize = previous.reduce((sum, upload) => sum + upload.file.size, 0);
|
|
532
|
+
let newSize2 = 0;
|
|
533
|
+
const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
|
|
534
|
+
let sizeCounter = 0;
|
|
535
|
+
const cutoffIndex = acceptedFiles.findIndex((file) => {
|
|
536
|
+
sizeCounter += file.size;
|
|
537
|
+
return sizeCounter > availableSize;
|
|
538
|
+
});
|
|
539
|
+
if (cutoffIndex !== -1) {
|
|
540
|
+
const filesToAdd2 = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
|
|
541
|
+
fileRejections.push({
|
|
542
|
+
file: acceptedFiles[cutoffIndex],
|
|
543
|
+
errors: [
|
|
544
|
+
{
|
|
545
|
+
code: "upload-too-large",
|
|
546
|
+
message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`
|
|
547
|
+
}
|
|
548
|
+
],
|
|
549
|
+
id: counter2.increment()
|
|
550
|
+
});
|
|
551
|
+
acceptedFiles = filesToAdd2;
|
|
552
|
+
}
|
|
553
|
+
newSize2 = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
554
|
+
setTotalSize((prev) => prev + newSize2);
|
|
555
|
+
}
|
|
556
|
+
const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
|
|
557
|
+
const filesToAdd = acceptedFiles.slice(0, remainingSlots);
|
|
558
|
+
const uploads = filesToAdd.map((file) => startUpload(file, uploadOptions));
|
|
447
559
|
setValue(name, previous.concat(yield Promise.all(uploads)));
|
|
560
|
+
if (maxFiles && acceptedFiles.length > remainingSlots) {
|
|
561
|
+
fileRejections.push({
|
|
562
|
+
file: acceptedFiles[remainingSlots],
|
|
563
|
+
// Use the first excess file
|
|
564
|
+
errors: [
|
|
565
|
+
{
|
|
566
|
+
code: "too-many-files",
|
|
567
|
+
message: `Too many files. You may only upload ${maxFiles} file(s).`
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
id: counter2.increment()
|
|
571
|
+
});
|
|
572
|
+
}
|
|
448
573
|
if (fileRejections.length > 0) {
|
|
449
574
|
const TOO_MANY_FILES_CODE = "too-many-files";
|
|
450
575
|
let hasTooManyFiles = false;
|
|
@@ -471,7 +596,7 @@ var Dropzone2 = ({
|
|
|
471
596
|
if (setFileRejections) setFileRejections(fileRejections);
|
|
472
597
|
if (onDrop) onDrop(acceptedFiles, fileRejections, event);
|
|
473
598
|
}),
|
|
474
|
-
[setFileRejections]
|
|
599
|
+
[setFileRejections, setTotalSize, watch, name, maxTotalSize, maxFiles, uploadOptions, setValue, onDrop]
|
|
475
600
|
);
|
|
476
601
|
const { getRootProps, getInputProps } = useDropzone2({
|
|
477
602
|
onDrop: handleOnDrop,
|
|
@@ -495,7 +620,7 @@ var Dropzone2 = ({
|
|
|
495
620
|
};
|
|
496
621
|
const handleOnClick = (event) => {
|
|
497
622
|
if (!enableDropArea && rootProps.onClick) rootProps.onClick(event);
|
|
498
|
-
if (onClick) onClick;
|
|
623
|
+
if (onClick) onClick(event);
|
|
499
624
|
};
|
|
500
625
|
const getFieldValue = () => {
|
|
501
626
|
const field = getValues();
|
|
@@ -541,6 +666,7 @@ import { List, ListItem } from "@availity/mui-list";
|
|
|
541
666
|
import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
542
667
|
var codes = {
|
|
543
668
|
"file-too-large": "File exceeds maximum size",
|
|
669
|
+
"upload-too-large": "File causes maximum total upload size to be exceeded",
|
|
544
670
|
"file-invalid-type": "File has an invalid type",
|
|
545
671
|
"file-too-small": "File is smaller than minimum size",
|
|
546
672
|
"too-many-file": "Too many files",
|
|
@@ -1052,14 +1178,18 @@ import { jsxs as jsxs11 } from "react/jsx-runtime";
|
|
|
1052
1178
|
var FileTypesMessage = ({
|
|
1053
1179
|
allowedFileTypes = [],
|
|
1054
1180
|
customSizeMessage,
|
|
1181
|
+
customTotalSizeMessage,
|
|
1055
1182
|
customTypesMessage,
|
|
1056
1183
|
maxFileSize,
|
|
1184
|
+
maxTotalSize,
|
|
1057
1185
|
variant = "caption"
|
|
1058
1186
|
}) => {
|
|
1059
1187
|
const fileSizeMsg = customSizeMessage || (typeof maxFileSize === "number" ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null);
|
|
1188
|
+
const totalFileSizeMsg = customTotalSizeMessage || (typeof maxTotalSize === "number" ? `Maximum total upload size is ${formatBytes(maxTotalSize)}. ` : null);
|
|
1060
1189
|
const fileTypesMsg = customTypesMessage || (allowedFileTypes.length > 0 ? `Supported file types include: ${allowedFileTypes.join(", ")}` : "All file types allowed.");
|
|
1061
1190
|
return /* @__PURE__ */ jsxs11(Typography4, { variant, children: [
|
|
1062
1191
|
fileSizeMsg,
|
|
1192
|
+
totalFileSizeMsg,
|
|
1063
1193
|
fileTypesMsg
|
|
1064
1194
|
] });
|
|
1065
1195
|
};
|
|
@@ -1067,12 +1197,14 @@ var FileTypesMessage = ({
|
|
|
1067
1197
|
// src/lib/HeaderMessage.tsx
|
|
1068
1198
|
import { Typography as Typography5 } from "@availity/mui-typography";
|
|
1069
1199
|
import { jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1070
|
-
var HeaderMessage = ({ maxFiles, maxSize }) => {
|
|
1200
|
+
var HeaderMessage = ({ maxFiles, maxSize, maxTotalSize }) => {
|
|
1071
1201
|
return /* @__PURE__ */ jsxs12(Typography5, { variant: "h6", children: [
|
|
1072
1202
|
"Attach up to ",
|
|
1073
1203
|
maxFiles,
|
|
1074
1204
|
" file(s), with a maximum individual size of ",
|
|
1075
|
-
formatBytes(maxSize)
|
|
1205
|
+
formatBytes(maxSize),
|
|
1206
|
+
" ",
|
|
1207
|
+
maxTotalSize && `and a maximum total size of ${formatBytes(maxTotalSize)}`
|
|
1076
1208
|
] });
|
|
1077
1209
|
};
|
|
1078
1210
|
|
|
@@ -1087,6 +1219,7 @@ var FileSelector = ({
|
|
|
1087
1219
|
clientId,
|
|
1088
1220
|
children,
|
|
1089
1221
|
customSizeMessage,
|
|
1222
|
+
customTotalSizeMessage,
|
|
1090
1223
|
customTypesMessage,
|
|
1091
1224
|
customerId,
|
|
1092
1225
|
customFileRow,
|
|
@@ -1097,6 +1230,7 @@ var FileSelector = ({
|
|
|
1097
1230
|
label = "Upload file",
|
|
1098
1231
|
maxFiles,
|
|
1099
1232
|
maxSize,
|
|
1233
|
+
maxTotalSize,
|
|
1100
1234
|
multiple = true,
|
|
1101
1235
|
onChange,
|
|
1102
1236
|
onDrop,
|
|
@@ -1156,10 +1290,12 @@ var FileSelector = ({
|
|
|
1156
1290
|
{
|
|
1157
1291
|
name,
|
|
1158
1292
|
allowedFileTypes,
|
|
1293
|
+
allowedFileNameCharacters,
|
|
1159
1294
|
disabled,
|
|
1160
1295
|
enableDropArea,
|
|
1161
1296
|
maxFiles,
|
|
1162
1297
|
maxSize,
|
|
1298
|
+
maxTotalSize,
|
|
1163
1299
|
multiple,
|
|
1164
1300
|
onChange,
|
|
1165
1301
|
onDrop,
|
|
@@ -1173,7 +1309,9 @@ var FileSelector = ({
|
|
|
1173
1309
|
{
|
|
1174
1310
|
allowedFileTypes,
|
|
1175
1311
|
maxFileSize: maxSize,
|
|
1312
|
+
maxTotalSize,
|
|
1176
1313
|
customSizeMessage,
|
|
1314
|
+
customTotalSizeMessage,
|
|
1177
1315
|
customTypesMessage,
|
|
1178
1316
|
variant: "caption"
|
|
1179
1317
|
}
|
|
@@ -1181,11 +1319,12 @@ var FileSelector = ({
|
|
|
1181
1319
|
children
|
|
1182
1320
|
] }) : /* @__PURE__ */ jsxs13(Grid4, { container: true, rowSpacing: 3, flexDirection: "column", children: [
|
|
1183
1321
|
/* @__PURE__ */ jsxs13(Grid4, { children: [
|
|
1184
|
-
/* @__PURE__ */ jsx14(HeaderMessage, { maxFiles, maxSize }),
|
|
1322
|
+
/* @__PURE__ */ jsx14(HeaderMessage, { maxFiles, maxSize, maxTotalSize }),
|
|
1185
1323
|
/* @__PURE__ */ jsx14(
|
|
1186
1324
|
FileTypesMessage,
|
|
1187
1325
|
{
|
|
1188
1326
|
allowedFileTypes,
|
|
1327
|
+
customTotalSizeMessage,
|
|
1189
1328
|
customSizeMessage,
|
|
1190
1329
|
customTypesMessage,
|
|
1191
1330
|
variant: "body2"
|
|
@@ -1198,10 +1337,12 @@ var FileSelector = ({
|
|
|
1198
1337
|
{
|
|
1199
1338
|
name,
|
|
1200
1339
|
allowedFileTypes,
|
|
1340
|
+
allowedFileNameCharacters,
|
|
1201
1341
|
disabled,
|
|
1202
1342
|
enableDropArea,
|
|
1203
1343
|
maxFiles,
|
|
1204
1344
|
maxSize,
|
|
1345
|
+
maxTotalSize,
|
|
1205
1346
|
multiple,
|
|
1206
1347
|
onChange,
|
|
1207
1348
|
onDrop,
|
|
@@ -1263,6 +1404,7 @@ var FileSelector2 = ({
|
|
|
1263
1404
|
clientId,
|
|
1264
1405
|
children,
|
|
1265
1406
|
customSizeMessage,
|
|
1407
|
+
customTotalSizeMessage,
|
|
1266
1408
|
customTypesMessage,
|
|
1267
1409
|
customerId,
|
|
1268
1410
|
customFileRow,
|
|
@@ -1273,6 +1415,7 @@ var FileSelector2 = ({
|
|
|
1273
1415
|
label = "Upload file",
|
|
1274
1416
|
maxFiles,
|
|
1275
1417
|
maxSize,
|
|
1418
|
+
maxTotalSize,
|
|
1276
1419
|
multiple = true,
|
|
1277
1420
|
onChange,
|
|
1278
1421
|
onDrop,
|
|
@@ -1333,6 +1476,7 @@ var FileSelector2 = ({
|
|
|
1333
1476
|
enableDropArea,
|
|
1334
1477
|
maxFiles,
|
|
1335
1478
|
maxSize,
|
|
1479
|
+
maxTotalSize,
|
|
1336
1480
|
multiple,
|
|
1337
1481
|
onChange,
|
|
1338
1482
|
onDrop,
|
|
@@ -1347,7 +1491,9 @@ var FileSelector2 = ({
|
|
|
1347
1491
|
{
|
|
1348
1492
|
allowedFileTypes,
|
|
1349
1493
|
maxFileSize: maxSize,
|
|
1494
|
+
maxTotalSize,
|
|
1350
1495
|
customSizeMessage,
|
|
1496
|
+
customTotalSizeMessage,
|
|
1351
1497
|
customTypesMessage,
|
|
1352
1498
|
variant: "caption"
|
|
1353
1499
|
}
|
|
@@ -1355,12 +1501,13 @@ var FileSelector2 = ({
|
|
|
1355
1501
|
children
|
|
1356
1502
|
] }) : /* @__PURE__ */ jsxs14(Grid5, { container: true, rowSpacing: 3, flexDirection: "column", children: [
|
|
1357
1503
|
/* @__PURE__ */ jsxs14(Grid5, { children: [
|
|
1358
|
-
/* @__PURE__ */ jsx15(HeaderMessage, { maxFiles, maxSize }),
|
|
1504
|
+
/* @__PURE__ */ jsx15(HeaderMessage, { maxFiles, maxSize, maxTotalSize }),
|
|
1359
1505
|
/* @__PURE__ */ jsx15(
|
|
1360
1506
|
FileTypesMessage,
|
|
1361
1507
|
{
|
|
1362
1508
|
allowedFileTypes,
|
|
1363
1509
|
customSizeMessage,
|
|
1510
|
+
customTotalSizeMessage,
|
|
1364
1511
|
customTypesMessage,
|
|
1365
1512
|
variant: "body2"
|
|
1366
1513
|
}
|
|
@@ -1376,6 +1523,7 @@ var FileSelector2 = ({
|
|
|
1376
1523
|
enableDropArea,
|
|
1377
1524
|
maxFiles,
|
|
1378
1525
|
maxSize,
|
|
1526
|
+
maxTotalSize,
|
|
1379
1527
|
multiple,
|
|
1380
1528
|
onChange,
|
|
1381
1529
|
onDrop,
|
package/package.json
CHANGED
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',
|
|
@@ -50,6 +51,11 @@ export type DropzoneProps = {
|
|
|
50
51
|
* List of allowed file extensions (e.g. ['.pdf', '.doc']). Each extension must start with a dot
|
|
51
52
|
*/
|
|
52
53
|
allowedFileTypes?: `.${string}`[];
|
|
54
|
+
/**
|
|
55
|
+
* Regular expression pattern of allowed characters in file names
|
|
56
|
+
* @example "a-zA-Z0-9-_."
|
|
57
|
+
*/
|
|
58
|
+
allowedFileNameCharacters?: string;
|
|
53
59
|
/**
|
|
54
60
|
* Whether the dropzone is disabled
|
|
55
61
|
*/
|
|
@@ -66,6 +72,10 @@ export type DropzoneProps = {
|
|
|
66
72
|
* Maximum size of each file in bytes
|
|
67
73
|
*/
|
|
68
74
|
maxSize?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Maximum size of total upload in bytes
|
|
77
|
+
*/
|
|
78
|
+
maxTotalSize?: number;
|
|
69
79
|
/**
|
|
70
80
|
* Whether multiple file selection is allowed
|
|
71
81
|
*/
|
|
@@ -106,10 +116,12 @@ export const DropzoneContainer = styled(Box, { name: 'AvDropzoneContainer', slot
|
|
|
106
116
|
|
|
107
117
|
export const Dropzone = ({
|
|
108
118
|
allowedFileTypes = [],
|
|
119
|
+
allowedFileNameCharacters,
|
|
109
120
|
disabled,
|
|
110
121
|
enableDropArea = true,
|
|
111
122
|
maxFiles,
|
|
112
123
|
maxSize,
|
|
124
|
+
maxTotalSize,
|
|
113
125
|
multiple,
|
|
114
126
|
name,
|
|
115
127
|
onChange,
|
|
@@ -141,6 +153,35 @@ export const Dropzone = ({
|
|
|
141
153
|
message: `Too many files. You may only upload ${maxFiles} file(s).`,
|
|
142
154
|
});
|
|
143
155
|
}
|
|
156
|
+
|
|
157
|
+
// Check for allowed file name characters
|
|
158
|
+
if (allowedFileNameCharacters) {
|
|
159
|
+
const fileName = file.name.substring(0, file.name.lastIndexOf('.'));
|
|
160
|
+
const regExp = new RegExp(`([^${allowedFileNameCharacters}])`, 'g');
|
|
161
|
+
|
|
162
|
+
if (fileName.match(regExp) !== null) {
|
|
163
|
+
errors.push({
|
|
164
|
+
code: 'invalid-file-name-characters',
|
|
165
|
+
message: 'File name contains characters not allowed',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Explicit check for allowed file types
|
|
171
|
+
if (allowedFileTypes.length > 0) {
|
|
172
|
+
const fileName = file.name;
|
|
173
|
+
const fileExt = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
|
|
174
|
+
|
|
175
|
+
// Convert all file types to lowercase for comparison
|
|
176
|
+
const lowerCaseAllowedTypes = allowedFileTypes.map(type => type.toLowerCase());
|
|
177
|
+
|
|
178
|
+
if (!lowerCaseAllowedTypes.includes(fileExt)) {
|
|
179
|
+
errors.push({
|
|
180
|
+
code: 'file-invalid-type',
|
|
181
|
+
message: `File type ${fileExt} is not allowed`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
144
185
|
|
|
145
186
|
if (validator) {
|
|
146
187
|
const validatorErrors = validator(file);
|
|
@@ -155,7 +196,7 @@ export const Dropzone = ({
|
|
|
155
196
|
|
|
156
197
|
return errors.length > 0 ? errors : null;
|
|
157
198
|
},
|
|
158
|
-
[maxFiles, validator]
|
|
199
|
+
[maxFiles, validator, allowedFileNameCharacters, watch, name, allowedFileTypes]
|
|
159
200
|
);
|
|
160
201
|
|
|
161
202
|
const handleOnDrop = useCallback(
|
|
@@ -169,8 +210,64 @@ export const Dropzone = ({
|
|
|
169
210
|
|
|
170
211
|
const previous = watch(name) ?? [];
|
|
171
212
|
|
|
213
|
+
if (maxTotalSize) {
|
|
214
|
+
// Calculate current total size
|
|
215
|
+
const currentTotalSize = previous.reduce((sum: number, file: File) => sum + file.size, 0);
|
|
216
|
+
let newSize = 0;
|
|
217
|
+
|
|
218
|
+
const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
|
|
219
|
+
let sizeCounter = 0;
|
|
220
|
+
|
|
221
|
+
// Find the index where we exceed the total size limit
|
|
222
|
+
const cutoffIndex = acceptedFiles.findIndex((file) => {
|
|
223
|
+
sizeCounter += file.size;
|
|
224
|
+
return sizeCounter > availableSize;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// If we found files that exceed the limit
|
|
228
|
+
if (cutoffIndex !== -1) {
|
|
229
|
+
// Files that fit within the size limit
|
|
230
|
+
const filesToAdd = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
|
|
231
|
+
|
|
232
|
+
// Create rejection for excess files
|
|
233
|
+
fileRejections.push({
|
|
234
|
+
file: acceptedFiles[cutoffIndex],
|
|
235
|
+
errors: [
|
|
236
|
+
{
|
|
237
|
+
code: 'upload-too-large',
|
|
238
|
+
message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
id: counter.increment(),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Update acceptedFiles to only include files that fit
|
|
245
|
+
acceptedFiles = filesToAdd;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Calculate size of accepted files for the state update
|
|
249
|
+
newSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
250
|
+
setTotalSize((prev) => prev + newSize);
|
|
251
|
+
}
|
|
252
|
+
|
|
172
253
|
// Set accepted files to form context
|
|
173
|
-
|
|
254
|
+
const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
|
|
255
|
+
const filesToAdd = acceptedFiles.slice(0, remainingSlots);
|
|
256
|
+
setValue(name, previous.concat(filesToAdd));
|
|
257
|
+
|
|
258
|
+
// Add rejections for excess files if needed
|
|
259
|
+
if (maxFiles && acceptedFiles.length > remainingSlots) {
|
|
260
|
+
fileRejections.push({
|
|
261
|
+
file: acceptedFiles[remainingSlots], // Use the first excess file
|
|
262
|
+
errors: [
|
|
263
|
+
{
|
|
264
|
+
code: 'too-many-files',
|
|
265
|
+
message: `Too many files. You may only upload ${maxFiles} file(s).`,
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
id: counter.increment(),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
174
271
|
|
|
175
272
|
if (fileRejections.length > 0) {
|
|
176
273
|
const TOO_MANY_FILES_CODE = 'too-many-files';
|
|
@@ -206,7 +303,7 @@ export const Dropzone = ({
|
|
|
206
303
|
if (setFileRejections) setFileRejections(fileRejections);
|
|
207
304
|
if (onDrop) onDrop(acceptedFiles, fileRejections, event);
|
|
208
305
|
},
|
|
209
|
-
[setFileRejections]
|
|
306
|
+
[setFileRejections, setTotalSize, watch, name, maxTotalSize, maxFiles, setValue, onDrop]
|
|
210
307
|
);
|
|
211
308
|
|
|
212
309
|
const accept = allowedFileTypes.join(',');
|
|
@@ -239,7 +336,7 @@ export const Dropzone = ({
|
|
|
239
336
|
|
|
240
337
|
const handleOnClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
241
338
|
if (!enableDropArea && rootProps.onClick) rootProps.onClick(event);
|
|
242
|
-
if (onClick) onClick;
|
|
339
|
+
if (onClick) onClick(event);
|
|
243
340
|
};
|
|
244
341
|
|
|
245
342
|
const getFieldValue = () => {
|