Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/file-selector/src/lib/ErrorAlert.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
48 changes: 33 additions & 15 deletions packages/file-selector/src/lib/ErrorAlert.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
'file-too-large': 'File exceeds maximum size',
Expand All @@ -12,6 +12,8 @@ const codes: Record<string, string> = {
'duplicate-name': 'Duplicate file selected',
};

export type GetErrorMessageFn = (error: FileError) => string | null | undefined;

export type ErrorAlertProps = {
/**
* Array of file rejection errors
Expand All @@ -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;
};
Expand All @@ -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 (
<Alert severity="error" onClose={onClose}>
{errors.length > 1 ? (
{uniqueErrors.length > 1 ? (
<>
<AlertTitle>
There were {errors.length} error(s) found when uploading {fileName}
There were {uniqueErrors.length} error(s) found when uploading {fileName}
</AlertTitle>
<List
sx={{
Expand All @@ -63,23 +84,20 @@ export const ErrorAlert = ({ errors, fileName, id, onClose }: ErrorAlertProps) =
disablePadding
>
<>
{errors.map((error) => {
const message = error.code === 'file-too-large' ? formatFileTooLarge(error.message) : error.message;
return (
<ListItem disableGutters disablePadding divider={false} key={`${id}-${error.code}`}>
{message}
</ListItem>
);
})}
{uniqueErrors.map((error) => (
<ListItem disableGutters disablePadding divider={false} key={`${id}-${error.code}`}>
{formatMessage(error)}
</ListItem>
))}
</>
</List>
</>
) : (
<>
<AlertTitle>
{codes[errors[0].code] || 'Error'}: {fileName}
{codes[uniqueErrors[0].code] || 'Error'}: {fileName}
</AlertTitle>
{errors[0].code === 'file-too-large' ? formatFileTooLarge(errors[0].message) : errors[0].message}
{formatMessage(uniqueErrors[0])}
</>
)}
</Alert>
Expand Down
10 changes: 9 additions & 1 deletion packages/file-selector/src/lib/FileSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = ({
Expand Down Expand Up @@ -177,6 +183,7 @@ export const FileSelector = ({
uploadOptions,
validator,
disableRemove,
getErrorMessage,
}: FileSelectorProps) => {
const [totalSize, setTotalSize] = useState(0);
const [fileRejections, setFileRejections] = useState<(FileRejection & { id: number })[]>([]);
Expand Down Expand Up @@ -323,6 +330,7 @@ export const FileSelector = ({
errors={rejection.errors}
fileName={rejection.file.name}
id={rejection.id}
getErrorMessage={getErrorMessage}
onClose={() => handleRemoveRejection(rejection.id)}
/>
))}
Expand Down
10 changes: 9 additions & 1 deletion packages/file-selector/src/lib/FileSelector2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,6 +18,12 @@ import type { FileSelectorProps } from './FileSelector';

export type FileSelector2Props = Omit<FileSelectorProps, 'onUploadRemove' | 'queryOptions'> & {
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;
};

/**
Expand Down Expand Up @@ -56,6 +62,7 @@ export const FileSelector2 = ({
uploadOptions,
validator,
disableRemove,
getErrorMessage,
}: FileSelector2Props) => {
const [totalSize, setTotalSize] = useState(0);
const [fileRejections, setFileRejections] = useState<(FileRejection & { id: number })[]>([]);
Expand Down Expand Up @@ -200,6 +207,7 @@ export const FileSelector2 = ({
errors={rejection.errors}
fileName={rejection.file.name}
id={rejection.id}
getErrorMessage={getErrorMessage}
onClose={() => handleRemoveRejection(rejection.id)}
/>
))}
Expand Down
4 changes: 4 additions & 0 deletions packages/file-selector/src/lib/UploadProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Loading