@availity/mui-file-selector 1.5.3 → 1.6.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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.6.0](https://github.com/Availity/element/compare/@availity/mui-file-selector@1.5.3...@availity/mui-file-selector@1.6.0) (2025-04-15)
6
+
7
+
8
+ ### Features
9
+
10
+ * **mui-file-selector:** allow customSizeMessage and customTypesMessage ([378ccc0](https://github.com/Availity/element/commit/378ccc05fac7bc0e9086aafd4ad9aa9fdd5026de))
11
+
5
12
  ## [1.5.3](https://github.com/Availity/element/compare/@availity/mui-file-selector@1.5.2...@availity/mui-file-selector@1.5.3) (2025-04-14)
6
13
 
7
14
  ### Dependency Updates
package/dist/index.d.mts CHANGED
@@ -83,6 +83,7 @@ type ErrorAlertProps = {
83
83
  id: number;
84
84
  onClose: () => void;
85
85
  };
86
+ declare const formatFileTooLarge: (message: string) => string;
86
87
  declare const ErrorAlert: ({ errors, fileName, id, onClose }: ErrorAlertProps) => react_jsx_runtime.JSX.Element | null;
87
88
 
88
89
  type Options = {
@@ -197,6 +198,14 @@ type FileSelectorProps = {
197
198
  * @default false
198
199
  */
199
200
  disabled?: boolean;
201
+ /**
202
+ * Overrides the standard file size message
203
+ */
204
+ customSizeMessage?: React.ReactNode;
205
+ /**
206
+ * Overrides the standard file types message
207
+ */
208
+ customTypesMessage?: React.ReactNode;
200
209
  /**
201
210
  * Whether to enable the dropzone area
202
211
  */
@@ -206,7 +215,7 @@ type FileSelectorProps = {
206
215
  */
207
216
  endpoint?: string;
208
217
  /**
209
- * Componet to render the File information. This should return a `ListItem`
218
+ * Component to render the File information. This should return a `ListItem`
210
219
  */
211
220
  customFileRow?: React.ElementType<{
212
221
  upload?: Upload;
@@ -272,20 +281,28 @@ type FileSelectorProps = {
272
281
  */
273
282
  disableRemove?: boolean;
274
283
  };
275
- declare const FileSelector: ({ name, allowedFileNameCharacters, allowedFileTypes, bucketId, clientId, children, customerId, customFileRow, disabled, enableDropArea, endpoint, isCloud, label, maxFiles, maxSize, multiple, onChange, onDrop, onUploadRemove, queryOptions, uploadOptions, validator, disableRemove, }: FileSelectorProps) => react_jsx_runtime.JSX.Element;
284
+ declare const FileSelector: ({ name, allowedFileNameCharacters, allowedFileTypes, bucketId, clientId, children, customSizeMessage, customTypesMessage, customerId, customFileRow, disabled, enableDropArea, endpoint, isCloud, label, maxFiles, maxSize, multiple, onChange, onDrop, onUploadRemove, queryOptions, uploadOptions, validator, disableRemove, }: FileSelectorProps) => react_jsx_runtime.JSX.Element;
276
285
 
277
286
  type FileTypesMessageProps = {
278
287
  /**
279
288
  * Allowed file type extensions. Each extension should be prefixed with a ".". eg: .txt, .pdf, .png
280
289
  */
281
290
  allowedFileTypes?: `.${string}`[];
291
+ /**
292
+ * Overrides the standard file size message
293
+ */
294
+ customSizeMessage?: React.ReactNode;
295
+ /**
296
+ * Overrides the standard file types message
297
+ */
298
+ customTypesMessage?: React.ReactNode;
282
299
  /**
283
300
  * Maximum size per file in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
284
301
  */
285
302
  maxFileSize?: number;
286
303
  variant?: 'caption' | 'body2';
287
304
  };
288
- declare const FileTypesMessage: ({ allowedFileTypes, maxFileSize, variant, }: FileTypesMessageProps) => react_jsx_runtime.JSX.Element;
305
+ declare const FileTypesMessage: ({ allowedFileTypes, customSizeMessage, customTypesMessage, maxFileSize, variant, }: FileTypesMessageProps) => react_jsx_runtime.JSX.Element;
289
306
 
290
307
  type HeaderMessageProps = {
291
308
  /**
@@ -319,4 +336,4 @@ type UploadProgressBarProps = {
319
336
  };
320
337
  declare const UploadProgressBar: ({ upload, onProgress, onError, onSuccess }: UploadProgressBarProps) => react_jsx_runtime.JSX.Element;
321
338
 
322
- export { Dropzone, type DropzoneProps, ErrorAlert, type ErrorAlertProps, FileList, type FileListProps, FilePickerBtn, type FilePickerBtnProps, FileRow, type FileRowProps, FileSelector, type FileSelectorProps, FileTypesMessage, type FileTypesMessageProps, HeaderMessage, type HeaderMessageProps, type Options, UploadProgressBar, type UploadProgressBarProps, type UploadQueryOptions, useUploadCore };
339
+ export { Dropzone, type DropzoneProps, ErrorAlert, type ErrorAlertProps, FileList, type FileListProps, FilePickerBtn, type FilePickerBtnProps, FileRow, type FileRowProps, FileSelector, type FileSelectorProps, FileTypesMessage, type FileTypesMessageProps, HeaderMessage, type HeaderMessageProps, type Options, UploadProgressBar, type UploadProgressBarProps, type UploadQueryOptions, formatFileTooLarge, useUploadCore };
package/dist/index.d.ts CHANGED
@@ -83,6 +83,7 @@ type ErrorAlertProps = {
83
83
  id: number;
84
84
  onClose: () => void;
85
85
  };
86
+ declare const formatFileTooLarge: (message: string) => string;
86
87
  declare const ErrorAlert: ({ errors, fileName, id, onClose }: ErrorAlertProps) => react_jsx_runtime.JSX.Element | null;
87
88
 
88
89
  type Options = {
@@ -197,6 +198,14 @@ type FileSelectorProps = {
197
198
  * @default false
198
199
  */
199
200
  disabled?: boolean;
201
+ /**
202
+ * Overrides the standard file size message
203
+ */
204
+ customSizeMessage?: React.ReactNode;
205
+ /**
206
+ * Overrides the standard file types message
207
+ */
208
+ customTypesMessage?: React.ReactNode;
200
209
  /**
201
210
  * Whether to enable the dropzone area
202
211
  */
@@ -206,7 +215,7 @@ type FileSelectorProps = {
206
215
  */
207
216
  endpoint?: string;
208
217
  /**
209
- * Componet to render the File information. This should return a `ListItem`
218
+ * Component to render the File information. This should return a `ListItem`
210
219
  */
211
220
  customFileRow?: React.ElementType<{
212
221
  upload?: Upload;
@@ -272,20 +281,28 @@ type FileSelectorProps = {
272
281
  */
273
282
  disableRemove?: boolean;
274
283
  };
275
- declare const FileSelector: ({ name, allowedFileNameCharacters, allowedFileTypes, bucketId, clientId, children, customerId, customFileRow, disabled, enableDropArea, endpoint, isCloud, label, maxFiles, maxSize, multiple, onChange, onDrop, onUploadRemove, queryOptions, uploadOptions, validator, disableRemove, }: FileSelectorProps) => react_jsx_runtime.JSX.Element;
284
+ declare const FileSelector: ({ name, allowedFileNameCharacters, allowedFileTypes, bucketId, clientId, children, customSizeMessage, customTypesMessage, customerId, customFileRow, disabled, enableDropArea, endpoint, isCloud, label, maxFiles, maxSize, multiple, onChange, onDrop, onUploadRemove, queryOptions, uploadOptions, validator, disableRemove, }: FileSelectorProps) => react_jsx_runtime.JSX.Element;
276
285
 
277
286
  type FileTypesMessageProps = {
278
287
  /**
279
288
  * Allowed file type extensions. Each extension should be prefixed with a ".". eg: .txt, .pdf, .png
280
289
  */
281
290
  allowedFileTypes?: `.${string}`[];
291
+ /**
292
+ * Overrides the standard file size message
293
+ */
294
+ customSizeMessage?: React.ReactNode;
295
+ /**
296
+ * Overrides the standard file types message
297
+ */
298
+ customTypesMessage?: React.ReactNode;
282
299
  /**
283
300
  * Maximum size per file in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
284
301
  */
285
302
  maxFileSize?: number;
286
303
  variant?: 'caption' | 'body2';
287
304
  };
288
- declare const FileTypesMessage: ({ allowedFileTypes, maxFileSize, variant, }: FileTypesMessageProps) => react_jsx_runtime.JSX.Element;
305
+ declare const FileTypesMessage: ({ allowedFileTypes, customSizeMessage, customTypesMessage, maxFileSize, variant, }: FileTypesMessageProps) => react_jsx_runtime.JSX.Element;
289
306
 
290
307
  type HeaderMessageProps = {
291
308
  /**
@@ -319,4 +336,4 @@ type UploadProgressBarProps = {
319
336
  };
320
337
  declare const UploadProgressBar: ({ upload, onProgress, onError, onSuccess }: UploadProgressBarProps) => react_jsx_runtime.JSX.Element;
321
338
 
322
- export { Dropzone, type DropzoneProps, ErrorAlert, type ErrorAlertProps, FileList, type FileListProps, FilePickerBtn, type FilePickerBtnProps, FileRow, type FileRowProps, FileSelector, type FileSelectorProps, FileTypesMessage, type FileTypesMessageProps, HeaderMessage, type HeaderMessageProps, type Options, UploadProgressBar, type UploadProgressBarProps, type UploadQueryOptions, useUploadCore };
339
+ export { Dropzone, type DropzoneProps, ErrorAlert, type ErrorAlertProps, FileList, type FileListProps, FilePickerBtn, type FilePickerBtnProps, FileRow, type FileRowProps, FileSelector, type FileSelectorProps, FileTypesMessage, type FileTypesMessageProps, HeaderMessage, type HeaderMessageProps, type Options, UploadProgressBar, type UploadProgressBarProps, type UploadQueryOptions, formatFileTooLarge, useUploadCore };
package/dist/index.js CHANGED
@@ -88,6 +88,7 @@ __export(index_exports, {
88
88
  FileTypesMessage: () => FileTypesMessage,
89
89
  HeaderMessage: () => HeaderMessage,
90
90
  UploadProgressBar: () => UploadProgressBar,
91
+ formatFileTooLarge: () => formatFileTooLarge,
91
92
  useUploadCore: () => useUploadCore
92
93
  });
93
94
  module.exports = __toCommonJS(index_exports);
@@ -324,6 +325,46 @@ var Dropzone = ({
324
325
  // src/lib/ErrorAlert.tsx
325
326
  var import_mui_alert = require("@availity/mui-alert");
326
327
  var import_mui_list = require("@availity/mui-list");
328
+
329
+ // src/lib/util.ts
330
+ var import_mui_icon2 = require("@availity/mui-icon");
331
+ function formatBytes(bytes, decimals = 2) {
332
+ if (!+bytes) return "0 Bytes";
333
+ const k = 1024;
334
+ const dm = decimals < 0 ? 0 : decimals;
335
+ const sizes = ["Bytes", "KB", "MB", "GB"];
336
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
337
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
338
+ }
339
+ var FILE_EXT_ICONS = {
340
+ png: import_mui_icon2.FileImageIcon,
341
+ jpg: import_mui_icon2.FileImageIcon,
342
+ jpeg: import_mui_icon2.FileImageIcon,
343
+ gif: import_mui_icon2.FileImageIcon,
344
+ csv: import_mui_icon2.FileCsvIcon,
345
+ ppt: import_mui_icon2.FilePowerpointIcon,
346
+ pptx: import_mui_icon2.FilePowerpointIcon,
347
+ xls: import_mui_icon2.FileExcelIcon,
348
+ xlsx: import_mui_icon2.FileExcelIcon,
349
+ doc: import_mui_icon2.FileWordIcon,
350
+ docx: import_mui_icon2.FileWordIcon,
351
+ txt: import_mui_icon2.FileLinesIcon,
352
+ text: import_mui_icon2.FileLinesIcon,
353
+ zip: import_mui_icon2.FileArchiveIcon,
354
+ "7zip": import_mui_icon2.FileArchiveIcon,
355
+ xml: import_mui_icon2.FileCodeIcon,
356
+ html: import_mui_icon2.FileCodeIcon,
357
+ pdf: import_mui_icon2.FilePdfIcon
358
+ };
359
+ var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
360
+ var getFileExtIcon = (fileName) => {
361
+ var _a;
362
+ const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
363
+ const icon = isValidKey(ext) ? FILE_EXT_ICONS[ext] : import_mui_icon2.FileIcon;
364
+ return icon;
365
+ };
366
+
367
+ // src/lib/ErrorAlert.tsx
327
368
  var import_jsx_runtime3 = require("react/jsx-runtime");
328
369
  var codes = {
329
370
  "file-too-large": "File exceeds maximum size",
@@ -332,6 +373,13 @@ var codes = {
332
373
  "too-many-file": "Too many files",
333
374
  "duplicate-name": "Duplicate file selected"
334
375
  };
376
+ var FILE_SIZE_REGEX = /\b(\d+)\b/;
377
+ var formatFileTooLarge = (message) => {
378
+ const fileSize = message.match(FILE_SIZE_REGEX);
379
+ if (!fileSize) return message;
380
+ const formattedSize = formatBytes(Number(fileSize[0]));
381
+ return message.replace(` bytes`, "").replace(FILE_SIZE_REGEX, formattedSize);
382
+ };
335
383
  var ErrorAlert = ({ errors, fileName, id, onClose }) => {
336
384
  if (errors.length === 0) return null;
337
385
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_mui_alert.Alert, { severity: "error", onClose, children: errors.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
@@ -341,23 +389,31 @@ var ErrorAlert = ({ errors, fileName, id, onClose }) => {
341
389
  " error(s) found when uploading ",
342
390
  fileName
343
391
  ] }),
344
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_mui_list.List, { sx: {
345
- listStyleType: "disc",
346
- listStylePosition: "inside",
347
- marginLeft: 1,
348
- ".MuiListItem-root": {
349
- display: "list-item"
392
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
393
+ import_mui_list.List,
394
+ {
395
+ sx: {
396
+ listStyleType: "disc",
397
+ listStylePosition: "inside",
398
+ marginLeft: 1,
399
+ ".MuiListItem-root": {
400
+ display: "list-item"
401
+ }
402
+ },
403
+ disablePadding: true,
404
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: errors.map((error) => {
405
+ const message = error.code === "file-too-large" ? formatFileTooLarge(error.message) : error.message;
406
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_mui_list.ListItem, { disableGutters: true, disablePadding: true, divider: false, children: message }, `${id}-${error.code}`);
407
+ }) })
350
408
  }
351
- }, disablePadding: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: errors.map((error) => {
352
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_mui_list.ListItem, { disableGutters: true, disablePadding: true, divider: false, children: error.message }, `${id}-${error.code}`);
353
- }) }) })
409
+ )
354
410
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
355
411
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_mui_alert.AlertTitle, { children: [
356
412
  codes[errors[0].code] || "Error",
357
413
  ": ",
358
414
  fileName
359
415
  ] }),
360
- errors[0].message
416
+ errors[0].code === "file-too-large" ? formatFileTooLarge(errors[0].message) : errors[0].message
361
417
  ] }) });
362
418
  };
363
419
 
@@ -376,9 +432,9 @@ var import_mui_button3 = require("@availity/mui-button");
376
432
  var import_Dialog = __toESM(require("@mui/material/Dialog"));
377
433
  var import_styles2 = require("@mui/material/styles");
378
434
  var import_mui_button2 = require("@availity/mui-button");
379
- var import_mui_icon2 = require("@availity/mui-icon");
435
+ var import_mui_icon3 = require("@availity/mui-icon");
380
436
  var import_jsx_runtime4 = require("react/jsx-runtime");
381
- var CloseButton = (args) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_mui_button2.IconButton, __spreadProps(__spreadValues({ title: "Close Dialog", color: "secondary" }, args), { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_mui_icon2.CloseIcon, { fontSize: "xsmall" }) }));
437
+ var CloseButton = (args) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_mui_button2.IconButton, __spreadProps(__spreadValues({ title: "Close Dialog", color: "secondary" }, args), { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_mui_icon3.CloseIcon, { fontSize: "xsmall" }) }));
382
438
  var CloseButtonSlot = (0, import_styles2.styled)(CloseButton, {
383
439
  name: "MuiDialog",
384
440
  slot: "AvCloseButton",
@@ -444,7 +500,7 @@ var DialogTitle = (_a) => {
444
500
 
445
501
  // src/lib/UploadProgressBar.tsx
446
502
  var import_mui_form_utils3 = require("@availity/mui-form-utils");
447
- var import_mui_icon3 = require("@availity/mui-icon");
503
+ var import_mui_icon4 = require("@availity/mui-icon");
448
504
  var import_mui_layout3 = require("@availity/mui-layout");
449
505
  var import_mui_list2 = require("@availity/mui-list");
450
506
  var import_mui_progress = require("@availity/mui-progress");
@@ -587,7 +643,7 @@ var UploadProgressBar = ({ upload, onProgress, onError, onSuccess }) => {
587
643
  upload.onError.push(handleOnError);
588
644
  return errorMessage ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_mui_layout3.Box, { sx: { display: "flex", flexWrap: "wrap", columnGap: "4px" }, children: [
589
645
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_mui_list2.ListItemText, { slotProps: { primary: { color: "text.error", variant: "body2", component: "div" } }, sx: { wordWrap: "break-word" }, children: [
590
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_icon3.WarningTriangleIcon, { sx: { verticalAlign: "middle", mt: "-2px" } }),
646
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_icon4.WarningTriangleIcon, { sx: { verticalAlign: "middle", mt: "-2px" } }),
591
647
  " ",
592
648
  errorMessage
593
649
  ] }),
@@ -604,7 +660,7 @@ var UploadProgressBar = ({ upload, onProgress, onError, onSuccess }) => {
604
660
  onChange: handlePasswordChange,
605
661
  autoFocus: true,
606
662
  InputProps: {
607
- endAdornment: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_form_utils3.InputAdornment, { position: "end", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_button3.IconButton, { title: "password visibility", onClick: () => setShowPassword((prev) => !prev), edge: "end", children: showPassword ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_icon3.EyeIcon, { fontSize: "small" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_icon3.EyeSlashIcon, { fontSize: "small" }) }) })
663
+ endAdornment: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_form_utils3.InputAdornment, { position: "end", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_button3.IconButton, { title: "password visibility", onClick: () => setShowPassword((prev) => !prev), edge: "end", children: showPassword ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_icon4.EyeIcon, { fontSize: "small" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_icon4.EyeSlashIcon, { fontSize: "small" }) }) })
608
664
  }
609
665
  }
610
666
  ) }),
@@ -614,44 +670,6 @@ var UploadProgressBar = ({ upload, onProgress, onError, onSuccess }) => {
614
670
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_mui_progress.LinearProgress, { value: statePercentage, "aria-label": `${upload.file.name}-progress` });
615
671
  };
616
672
 
617
- // src/lib/util.ts
618
- var import_mui_icon4 = require("@availity/mui-icon");
619
- function formatBytes(bytes, decimals = 2) {
620
- if (!+bytes) return "0 Bytes";
621
- const k = 1024;
622
- const dm = decimals < 0 ? 0 : decimals;
623
- const sizes = ["Bytes", "KB", "MB", "GB"];
624
- const i = Math.floor(Math.log(bytes) / Math.log(k));
625
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
626
- }
627
- var FILE_EXT_ICONS = {
628
- png: import_mui_icon4.FileImageIcon,
629
- jpg: import_mui_icon4.FileImageIcon,
630
- jpeg: import_mui_icon4.FileImageIcon,
631
- gif: import_mui_icon4.FileImageIcon,
632
- csv: import_mui_icon4.FileCsvIcon,
633
- ppt: import_mui_icon4.FilePowerpointIcon,
634
- pptx: import_mui_icon4.FilePowerpointIcon,
635
- xls: import_mui_icon4.FileExcelIcon,
636
- xlsx: import_mui_icon4.FileExcelIcon,
637
- doc: import_mui_icon4.FileWordIcon,
638
- docx: import_mui_icon4.FileWordIcon,
639
- txt: import_mui_icon4.FileLinesIcon,
640
- text: import_mui_icon4.FileLinesIcon,
641
- zip: import_mui_icon4.FileArchiveIcon,
642
- "7zip": import_mui_icon4.FileArchiveIcon,
643
- xml: import_mui_icon4.FileCodeIcon,
644
- html: import_mui_icon4.FileCodeIcon,
645
- pdf: import_mui_icon4.FilePdfIcon
646
- };
647
- var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
648
- var getFileExtIcon = (fileName) => {
649
- var _a;
650
- const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
651
- const icon = isValidKey(ext) ? FILE_EXT_ICONS[ext] : import_mui_icon4.FileIcon;
652
- return icon;
653
- };
654
-
655
673
  // src/lib/useUploadCore.tsx
656
674
  var import_react_query = require("@tanstack/react-query");
657
675
  var import_upload_core = __toESM(require("@availity/upload-core"));
@@ -751,17 +769,20 @@ var import_react_hook_form3 = require("react-hook-form");
751
769
  var import_react_query2 = require("@tanstack/react-query");
752
770
  var import_mui_layout5 = require("@availity/mui-layout");
753
771
  var import_mui_typography5 = require("@availity/mui-typography");
772
+ var import_mui_alert3 = require("@availity/mui-alert");
754
773
 
755
774
  // src/lib/FileTypesMessage.tsx
756
775
  var import_mui_typography3 = require("@availity/mui-typography");
757
776
  var import_jsx_runtime12 = require("react/jsx-runtime");
758
777
  var FileTypesMessage = ({
759
778
  allowedFileTypes = [],
779
+ customSizeMessage,
780
+ customTypesMessage,
760
781
  maxFileSize,
761
782
  variant = "caption"
762
783
  }) => {
763
- const fileSizeMsg = typeof maxFileSize === "number" ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null;
764
- const fileTypesMsg = allowedFileTypes.length > 0 ? `Supported file types include: ${allowedFileTypes.join(", ")}` : "All file types allowed.";
784
+ const fileSizeMsg = customSizeMessage || (typeof maxFileSize === "number" ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null);
785
+ const fileTypesMsg = customTypesMessage || (allowedFileTypes.length > 0 ? `Supported file types include: ${allowedFileTypes.join(", ")}` : "All file types allowed.");
765
786
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_mui_typography3.Typography, { variant, children: [
766
787
  fileSizeMsg,
767
788
  fileTypesMsg
@@ -790,6 +811,8 @@ var FileSelector = ({
790
811
  bucketId,
791
812
  clientId,
792
813
  children,
814
+ customSizeMessage,
815
+ customTypesMessage,
793
816
  customerId,
794
817
  customFileRow,
795
818
  disabled = false,
@@ -843,6 +866,13 @@ var FileSelector = ({
843
866
  const rejections = fileRejections.filter((value) => value.id !== id);
844
867
  setFileRejections(rejections);
845
868
  };
869
+ const TOO_MANY_FILES_CODE = "too-many-files";
870
+ const tooManyFilesRejections = fileRejections.filter(
871
+ (rejection) => rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
872
+ );
873
+ const otherRejections = fileRejections.filter(
874
+ (rejection) => !rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
875
+ );
846
876
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
847
877
  enableDropArea ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
848
878
  label ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_mui_typography5.Typography, { marginBottom: "4px", children: label }) : null,
@@ -863,12 +893,29 @@ var FileSelector = ({
863
893
  validator
864
894
  }
865
895
  ),
866
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(FileTypesMessage, { allowedFileTypes, maxFileSize: maxSize, variant: "caption" }),
896
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
897
+ FileTypesMessage,
898
+ {
899
+ allowedFileTypes,
900
+ maxFileSize: maxSize,
901
+ customSizeMessage,
902
+ customTypesMessage,
903
+ variant: "caption"
904
+ }
905
+ ),
867
906
  children
868
907
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_mui_layout5.Grid, { container: true, rowSpacing: 3, flexDirection: "column", children: [
869
908
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_mui_layout5.Grid, { children: [
870
909
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(HeaderMessage, { maxFiles, maxSize }),
871
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(FileTypesMessage, { allowedFileTypes, variant: "body2" })
910
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
911
+ FileTypesMessage,
912
+ {
913
+ allowedFileTypes,
914
+ customSizeMessage,
915
+ customTypesMessage,
916
+ variant: "body2"
917
+ }
918
+ )
872
919
  ] }),
873
920
  children ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_mui_layout5.Grid, { children }) : null,
874
921
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_mui_layout5.Grid, { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
@@ -889,7 +936,20 @@ var FileSelector = ({
889
936
  }
890
937
  ) })
891
938
  ] }),
892
- fileRejections.length > 0 ? fileRejections.map((rejection) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
939
+ tooManyFilesRejections.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
940
+ import_mui_alert3.Alert,
941
+ {
942
+ severity: "error",
943
+ onClose: () => tooManyFilesRejections.forEach((rejection) => handleRemoveRejection(rejection.id)),
944
+ children: [
945
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_mui_alert3.AlertTitle, { children: "Items not allowed." }),
946
+ "Too many files are selected for upload, maximum ",
947
+ maxFiles,
948
+ " allowed."
949
+ ]
950
+ }
951
+ ),
952
+ otherRejections.length > 0 && otherRejections.map((rejection) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
893
953
  ErrorAlert,
894
954
  {
895
955
  errors: rejection.errors,
@@ -898,7 +958,7 @@ var FileSelector = ({
898
958
  onClose: () => handleRemoveRejection(rejection.id)
899
959
  },
900
960
  rejection.id
901
- )) : null,
961
+ )),
902
962
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
903
963
  FileList,
904
964
  {
@@ -923,5 +983,6 @@ var FileSelector = ({
923
983
  FileTypesMessage,
924
984
  HeaderMessage,
925
985
  UploadProgressBar,
986
+ formatFileTooLarge,
926
987
  useUploadCore
927
988
  });
package/dist/index.mjs CHANGED
@@ -282,6 +282,57 @@ var Dropzone = ({
282
282
  // src/lib/ErrorAlert.tsx
283
283
  import { Alert, AlertTitle } from "@availity/mui-alert";
284
284
  import { List, ListItem } from "@availity/mui-list";
285
+
286
+ // src/lib/util.ts
287
+ import {
288
+ FileArchiveIcon,
289
+ FileCodeIcon,
290
+ FileCsvIcon,
291
+ FileExcelIcon,
292
+ FileIcon,
293
+ FileImageIcon,
294
+ FileLinesIcon,
295
+ FilePdfIcon,
296
+ FilePowerpointIcon,
297
+ FileWordIcon
298
+ } from "@availity/mui-icon";
299
+ function formatBytes(bytes, decimals = 2) {
300
+ if (!+bytes) return "0 Bytes";
301
+ const k = 1024;
302
+ const dm = decimals < 0 ? 0 : decimals;
303
+ const sizes = ["Bytes", "KB", "MB", "GB"];
304
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
305
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
306
+ }
307
+ var FILE_EXT_ICONS = {
308
+ png: FileImageIcon,
309
+ jpg: FileImageIcon,
310
+ jpeg: FileImageIcon,
311
+ gif: FileImageIcon,
312
+ csv: FileCsvIcon,
313
+ ppt: FilePowerpointIcon,
314
+ pptx: FilePowerpointIcon,
315
+ xls: FileExcelIcon,
316
+ xlsx: FileExcelIcon,
317
+ doc: FileWordIcon,
318
+ docx: FileWordIcon,
319
+ txt: FileLinesIcon,
320
+ text: FileLinesIcon,
321
+ zip: FileArchiveIcon,
322
+ "7zip": FileArchiveIcon,
323
+ xml: FileCodeIcon,
324
+ html: FileCodeIcon,
325
+ pdf: FilePdfIcon
326
+ };
327
+ var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
328
+ var getFileExtIcon = (fileName) => {
329
+ var _a;
330
+ const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
331
+ const icon = isValidKey(ext) ? FILE_EXT_ICONS[ext] : FileIcon;
332
+ return icon;
333
+ };
334
+
335
+ // src/lib/ErrorAlert.tsx
285
336
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
286
337
  var codes = {
287
338
  "file-too-large": "File exceeds maximum size",
@@ -290,6 +341,13 @@ var codes = {
290
341
  "too-many-file": "Too many files",
291
342
  "duplicate-name": "Duplicate file selected"
292
343
  };
344
+ var FILE_SIZE_REGEX = /\b(\d+)\b/;
345
+ var formatFileTooLarge = (message) => {
346
+ const fileSize = message.match(FILE_SIZE_REGEX);
347
+ if (!fileSize) return message;
348
+ const formattedSize = formatBytes(Number(fileSize[0]));
349
+ return message.replace(` bytes`, "").replace(FILE_SIZE_REGEX, formattedSize);
350
+ };
293
351
  var ErrorAlert = ({ errors, fileName, id, onClose }) => {
294
352
  if (errors.length === 0) return null;
295
353
  return /* @__PURE__ */ jsx3(Alert, { severity: "error", onClose, children: errors.length > 1 ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
@@ -299,23 +357,31 @@ var ErrorAlert = ({ errors, fileName, id, onClose }) => {
299
357
  " error(s) found when uploading ",
300
358
  fileName
301
359
  ] }),
302
- /* @__PURE__ */ jsx3(List, { sx: {
303
- listStyleType: "disc",
304
- listStylePosition: "inside",
305
- marginLeft: 1,
306
- ".MuiListItem-root": {
307
- display: "list-item"
360
+ /* @__PURE__ */ jsx3(
361
+ List,
362
+ {
363
+ sx: {
364
+ listStyleType: "disc",
365
+ listStylePosition: "inside",
366
+ marginLeft: 1,
367
+ ".MuiListItem-root": {
368
+ display: "list-item"
369
+ }
370
+ },
371
+ disablePadding: true,
372
+ children: /* @__PURE__ */ jsx3(Fragment3, { children: errors.map((error) => {
373
+ const message = error.code === "file-too-large" ? formatFileTooLarge(error.message) : error.message;
374
+ return /* @__PURE__ */ jsx3(ListItem, { disableGutters: true, disablePadding: true, divider: false, children: message }, `${id}-${error.code}`);
375
+ }) })
308
376
  }
309
- }, disablePadding: true, children: /* @__PURE__ */ jsx3(Fragment3, { children: errors.map((error) => {
310
- return /* @__PURE__ */ jsx3(ListItem, { disableGutters: true, disablePadding: true, divider: false, children: error.message }, `${id}-${error.code}`);
311
- }) }) })
377
+ )
312
378
  ] }) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
313
379
  /* @__PURE__ */ jsxs3(AlertTitle, { children: [
314
380
  codes[errors[0].code] || "Error",
315
381
  ": ",
316
382
  fileName
317
383
  ] }),
318
- errors[0].message
384
+ errors[0].code === "file-too-large" ? formatFileTooLarge(errors[0].message) : errors[0].message
319
385
  ] }) });
320
386
  };
321
387
 
@@ -578,55 +644,6 @@ var UploadProgressBar = ({ upload, onProgress, onError, onSuccess }) => {
578
644
  ] }) : /* @__PURE__ */ jsx10(LinearProgress, { value: statePercentage, "aria-label": `${upload.file.name}-progress` });
579
645
  };
580
646
 
581
- // src/lib/util.ts
582
- import {
583
- FileArchiveIcon,
584
- FileCodeIcon,
585
- FileCsvIcon,
586
- FileExcelIcon,
587
- FileIcon,
588
- FileImageIcon,
589
- FileLinesIcon,
590
- FilePdfIcon,
591
- FilePowerpointIcon,
592
- FileWordIcon
593
- } from "@availity/mui-icon";
594
- function formatBytes(bytes, decimals = 2) {
595
- if (!+bytes) return "0 Bytes";
596
- const k = 1024;
597
- const dm = decimals < 0 ? 0 : decimals;
598
- const sizes = ["Bytes", "KB", "MB", "GB"];
599
- const i = Math.floor(Math.log(bytes) / Math.log(k));
600
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
601
- }
602
- var FILE_EXT_ICONS = {
603
- png: FileImageIcon,
604
- jpg: FileImageIcon,
605
- jpeg: FileImageIcon,
606
- gif: FileImageIcon,
607
- csv: FileCsvIcon,
608
- ppt: FilePowerpointIcon,
609
- pptx: FilePowerpointIcon,
610
- xls: FileExcelIcon,
611
- xlsx: FileExcelIcon,
612
- doc: FileWordIcon,
613
- docx: FileWordIcon,
614
- txt: FileLinesIcon,
615
- text: FileLinesIcon,
616
- zip: FileArchiveIcon,
617
- "7zip": FileArchiveIcon,
618
- xml: FileCodeIcon,
619
- html: FileCodeIcon,
620
- pdf: FilePdfIcon
621
- };
622
- var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
623
- var getFileExtIcon = (fileName) => {
624
- var _a;
625
- const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
626
- const icon = isValidKey(ext) ? FILE_EXT_ICONS[ext] : FileIcon;
627
- return icon;
628
- };
629
-
630
647
  // src/lib/useUploadCore.tsx
631
648
  import { useQuery } from "@tanstack/react-query";
632
649
  import Upload from "@availity/upload-core";
@@ -726,17 +743,20 @@ import { useFormContext as useFormContext2 } from "react-hook-form";
726
743
  import { useQueryClient } from "@tanstack/react-query";
727
744
  import { Grid as Grid3 } from "@availity/mui-layout";
728
745
  import { Typography as Typography5 } from "@availity/mui-typography";
746
+ import { Alert as Alert2, AlertTitle as AlertTitle2 } from "@availity/mui-alert";
729
747
 
730
748
  // src/lib/FileTypesMessage.tsx
731
749
  import { Typography as Typography3 } from "@availity/mui-typography";
732
750
  import { jsxs as jsxs9 } from "react/jsx-runtime";
733
751
  var FileTypesMessage = ({
734
752
  allowedFileTypes = [],
753
+ customSizeMessage,
754
+ customTypesMessage,
735
755
  maxFileSize,
736
756
  variant = "caption"
737
757
  }) => {
738
- const fileSizeMsg = typeof maxFileSize === "number" ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null;
739
- const fileTypesMsg = allowedFileTypes.length > 0 ? `Supported file types include: ${allowedFileTypes.join(", ")}` : "All file types allowed.";
758
+ const fileSizeMsg = customSizeMessage || (typeof maxFileSize === "number" ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null);
759
+ const fileTypesMsg = customTypesMessage || (allowedFileTypes.length > 0 ? `Supported file types include: ${allowedFileTypes.join(", ")}` : "All file types allowed.");
740
760
  return /* @__PURE__ */ jsxs9(Typography3, { variant, children: [
741
761
  fileSizeMsg,
742
762
  fileTypesMsg
@@ -765,6 +785,8 @@ var FileSelector = ({
765
785
  bucketId,
766
786
  clientId,
767
787
  children,
788
+ customSizeMessage,
789
+ customTypesMessage,
768
790
  customerId,
769
791
  customFileRow,
770
792
  disabled = false,
@@ -818,6 +840,13 @@ var FileSelector = ({
818
840
  const rejections = fileRejections.filter((value) => value.id !== id);
819
841
  setFileRejections(rejections);
820
842
  };
843
+ const TOO_MANY_FILES_CODE = "too-many-files";
844
+ const tooManyFilesRejections = fileRejections.filter(
845
+ (rejection) => rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
846
+ );
847
+ const otherRejections = fileRejections.filter(
848
+ (rejection) => !rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
849
+ );
821
850
  return /* @__PURE__ */ jsxs11(Fragment5, { children: [
822
851
  enableDropArea ? /* @__PURE__ */ jsxs11(Fragment5, { children: [
823
852
  label ? /* @__PURE__ */ jsx12(Typography5, { marginBottom: "4px", children: label }) : null,
@@ -838,12 +867,29 @@ var FileSelector = ({
838
867
  validator
839
868
  }
840
869
  ),
841
- /* @__PURE__ */ jsx12(FileTypesMessage, { allowedFileTypes, maxFileSize: maxSize, variant: "caption" }),
870
+ /* @__PURE__ */ jsx12(
871
+ FileTypesMessage,
872
+ {
873
+ allowedFileTypes,
874
+ maxFileSize: maxSize,
875
+ customSizeMessage,
876
+ customTypesMessage,
877
+ variant: "caption"
878
+ }
879
+ ),
842
880
  children
843
881
  ] }) : /* @__PURE__ */ jsxs11(Grid3, { container: true, rowSpacing: 3, flexDirection: "column", children: [
844
882
  /* @__PURE__ */ jsxs11(Grid3, { children: [
845
883
  /* @__PURE__ */ jsx12(HeaderMessage, { maxFiles, maxSize }),
846
- /* @__PURE__ */ jsx12(FileTypesMessage, { allowedFileTypes, variant: "body2" })
884
+ /* @__PURE__ */ jsx12(
885
+ FileTypesMessage,
886
+ {
887
+ allowedFileTypes,
888
+ customSizeMessage,
889
+ customTypesMessage,
890
+ variant: "body2"
891
+ }
892
+ )
847
893
  ] }),
848
894
  children ? /* @__PURE__ */ jsx12(Grid3, { children }) : null,
849
895
  /* @__PURE__ */ jsx12(Grid3, { children: /* @__PURE__ */ jsx12(
@@ -864,7 +910,20 @@ var FileSelector = ({
864
910
  }
865
911
  ) })
866
912
  ] }),
867
- fileRejections.length > 0 ? fileRejections.map((rejection) => /* @__PURE__ */ jsx12(
913
+ tooManyFilesRejections.length > 0 && /* @__PURE__ */ jsxs11(
914
+ Alert2,
915
+ {
916
+ severity: "error",
917
+ onClose: () => tooManyFilesRejections.forEach((rejection) => handleRemoveRejection(rejection.id)),
918
+ children: [
919
+ /* @__PURE__ */ jsx12(AlertTitle2, { children: "Items not allowed." }),
920
+ "Too many files are selected for upload, maximum ",
921
+ maxFiles,
922
+ " allowed."
923
+ ]
924
+ }
925
+ ),
926
+ otherRejections.length > 0 && otherRejections.map((rejection) => /* @__PURE__ */ jsx12(
868
927
  ErrorAlert,
869
928
  {
870
929
  errors: rejection.errors,
@@ -873,7 +932,7 @@ var FileSelector = ({
873
932
  onClose: () => handleRemoveRejection(rejection.id)
874
933
  },
875
934
  rejection.id
876
- )) : null,
935
+ )),
877
936
  /* @__PURE__ */ jsx12(
878
937
  FileList,
879
938
  {
@@ -897,5 +956,6 @@ export {
897
956
  FileTypesMessage,
898
957
  HeaderMessage,
899
958
  UploadProgressBar,
959
+ formatFileTooLarge,
900
960
  useUploadCore
901
961
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@availity/mui-file-selector",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "Availity MUI file-selector Component - part of the @availity/element design system",
5
5
  "keywords": [
6
6
  "react",
@@ -1,6 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
 
3
- import { ErrorAlert } from './ErrorAlert';
3
+ import { ErrorAlert, formatFileTooLarge } from './ErrorAlert';
4
4
 
5
5
  describe('ErrorAlert', () => {
6
6
  test('should render error message', () => {
@@ -9,3 +9,17 @@ describe('ErrorAlert', () => {
9
9
  expect(screen.getByText('Error: file')).toBeDefined();
10
10
  });
11
11
  });
12
+
13
+ describe('formatFileTooLarge', () => {
14
+ test('should format file too large error message', () => {
15
+ const formattedError = formatFileTooLarge('File is larger than 10485760 bytes');
16
+
17
+ expect(formattedError).toEqual('File is larger than 10 MB');
18
+ });
19
+
20
+ test('should return original error message if it does not match', () => {
21
+ const formattedError = formatFileTooLarge('File is larger than one hundred bytes');
22
+
23
+ expect(formattedError).toEqual('File is larger than one hundred bytes');
24
+ });
25
+ });
@@ -1,6 +1,7 @@
1
1
  import { Alert, AlertTitle } from '@availity/mui-alert';
2
2
  import { List, ListItem } from '@availity/mui-list';
3
3
  import type { FileRejection } from 'react-dropzone';
4
+ import { formatBytes } from './util';
4
5
 
5
6
  const codes: Record<string, string> = {
6
7
  'file-too-large': 'File exceeds maximum size',
@@ -27,43 +28,59 @@ export type ErrorAlertProps = {
27
28
  onClose: () => void;
28
29
  };
29
30
 
31
+ const FILE_SIZE_REGEX = /\b(\d+)\b/;
32
+
33
+ export const formatFileTooLarge = (message: string): string => {
34
+ const fileSize = message.match(FILE_SIZE_REGEX);
35
+
36
+ if (!fileSize) return message;
37
+
38
+ const formattedSize = formatBytes(Number(fileSize[0]));
39
+
40
+ return message.replace(` bytes`, '').replace(FILE_SIZE_REGEX, formattedSize);
41
+ };
42
+
30
43
  export const ErrorAlert = ({ errors, fileName, id, onClose }: ErrorAlertProps) => {
31
44
  if (errors.length === 0) return null;
32
45
 
33
46
  return (
34
47
  <Alert severity="error" onClose={onClose}>
35
- {errors.length > 1 ?
48
+ {errors.length > 1 ? (
36
49
  <>
37
50
  <AlertTitle>
38
51
  There were {errors.length} error(s) found when uploading {fileName}
39
52
  </AlertTitle>
40
- <List sx={{
41
- listStyleType: 'disc',
42
- listStylePosition: 'inside',
43
- marginLeft: 1,
44
- '.MuiListItem-root': {
45
- display: 'list-item'
46
- }
47
- }} disablePadding>
53
+ <List
54
+ sx={{
55
+ listStyleType: 'disc',
56
+ listStylePosition: 'inside',
57
+ marginLeft: 1,
58
+ '.MuiListItem-root': {
59
+ display: 'list-item',
60
+ },
61
+ }}
62
+ disablePadding
63
+ >
48
64
  <>
49
65
  {errors.map((error) => {
66
+ const message = error.code === 'file-too-large' ? formatFileTooLarge(error.message) : error.message;
50
67
  return (
51
68
  <ListItem disableGutters disablePadding divider={false} key={`${id}-${error.code}`}>
52
- {error.message}
69
+ {message}
53
70
  </ListItem>
54
71
  );
55
72
  })}
56
73
  </>
57
74
  </List>
58
75
  </>
59
- :
76
+ ) : (
60
77
  <>
61
78
  <AlertTitle>
62
79
  {codes[errors[0].code] || 'Error'}: {fileName}
63
80
  </AlertTitle>
64
- {errors[0].message}
81
+ {errors[0].code === 'file-too-large' ? formatFileTooLarge(errors[0].message) : errors[0].message}
65
82
  </>
66
- }
83
+ )}
67
84
  </Alert>
68
85
  );
69
86
  };
@@ -42,7 +42,7 @@ const meta: Meta<typeof FileSelector> = {
42
42
  uploadOptions: {
43
43
  retryDelays: [],
44
44
  },
45
- maxFiles: 5,
45
+ maxFiles: 2,
46
46
  maxSize: 1 * 1024 * 1024, // 1MB
47
47
  enableDropArea: true,
48
48
  isCloud: true,
@@ -100,7 +100,7 @@ export const _FileSelector: StoryObj<typeof FileSelector> = {
100
100
  </DismissableAlert>
101
101
  </FileSelector>
102
102
  {files.length > 0 && (
103
- <Grid size={{xs: 12}} justifyContent="end" display="flex" paddingTop={2.5}>
103
+ <Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
104
104
  <Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
105
105
  Submit
106
106
  </Button>
@@ -140,9 +140,9 @@ export const _ButtonOnly: StoryObj<typeof FileSelector> = {
140
140
  <Paper sx={{ padding: '2rem' }}>
141
141
  <FormProvider {...methods}>
142
142
  <form onSubmit={methods.handleSubmit(handleOnSubmit)}>
143
- <FileSelector {...props} enableDropArea={false}/>
143
+ <FileSelector {...props} enableDropArea={false} />
144
144
  {files.length > 0 && (
145
- <Grid size={{xs: 12}} justifyContent="end" display="flex" paddingTop={2.5}>
145
+ <Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
146
146
  <Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
147
147
  Submit
148
148
  </Button>
@@ -182,9 +182,9 @@ export const _Encrypted: StoryObj<typeof FileSelector> = {
182
182
  <Paper sx={{ padding: '2rem' }}>
183
183
  <FormProvider {...methods}>
184
184
  <form onSubmit={methods.handleSubmit(handleOnSubmit)}>
185
- <FileSelector {...props} bucketId="enc"/>
185
+ <FileSelector {...props} bucketId="enc" />
186
186
  {files.length > 0 && (
187
- <Grid size={{xs: 12}} justifyContent="end" display="flex" paddingTop={2.5}>
187
+ <Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
188
188
  <Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
189
189
  Submit
190
190
  </Button>
@@ -196,9 +196,109 @@ export const _Encrypted: StoryObj<typeof FileSelector> = {
196
196
  );
197
197
  },
198
198
  args: {
199
- bucketId: 'enc'
199
+ bucketId: 'enc',
200
200
  },
201
201
  argTypes: {
202
- bucketId: { control: false }
203
- }
202
+ bucketId: { control: false },
203
+ },
204
+ };
205
+
206
+ export const _FileSelectorCustomTypesMessage: StoryObj<typeof FileSelector> = {
207
+ render: (props: FileSelectorProps) => {
208
+ const methods = useForm({
209
+ defaultValues: {
210
+ [props.name]: [] as File[],
211
+ },
212
+ });
213
+
214
+ const client = useQueryClient();
215
+
216
+ const files = methods.watch(props.name);
217
+
218
+ const handleOnSubmit = (values: Record<string, File[]>) => {
219
+ if (values[props.name].length === 0) return;
220
+
221
+ const queries = client.getQueriesData<Upload>(['upload']);
222
+ const uploads = [];
223
+ for (const [, data] of queries) {
224
+ if (data) uploads.push(data);
225
+ }
226
+ };
227
+
228
+ return (
229
+ <Paper sx={{ padding: '2rem' }}>
230
+ <FormProvider {...methods}>
231
+ <form onSubmit={methods.handleSubmit(handleOnSubmit)}>
232
+ <FileSelector {...props}>
233
+ <DismissableAlert severity="warning">
234
+ <AlertTitle>Make an Appeal</AlertTitle>
235
+ This is an example alert. It is not part of the component. `children` you pass to the component will
236
+ show up here.
237
+ </DismissableAlert>
238
+ </FileSelector>
239
+ {files.length > 0 && (
240
+ <Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
241
+ <Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
242
+ Submit
243
+ </Button>
244
+ </Grid>
245
+ )}
246
+ </form>
247
+ </FormProvider>
248
+ </Paper>
249
+ );
250
+ },
251
+ args: {
252
+ customTypesMessage: 'Only cool file types allowed',
253
+ },
254
+ };
255
+
256
+ export const _FileSelectorCustomSizeMessage: StoryObj<typeof FileSelector> = {
257
+ render: (props: FileSelectorProps) => {
258
+ const methods = useForm({
259
+ defaultValues: {
260
+ [props.name]: [] as File[],
261
+ },
262
+ });
263
+
264
+ const client = useQueryClient();
265
+
266
+ const files = methods.watch(props.name);
267
+
268
+ const handleOnSubmit = (values: Record<string, File[]>) => {
269
+ if (values[props.name].length === 0) return;
270
+
271
+ const queries = client.getQueriesData<Upload>(['upload']);
272
+ const uploads = [];
273
+ for (const [, data] of queries) {
274
+ if (data) uploads.push(data);
275
+ }
276
+ };
277
+
278
+ return (
279
+ <Paper sx={{ padding: '2rem' }}>
280
+ <FormProvider {...methods}>
281
+ <form onSubmit={methods.handleSubmit(handleOnSubmit)}>
282
+ <FileSelector {...props}>
283
+ <DismissableAlert severity="warning">
284
+ <AlertTitle>Make an Appeal</AlertTitle>
285
+ This is an example alert. It is not part of the component. `children` you pass to the component will
286
+ show up here.
287
+ </DismissableAlert>
288
+ </FileSelector>
289
+ {files.length > 0 && (
290
+ <Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
291
+ <Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
292
+ Submit
293
+ </Button>
294
+ </Grid>
295
+ )}
296
+ </form>
297
+ </FormProvider>
298
+ </Paper>
299
+ );
300
+ },
301
+ args: {
302
+ customSizeMessage: <div>Only huge files allowed</div>,
303
+ },
204
304
  };
@@ -1,16 +1,16 @@
1
1
  import { useState } from 'react';
2
- import type { ChangeEvent, ElementType, ReactNode } from 'react';
2
+ import type { ChangeEvent, ReactNode } from 'react';
3
3
  import { useFormContext } from 'react-hook-form';
4
4
  import type { DropEvent, FileError, FileRejection } from 'react-dropzone/typings/react-dropzone';
5
5
  import { useQueryClient } from '@tanstack/react-query';
6
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
+ import { Alert, AlertTitle } from '@availity/mui-alert';
9
10
 
10
11
  import { Dropzone } from './Dropzone';
11
12
  import { ErrorAlert } from './ErrorAlert';
12
13
  import { FileList } from './FileList';
13
- import type { FileListProps } from './FileList';
14
14
  import { FileTypesMessage } from './FileTypesMessage';
15
15
  import { HeaderMessage } from './HeaderMessage';
16
16
  import type { Options, UploadQueryOptions } from './useUploadCore';
@@ -55,6 +55,14 @@ export type FileSelectorProps = {
55
55
  * @default false
56
56
  */
57
57
  disabled?: boolean;
58
+ /**
59
+ * Overrides the standard file size message
60
+ */
61
+ customSizeMessage?: React.ReactNode;
62
+ /**
63
+ * Overrides the standard file types message
64
+ */
65
+ customTypesMessage?: React.ReactNode;
58
66
  /**
59
67
  * Whether to enable the dropzone area
60
68
  */
@@ -64,7 +72,7 @@ export type FileSelectorProps = {
64
72
  */
65
73
  endpoint?: string;
66
74
  /**
67
- * Componet to render the File information. This should return a `ListItem`
75
+ * Component to render the File information. This should return a `ListItem`
68
76
  */
69
77
  customFileRow?: React.ElementType<{
70
78
  upload?: Upload;
@@ -136,6 +144,8 @@ export const FileSelector = ({
136
144
  bucketId,
137
145
  clientId,
138
146
  children,
147
+ customSizeMessage,
148
+ customTypesMessage,
139
149
  customerId,
140
150
  customFileRow,
141
151
  disabled = false,
@@ -206,6 +216,18 @@ export const FileSelector = ({
206
216
  setFileRejections(rejections);
207
217
  };
208
218
 
219
+ const TOO_MANY_FILES_CODE = 'too-many-files';
220
+
221
+ // Extract too-many-files rejections
222
+ const tooManyFilesRejections = fileRejections.filter((rejection) =>
223
+ rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
224
+ );
225
+
226
+ // Extract other rejections
227
+ const otherRejections = fileRejections.filter(
228
+ (rejection) => !rejection.errors.some((error) => error.code === TOO_MANY_FILES_CODE)
229
+ );
230
+
209
231
  return (
210
232
  <>
211
233
  {enableDropArea ? (
@@ -225,14 +247,25 @@ export const FileSelector = ({
225
247
  setTotalSize={setTotalSize}
226
248
  validator={validator}
227
249
  />
228
- <FileTypesMessage allowedFileTypes={allowedFileTypes} maxFileSize={maxSize} variant="caption" />
250
+ <FileTypesMessage
251
+ allowedFileTypes={allowedFileTypes}
252
+ maxFileSize={maxSize}
253
+ customSizeMessage={customSizeMessage}
254
+ customTypesMessage={customTypesMessage}
255
+ variant="caption"
256
+ />
229
257
  {children}
230
258
  </>
231
259
  ) : (
232
260
  <Grid container rowSpacing={3} flexDirection="column">
233
261
  <Grid>
234
262
  <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
235
- <FileTypesMessage allowedFileTypes={allowedFileTypes} variant="body2" />
263
+ <FileTypesMessage
264
+ allowedFileTypes={allowedFileTypes}
265
+ customSizeMessage={customSizeMessage}
266
+ customTypesMessage={customTypesMessage}
267
+ variant="body2"
268
+ />
236
269
  </Grid>
237
270
  {children ? <Grid>{children}</Grid> : null}
238
271
  <Grid>
@@ -253,17 +286,26 @@ export const FileSelector = ({
253
286
  </Grid>
254
287
  </Grid>
255
288
  )}
256
- {fileRejections.length > 0
257
- ? fileRejections.map((rejection) => (
258
- <ErrorAlert
259
- key={rejection.id}
260
- errors={rejection.errors}
261
- fileName={rejection.file.name}
262
- id={rejection.id}
263
- onClose={() => handleRemoveRejection(rejection.id)}
264
- />
265
- ))
266
- : null}
289
+ {tooManyFilesRejections.length > 0 && (
290
+ <Alert
291
+ severity="error"
292
+ onClose={() => tooManyFilesRejections.forEach((rejection) => handleRemoveRejection(rejection.id))}
293
+ >
294
+ <AlertTitle>Items not allowed.</AlertTitle>
295
+ Too many files are selected for upload, maximum {maxFiles} allowed.
296
+ </Alert>
297
+ )}
298
+
299
+ {otherRejections.length > 0 &&
300
+ otherRejections.map((rejection) => (
301
+ <ErrorAlert
302
+ key={rejection.id}
303
+ errors={rejection.errors}
304
+ fileName={rejection.file.name}
305
+ id={rejection.id}
306
+ onClose={() => handleRemoveRejection(rejection.id)}
307
+ />
308
+ ))}
267
309
  <FileList
268
310
  files={files || []}
269
311
  options={options}
@@ -14,4 +14,16 @@ describe('FileTypesMessage', () => {
14
14
 
15
15
  expect(screen.getByText(/Maximum file size is/)).toBeTruthy();
16
16
  });
17
+
18
+ test('custom size message', () => {
19
+ render(<FileTypesMessage customSizeMessage="Only small files" variant="body2" />);
20
+
21
+ expect(screen.getByText(/Only small files/)).toBeTruthy();
22
+ });
23
+
24
+ test('custom file types message', () => {
25
+ render(<FileTypesMessage customTypesMessage="Only cool types allowed" variant="body2" />);
26
+
27
+ expect(screen.getByText(/Only cool types allowed/)).toBeTruthy();
28
+ });
17
29
  });
@@ -7,6 +7,14 @@ export type FileTypesMessageProps = {
7
7
  * Allowed file type extensions. Each extension should be prefixed with a ".". eg: .txt, .pdf, .png
8
8
  */
9
9
  allowedFileTypes?: `.${string}`[];
10
+ /**
11
+ * Overrides the standard file size message
12
+ */
13
+ customSizeMessage?: React.ReactNode;
14
+ /**
15
+ * Overrides the standard file types message
16
+ */
17
+ customTypesMessage?: React.ReactNode;
10
18
  /**
11
19
  * Maximum size per file in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
12
20
  */
@@ -16,15 +24,19 @@ export type FileTypesMessageProps = {
16
24
 
17
25
  export const FileTypesMessage = ({
18
26
  allowedFileTypes = [],
27
+ customSizeMessage,
28
+ customTypesMessage,
19
29
  maxFileSize,
20
30
  variant = 'caption',
21
31
  }: FileTypesMessageProps) => {
22
- const fileSizeMsg = typeof maxFileSize === 'number' ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null;
32
+ const fileSizeMsg =
33
+ customSizeMessage ||
34
+ (typeof maxFileSize === 'number' ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null);
23
35
  const fileTypesMsg =
24
- allowedFileTypes.length > 0
36
+ customTypesMessage ||
37
+ (allowedFileTypes.length > 0
25
38
  ? `Supported file types include: ${allowedFileTypes.join(', ')}`
26
- : 'All file types allowed.';
27
-
39
+ : 'All file types allowed.');
28
40
  return (
29
41
  <Typography variant={variant}>
30
42
  {fileSizeMsg}