From 5ca9a36d9295dd53e14fc76ced9a25e65aadd43f Mon Sep 17 00:00:00 2001 From: amandaDavisAvaility <117665061+amandaDavisAvaility@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:36:20 -0500 Subject: [PATCH] feat(mui-file-selector): add getErrorMessage prop and improve error handling --- .../file-selector/src/lib/ErrorAlert.test.tsx | 1 + packages/file-selector/src/lib/ErrorAlert.tsx | 48 +++++++++++++------ .../file-selector/src/lib/FileSelector.tsx | 10 +++- .../file-selector/src/lib/FileSelector2.tsx | 10 +++- .../src/lib/UploadProgressBar.tsx | 4 ++ 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/file-selector/src/lib/ErrorAlert.test.tsx b/packages/file-selector/src/lib/ErrorAlert.test.tsx index 74ad1ae1b7..85313e8e4b 100644 --- a/packages/file-selector/src/lib/ErrorAlert.test.tsx +++ b/packages/file-selector/src/lib/ErrorAlert.test.tsx @@ -12,6 +12,7 @@ describe('ErrorAlert', () => { describe('formatFileTooLarge', () => { test('should format file too large error message', () => { + // 10 MiB = 10 * 1024 * 1024 = 10485760 bytes const formattedError = formatFileTooLarge('File is larger than 10485760 bytes'); expect(formattedError).toEqual('File is larger than 10 MB'); diff --git a/packages/file-selector/src/lib/ErrorAlert.tsx b/packages/file-selector/src/lib/ErrorAlert.tsx index f551baef1d..ece05c04bb 100644 --- a/packages/file-selector/src/lib/ErrorAlert.tsx +++ b/packages/file-selector/src/lib/ErrorAlert.tsx @@ -1,7 +1,7 @@ import { Alert, AlertTitle } from '@availity/mui-alert'; import { List, ListItem } from '@availity/mui-list'; -import type { FileRejection } from 'react-dropzone'; -import { formatBytes } from './util'; +import type { FileError, FileRejection } from 'react-dropzone'; +import { dedupeErrors, formatBytes } from './util'; const codes: Record = { 'file-too-large': 'File exceeds maximum size', @@ -12,6 +12,8 @@ const codes: Record = { 'duplicate-name': 'Duplicate file selected', }; +export type GetErrorMessageFn = (error: FileError) => string | null | undefined; + export type ErrorAlertProps = { /** * Array of file rejection errors @@ -25,6 +27,10 @@ export type ErrorAlertProps = { * Unique identifier for the error alert */ id: number; + /** + * Custom function to format error messages. Return null/undefined to use default message. + */ + getErrorMessage?: GetErrorMessageFn; onClose: () => void; }; @@ -41,15 +47,30 @@ export const formatFileTooLarge = (message: string): string => { return message.replace(` bytes`, '').replace(FILE_SIZE_REGEX, formattedSize); }; -export const ErrorAlert = ({ errors, fileName, id, onClose }: ErrorAlertProps) => { +export const ErrorAlert = ({ errors, fileName, id, getErrorMessage, onClose }: ErrorAlertProps) => { if (errors.length === 0) return null; + // Dedupe errors by code (e.g., react-dropzone may produce multiple file-invalid-type errors) + const uniqueErrors = dedupeErrors(errors); + + const formatMessage = (error: FileRejection['errors'][0]) => { + // Try custom error message first + const customMessage = getErrorMessage?.(error); + if (customMessage) return customMessage; + + // Fall back to default formatting + if (error.code === 'file-too-large') { + return formatFileTooLarge(error.message); + } + return error.message; + }; + return ( - {errors.length > 1 ? ( + {uniqueErrors.length > 1 ? ( <> - There were {errors.length} error(s) found when uploading {fileName} + There were {uniqueErrors.length} error(s) found when uploading {fileName} <> - {errors.map((error) => { - const message = error.code === 'file-too-large' ? formatFileTooLarge(error.message) : error.message; - return ( - - {message} - - ); - })} + {uniqueErrors.map((error) => ( + + {formatMessage(error)} + + ))} ) : ( <> - {codes[errors[0].code] || 'Error'}: {fileName} + {codes[uniqueErrors[0].code] || 'Error'}: {fileName} - {errors[0].code === 'file-too-large' ? formatFileTooLarge(errors[0].message) : errors[0].message} + {formatMessage(uniqueErrors[0])} )} diff --git a/packages/file-selector/src/lib/FileSelector.tsx b/packages/file-selector/src/lib/FileSelector.tsx index 3d5ceb7dc4..dbaf459ddb 100644 --- a/packages/file-selector/src/lib/FileSelector.tsx +++ b/packages/file-selector/src/lib/FileSelector.tsx @@ -9,7 +9,7 @@ import { Typography } from '@availity/mui-typography'; import { Alert, AlertTitle } from '@availity/mui-alert'; import { Dropzone } from './Dropzone'; -import { ErrorAlert } from './ErrorAlert'; +import { ErrorAlert, GetErrorMessageFn } from './ErrorAlert'; import { FileList } from './FileList'; import { FileTypesMessage } from './FileTypesMessage'; import { HeaderMessage } from './HeaderMessage'; @@ -147,6 +147,12 @@ export type FileSelectorProps = { * @default false */ disableRemove?: boolean; + /** + * Custom function to format error messages. Return undefined to use default message. + * @param error - The file error object with code and message + * @returns Custom error message string, or undefined to use default + */ + getErrorMessage?: GetErrorMessageFn; }; export const FileSelector = ({ @@ -177,6 +183,7 @@ export const FileSelector = ({ uploadOptions, validator, disableRemove, + getErrorMessage, }: FileSelectorProps) => { const [totalSize, setTotalSize] = useState(0); const [fileRejections, setFileRejections] = useState<(FileRejection & { id: number })[]>([]); @@ -323,6 +330,7 @@ export const FileSelector = ({ errors={rejection.errors} fileName={rejection.file.name} id={rejection.id} + getErrorMessage={getErrorMessage} onClose={() => handleRemoveRejection(rejection.id)} /> ))} diff --git a/packages/file-selector/src/lib/FileSelector2.tsx b/packages/file-selector/src/lib/FileSelector2.tsx index a203a4d22c..ab164cd253 100644 --- a/packages/file-selector/src/lib/FileSelector2.tsx +++ b/packages/file-selector/src/lib/FileSelector2.tsx @@ -7,7 +7,7 @@ import { Typography } from '@availity/mui-typography'; import { Alert, AlertTitle } from '@availity/mui-alert'; import { Dropzone2 } from './Dropzone2'; -import { ErrorAlert } from './ErrorAlert'; +import { ErrorAlert, GetErrorMessageFn } from './ErrorAlert'; import { FileList2 } from './FileList2'; import { FileTypesMessage } from './FileTypesMessage'; import { HeaderMessage } from './HeaderMessage'; @@ -18,6 +18,12 @@ import type { FileSelectorProps } from './FileSelector'; export type FileSelector2Props = Omit & { onUploadRemove?: (uploads: Upload[], removedUploadId: string) => void; + /** + * Custom function to format error messages. Return undefined to use default message. + * @param error - The file error object with code and message + * @returns Custom error message string, or undefined to use default + */ + getErrorMessage?: GetErrorMessageFn; }; /** @@ -56,6 +62,7 @@ export const FileSelector2 = ({ uploadOptions, validator, disableRemove, + getErrorMessage, }: FileSelector2Props) => { const [totalSize, setTotalSize] = useState(0); const [fileRejections, setFileRejections] = useState<(FileRejection & { id: number })[]>([]); @@ -200,6 +207,7 @@ export const FileSelector2 = ({ errors={rejection.errors} fileName={rejection.file.name} id={rejection.id} + getErrorMessage={getErrorMessage} onClose={() => handleRemoveRejection(rejection.id)} /> ))} diff --git a/packages/file-selector/src/lib/UploadProgressBar.tsx b/packages/file-selector/src/lib/UploadProgressBar.tsx index 3af6213032..e18f02b8e9 100644 --- a/packages/file-selector/src/lib/UploadProgressBar.tsx +++ b/packages/file-selector/src/lib/UploadProgressBar.tsx @@ -42,6 +42,10 @@ const ERROR_MAPPINGS: ErrorMapping[] = [ pattern: /but has an extension for/i, friendlyMessage: 'File format does not match file extension.', }, + { + pattern: /tus: failed to upload/i, + friendlyMessage: 'Oops! There was an issue trying to upload your attachment.', + }, ]; const friendlyErrorMessage = (errorMessage: string): string => {