@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/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
- setValue(name, previous.concat(acceptedFiles));
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
- const uploads = acceptedFiles.map((file) => startUpload(file, uploadOptions));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@availity/mui-file-selector",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
4
  "description": "Availity MUI file-selector Component - part of the @availity/element design system",
5
5
  "keywords": [
6
6
  "react",
@@ -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
- setValue(name, previous.concat(acceptedFiles));
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 = () => {