@availity/mui-file-selector 0.1.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 +26 -0
- package/README.md +61 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +65 -0
- package/dist/index.mjs +38 -0
- package/introduction.stories.mdx +7 -0
- package/jest.config.js +7 -0
- package/package.json +65 -0
- package/project.json +41 -0
- package/src/index.ts +1 -0
- package/src/lib/Dropzone.test.tsx +28 -0
- package/src/lib/Dropzone.tsx +185 -0
- package/src/lib/FileList.test.tsx +16 -0
- package/src/lib/FileList.tsx +70 -0
- package/src/lib/FilePickerBtn.test.tsx +24 -0
- package/src/lib/FilePickerBtn.tsx +74 -0
- package/src/lib/FilePickerInput.md +82 -0
- package/src/lib/FileRow.md +70 -0
- package/src/lib/FileSelector.stories.tsx +49 -0
- package/src/lib/FileSelector.test.tsx +17 -0
- package/src/lib/FileSelector.tsx +148 -0
- package/src/lib/FileTypesMessage.test.tsx +11 -0
- package/src/lib/FileTypesMessage.tsx +23 -0
- package/src/lib/HeaderMessage.test.tsx +11 -0
- package/src/lib/HeaderMessage.tsx +16 -0
- package/src/lib/UploadProgressBar.test.tsx +23 -0
- package/src/lib/UploadProgressBar.tsx +93 -0
- package/src/lib/useFileDelivery.tsx +75 -0
- package/src/lib/useUploadCore.tsx +23 -0
- package/src/lib/util.ts +42 -0
- package/tsconfig.json +5 -0
- package/tsconfig.spec.json +10 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
import { FilePickerBtn } from './FilePickerBtn';
|
|
6
|
+
|
|
7
|
+
const TestForm = () => {
|
|
8
|
+
const methods = useForm();
|
|
9
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<FormProvider {...methods}>
|
|
13
|
+
<FilePickerBtn name="test" inputProps={{ ref }} />
|
|
14
|
+
</FormProvider>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('FilePickerBtn', () => {
|
|
19
|
+
test('should render successfully', () => {
|
|
20
|
+
render(<TestForm />);
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText('Browse Files')).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ChangeEvent, MouseEvent, RefObject } from 'react';
|
|
2
|
+
import type { DropzoneInputProps } from 'react-dropzone';
|
|
3
|
+
import { useFormContext } from 'react-hook-form';
|
|
4
|
+
import { Button, ButtonProps } from '@availity/mui-button';
|
|
5
|
+
import { Input } from '@availity/mui-form-utils';
|
|
6
|
+
|
|
7
|
+
type FilePickerBtnProps = {
|
|
8
|
+
name: string;
|
|
9
|
+
maxSize?: number;
|
|
10
|
+
inputId?: string;
|
|
11
|
+
inputProps?: DropzoneInputProps & { ref?: RefObject<HTMLInputElement> };
|
|
12
|
+
} & Omit<ButtonProps, 'onChange'>;
|
|
13
|
+
|
|
14
|
+
export const FilePickerBtn = ({
|
|
15
|
+
name,
|
|
16
|
+
children = 'Browse Files',
|
|
17
|
+
color,
|
|
18
|
+
inputId,
|
|
19
|
+
inputProps = {},
|
|
20
|
+
maxSize,
|
|
21
|
+
onClick,
|
|
22
|
+
...rest
|
|
23
|
+
}: FilePickerBtnProps) => {
|
|
24
|
+
const { register, setValue } = useFormContext();
|
|
25
|
+
|
|
26
|
+
const { accept, multiple, ref, style, type: inputType, onChange } = inputProps;
|
|
27
|
+
|
|
28
|
+
const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
29
|
+
const { files } = event.target;
|
|
30
|
+
|
|
31
|
+
const value: File[] = [];
|
|
32
|
+
if (files) {
|
|
33
|
+
// FileList is not iterable. Must use for loop for now
|
|
34
|
+
for (let i = 0; i < files.length; i++) {
|
|
35
|
+
if (maxSize) {
|
|
36
|
+
console.log('file is too big:', files[i].size > maxSize);
|
|
37
|
+
}
|
|
38
|
+
value[i] = files[i];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setValue(name, value);
|
|
43
|
+
|
|
44
|
+
// if (onChange) onChange(event);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleOnClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
48
|
+
if (onClick) onClick(event);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const field = register(name);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
<Input
|
|
56
|
+
{...field}
|
|
57
|
+
onChange={handleOnChange}
|
|
58
|
+
value=""
|
|
59
|
+
inputRef={ref}
|
|
60
|
+
type={inputType}
|
|
61
|
+
sx={style}
|
|
62
|
+
inputProps={{
|
|
63
|
+
accept,
|
|
64
|
+
size: maxSize ?? undefined,
|
|
65
|
+
multiple,
|
|
66
|
+
}}
|
|
67
|
+
id={inputId}
|
|
68
|
+
/>
|
|
69
|
+
<Button color={color} {...rest} onClick={handleOnClick} fullWidth={false}>
|
|
70
|
+
{children}
|
|
71
|
+
</Button>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ChangeEvent, RefObject, useState } from 'react';
|
|
2
|
+
import { Input } from '@availity/mui-form-utils';
|
|
3
|
+
|
|
4
|
+
const idCounter = () => {
|
|
5
|
+
let id = 0;
|
|
6
|
+
const increment = () => (id += 1);
|
|
7
|
+
const generateId = () => {
|
|
8
|
+
return `filepicker-${increment()}`;
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
generateId,
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const counter = idCounter();
|
|
16
|
+
|
|
17
|
+
const inputSx = { display: 'none' };
|
|
18
|
+
|
|
19
|
+
export type FilePickerInputProps = {
|
|
20
|
+
/** Identifies the field and matches the validation schema. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** The file types you want to restrict uploading to. eg: ['.jpeg', '.jpg']. */
|
|
23
|
+
allowedFileTypes?: `.${string}`[];
|
|
24
|
+
/** id passed to the input component. It is randomly generated if not passed */
|
|
25
|
+
id?: string;
|
|
26
|
+
/** ref passed to the input component */
|
|
27
|
+
inputRef?: RefObject<HTMLInputElement>;
|
|
28
|
+
/** The maximum file size (in bytes) for a file to be uploaded. */
|
|
29
|
+
maxSize?: number;
|
|
30
|
+
/** Indicates that the user will be allowed to select multiple files when selecting files from the OS prompt. */
|
|
31
|
+
multiple?: boolean;
|
|
32
|
+
/** Callback when the user has selected a file or multiple files. */
|
|
33
|
+
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const FilePickerInput = ({
|
|
37
|
+
allowedFileTypes,
|
|
38
|
+
id,
|
|
39
|
+
inputRef,
|
|
40
|
+
maxSize,
|
|
41
|
+
multiple,
|
|
42
|
+
name,
|
|
43
|
+
onChange,
|
|
44
|
+
}: FilePickerInputProps) => {
|
|
45
|
+
const [stateId] = useState(counter.generateId());
|
|
46
|
+
|
|
47
|
+
const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
48
|
+
const { files } = event.target;
|
|
49
|
+
|
|
50
|
+
// TODO: get size of file and compare to maxSize
|
|
51
|
+
|
|
52
|
+
const value: File[] = [];
|
|
53
|
+
if (files) {
|
|
54
|
+
// FileList is not iterable. Must use for loop for now
|
|
55
|
+
for (let i = 0; i < files.length; i++) {
|
|
56
|
+
if (maxSize) {
|
|
57
|
+
console.log('file is too big:', files[i].size > maxSize);
|
|
58
|
+
}
|
|
59
|
+
value[i] = files[i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (onChange) onChange(event);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const inputId = id || stateId;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Input
|
|
70
|
+
inputRef={inputRef}
|
|
71
|
+
sx={inputSx}
|
|
72
|
+
value=""
|
|
73
|
+
type="file"
|
|
74
|
+
inputProps={{
|
|
75
|
+
accept: Array.isArray(allowedFileTypes) && allowedFileTypes.length > 0 ? allowedFileTypes.join(',') : undefined,
|
|
76
|
+
multiple,
|
|
77
|
+
}}
|
|
78
|
+
id={inputId}
|
|
79
|
+
onChange={handleOnChange}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Button } from 'reactstrap';
|
|
2
|
+
import Icon from '@availity/icon';
|
|
3
|
+
import type Upload from '@availity/upload-core';
|
|
4
|
+
|
|
5
|
+
import { UploadProgressBar } from './UploadProgressBar';
|
|
6
|
+
|
|
7
|
+
const FILE_EXT_ICONS = {
|
|
8
|
+
png: 'file-image',
|
|
9
|
+
jpg: 'file-image',
|
|
10
|
+
jpeg: 'file-image',
|
|
11
|
+
gif: 'file-image',
|
|
12
|
+
ppt: 'file-powerpoint',
|
|
13
|
+
pptx: 'file-powerpoint',
|
|
14
|
+
xls: 'file-excel',
|
|
15
|
+
xlsx: 'file-excel',
|
|
16
|
+
doc: 'file-word',
|
|
17
|
+
docx: 'file-word',
|
|
18
|
+
txt: 'doc-alt',
|
|
19
|
+
text: 'doc-alt',
|
|
20
|
+
zip: 'file-archive',
|
|
21
|
+
'7zip': 'file-archive',
|
|
22
|
+
xml: 'file-code',
|
|
23
|
+
html: 'file-code',
|
|
24
|
+
pdf: 'file-pdf',
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
type FileExtensionKey = keyof typeof FILE_EXT_ICONS;
|
|
28
|
+
|
|
29
|
+
const isValidKey = (key: string): key is FileExtensionKey => {
|
|
30
|
+
return key ? key in FILE_EXT_ICONS : false;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type FileRowProps = {
|
|
34
|
+
upload: Upload;
|
|
35
|
+
onRemoveFile: (id: string) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const FileRow = ({ upload, onRemoveFile }: FileRowProps) => {
|
|
39
|
+
const remove = () => {
|
|
40
|
+
onRemoveFile(upload.id);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const ext = upload.file.name.split('.').pop()?.toLowerCase() || '';
|
|
44
|
+
const icon = isValidKey(ext) ? FILE_EXT_ICONS[ext] : 'doc';
|
|
45
|
+
const extLabel = ext.toUpperCase();
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<tr>
|
|
49
|
+
<td className="align-middle" style={{ width: '10%' }}>
|
|
50
|
+
<Icon name={icon} title={`${extLabel} File Icon`}>
|
|
51
|
+
<span className="sr-only">{extLabel} File Icon</span>
|
|
52
|
+
</Icon>{' '}
|
|
53
|
+
</td>
|
|
54
|
+
<td className="align-middle" style={{ width: '35%' }}>
|
|
55
|
+
<div className="text-truncate" title={upload.file.name}>
|
|
56
|
+
{upload.file.name}
|
|
57
|
+
</div>
|
|
58
|
+
</td>
|
|
59
|
+
<td className="align-middle" style={{ width: '45%' }}>
|
|
60
|
+
<UploadProgressBar upload={upload} aria-label={`${upload.file.name} upload`} />
|
|
61
|
+
</td>
|
|
62
|
+
<td className="align-middle" style={{ width: '10%' }}>
|
|
63
|
+
<Button data-testid="remove-file-btn" color="link" className="text-danger px-0" onClick={remove}>
|
|
64
|
+
<Icon name="trash-empty" />
|
|
65
|
+
<span className="sr-only">Remove {upload.file.name}</span>
|
|
66
|
+
</Button>
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Each exported component in the package should have its own stories file
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { Paper } from '@availity/mui-paper';
|
|
5
|
+
|
|
6
|
+
import { FileSelector, FileSelectorProps } from './FileSelector';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof FileSelector> = {
|
|
9
|
+
title: 'Components/File Selector/File Selector',
|
|
10
|
+
component: FileSelector,
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
decorators: [
|
|
13
|
+
(Story: () => JSX.Element) => (
|
|
14
|
+
<QueryClientProvider
|
|
15
|
+
client={
|
|
16
|
+
new QueryClient({
|
|
17
|
+
defaultOptions: {
|
|
18
|
+
queries: {
|
|
19
|
+
refetchOnWindowFocus: false,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
<Story />
|
|
26
|
+
</QueryClientProvider>
|
|
27
|
+
),
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
|
|
33
|
+
export const _FileSelector: StoryObj<typeof FileSelector> = {
|
|
34
|
+
render: (props: FileSelectorProps) => (
|
|
35
|
+
<Paper sx={{ padding: '2rem' }}>
|
|
36
|
+
<FileSelector {...props} />
|
|
37
|
+
</Paper>
|
|
38
|
+
),
|
|
39
|
+
args: {
|
|
40
|
+
name: 'file-selector',
|
|
41
|
+
allowedFileTypes: ['.txt'],
|
|
42
|
+
clientId: '123',
|
|
43
|
+
customerId: '456',
|
|
44
|
+
bucketId: '789',
|
|
45
|
+
maxSize: 1 * 1000 * 1000, // 1MB
|
|
46
|
+
isCloud: true,
|
|
47
|
+
multiple: true,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
import { FileSelector } from './FileSelector';
|
|
5
|
+
|
|
6
|
+
describe('FileSelector', () => {
|
|
7
|
+
test('should render successfully', () => {
|
|
8
|
+
const client = new QueryClient();
|
|
9
|
+
const { getByText } = render(
|
|
10
|
+
<QueryClientProvider client={client}>
|
|
11
|
+
<FileSelector name="test" bucketId="test" customerId="123" clientId="test" maxSize={1000} />
|
|
12
|
+
</QueryClientProvider>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(getByText('Upload file')).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ReactNode, useState } from 'react';
|
|
2
|
+
import { useForm, FormProvider } from 'react-hook-form';
|
|
3
|
+
import type { FileRejection } from 'react-dropzone/typings/react-dropzone';
|
|
4
|
+
import Upload, { Options } from '@availity/upload-core';
|
|
5
|
+
|
|
6
|
+
import { Dropzone } from './Dropzone';
|
|
7
|
+
import { FileList } from './FileList';
|
|
8
|
+
import { FileTypesMessage } from './FileTypesMessage';
|
|
9
|
+
import { useUploadCore } from './useUploadCore';
|
|
10
|
+
import { Typography } from '@availity/mui-typography';
|
|
11
|
+
|
|
12
|
+
const CLOUD_URL = '/cloud/web/appl/vault/upload/v1/resumable';
|
|
13
|
+
|
|
14
|
+
export type FileSelectorProps = {
|
|
15
|
+
name: string;
|
|
16
|
+
bucketId: string;
|
|
17
|
+
customerId: string;
|
|
18
|
+
allowedFileNameCharacters?: string;
|
|
19
|
+
allowedFileTypes?: `.${string}`[];
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
clientId: string;
|
|
22
|
+
deliverFileOnSubmit?: boolean;
|
|
23
|
+
deliveryChannel?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
fileDeliveryMetadata?: Record<string, unknown> | ((file: Upload) => Record<string, unknown>);
|
|
27
|
+
getDropRejectionMessages?: (rejections: FileRejection[]) => void;
|
|
28
|
+
isCloud?: boolean;
|
|
29
|
+
label?: ReactNode;
|
|
30
|
+
maxFiles?: number;
|
|
31
|
+
maxSize: number;
|
|
32
|
+
multiple?: boolean;
|
|
33
|
+
onDeliveryError?: (error: unknown) => void;
|
|
34
|
+
onDeliverySuccess?: () => void;
|
|
35
|
+
onSubmit?: (values: Record<string, unknown>) => void;
|
|
36
|
+
onSuccess?: (() => void)[];
|
|
37
|
+
onError?: ((error: Error) => void)[];
|
|
38
|
+
onFilePreUpload?: (() => boolean)[];
|
|
39
|
+
onUploadRemove?: (uploads: Upload[], removedUploadId: string) => void;
|
|
40
|
+
onFileDelivery?: (upload: Upload) => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const FileSelector = ({
|
|
44
|
+
name,
|
|
45
|
+
allowedFileNameCharacters,
|
|
46
|
+
allowedFileTypes = [],
|
|
47
|
+
bucketId,
|
|
48
|
+
clientId,
|
|
49
|
+
children,
|
|
50
|
+
customerId,
|
|
51
|
+
deliverFileOnSubmit = false,
|
|
52
|
+
deliveryChannel,
|
|
53
|
+
disabled = false,
|
|
54
|
+
endpoint,
|
|
55
|
+
fileDeliveryMetadata,
|
|
56
|
+
getDropRejectionMessages,
|
|
57
|
+
isCloud,
|
|
58
|
+
label = 'Upload file',
|
|
59
|
+
maxFiles = 1,
|
|
60
|
+
maxSize,
|
|
61
|
+
multiple = true,
|
|
62
|
+
// onDeliveryError,
|
|
63
|
+
// onDeliverySuccess,
|
|
64
|
+
onSubmit,
|
|
65
|
+
onSuccess,
|
|
66
|
+
onError,
|
|
67
|
+
onFilePreUpload = [],
|
|
68
|
+
onUploadRemove,
|
|
69
|
+
onFileDelivery,
|
|
70
|
+
}: FileSelectorProps) => {
|
|
71
|
+
// const classes = classNames(
|
|
72
|
+
// className,
|
|
73
|
+
// metadata.touched ? 'is-touched' : 'is-untouched',
|
|
74
|
+
// metadata.touched && metadata.error && 'is-invalid'
|
|
75
|
+
// );
|
|
76
|
+
const [totalSize, setTotalSize] = useState(0);
|
|
77
|
+
|
|
78
|
+
const methods = useForm();
|
|
79
|
+
|
|
80
|
+
const options: Options = {
|
|
81
|
+
bucketId,
|
|
82
|
+
customerId,
|
|
83
|
+
clientId,
|
|
84
|
+
fileTypes: allowedFileTypes,
|
|
85
|
+
maxSize,
|
|
86
|
+
allowedFileNameCharacters,
|
|
87
|
+
onError,
|
|
88
|
+
onSuccess,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (onFilePreUpload) options.onPreStart = onFilePreUpload;
|
|
92
|
+
if (endpoint) options.endpoint = endpoint;
|
|
93
|
+
if (isCloud) options.endpoint = CLOUD_URL;
|
|
94
|
+
|
|
95
|
+
const { data: uploads = [] } = useUploadCore(methods.watch(name) || [], options);
|
|
96
|
+
|
|
97
|
+
const handleOnRemoveFile = (uploadId: string) => {
|
|
98
|
+
const newFiles = uploads.filter((upload) => upload.id !== uploadId);
|
|
99
|
+
|
|
100
|
+
if (newFiles.length !== uploads.length) {
|
|
101
|
+
const removedFile = uploads.find((upload) => upload.id === uploadId);
|
|
102
|
+
|
|
103
|
+
methods.setValue(name, newFiles);
|
|
104
|
+
|
|
105
|
+
if (!removedFile?.error && !removedFile?.errorMessage && removedFile?.file.size)
|
|
106
|
+
setTotalSize(totalSize - removedFile.file.size);
|
|
107
|
+
if (onUploadRemove) onUploadRemove(newFiles, uploadId);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleOSubmit = (values: Record<string, unknown>) => {
|
|
112
|
+
if (onSubmit) onSubmit(values);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<FormProvider {...methods}>
|
|
117
|
+
<form onSubmit={methods.handleSubmit(handleOSubmit)}>
|
|
118
|
+
<>
|
|
119
|
+
<Typography>{label}</Typography>
|
|
120
|
+
<Dropzone
|
|
121
|
+
name={name}
|
|
122
|
+
allowedFileNameCharacters={allowedFileNameCharacters}
|
|
123
|
+
allowedFileTypes={allowedFileTypes}
|
|
124
|
+
bucketId={bucketId}
|
|
125
|
+
clientId={clientId}
|
|
126
|
+
customerId={customerId}
|
|
127
|
+
deliverFileOnSubmit={deliverFileOnSubmit}
|
|
128
|
+
deliveryChannel={deliveryChannel}
|
|
129
|
+
disabled={disabled}
|
|
130
|
+
endpoint={endpoint}
|
|
131
|
+
fileDeliveryMetadata={fileDeliveryMetadata}
|
|
132
|
+
getDropRejectionMessages={getDropRejectionMessages}
|
|
133
|
+
isCloud={isCloud}
|
|
134
|
+
maxSize={maxSize}
|
|
135
|
+
multiple={multiple}
|
|
136
|
+
// onDeliveryError={onDeliveryError}
|
|
137
|
+
// onDeliverySuccess={onDeliverySuccess}
|
|
138
|
+
onFilePreUpload={onFilePreUpload}
|
|
139
|
+
onFileDelivery={onFileDelivery}
|
|
140
|
+
/>
|
|
141
|
+
<FileTypesMessage allowedFileTypes={allowedFileTypes} maxFileSize={maxSize} />
|
|
142
|
+
</>
|
|
143
|
+
{children}
|
|
144
|
+
<FileList uploads={uploads} onRemoveFile={handleOnRemoveFile} />
|
|
145
|
+
</form>
|
|
146
|
+
</FormProvider>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { FileTypesMessage } from './FileTypesMessage';
|
|
4
|
+
|
|
5
|
+
describe('FileTypesMessage', () => {
|
|
6
|
+
test('should render successfully', () => {
|
|
7
|
+
render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} />);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByText(/All file types allowed/)).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Typography } from '@availity/mui-typography';
|
|
2
|
+
|
|
3
|
+
import { formatBytes } from './util';
|
|
4
|
+
|
|
5
|
+
type FileTypesMessageProps = {
|
|
6
|
+
allowedFileTypes: `.${string}`[];
|
|
7
|
+
maxFileSize: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const FileTypesMessage = ({ allowedFileTypes, maxFileSize }: FileTypesMessageProps) => {
|
|
11
|
+
const fileSizeMsg = typeof maxFileSize === 'number' ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null;
|
|
12
|
+
const fileTypesMsg =
|
|
13
|
+
allowedFileTypes.length > 0
|
|
14
|
+
? `Supported file types include: ${allowedFileTypes.join(', ')}.`
|
|
15
|
+
: 'All file types allowed.';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Typography variant="caption">
|
|
19
|
+
{fileSizeMsg}
|
|
20
|
+
{fileTypesMsg}
|
|
21
|
+
</Typography>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { HeaderMessage } from './HeaderMessage';
|
|
4
|
+
|
|
5
|
+
describe('HeaderMessage', () => {
|
|
6
|
+
test('should render successfully', () => {
|
|
7
|
+
render(<HeaderMessage maxFiles={5} maxSize={1000} />);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByText(/Attach/)).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Typography } from '@availity/mui-typography';
|
|
2
|
+
|
|
3
|
+
import { formatBytes } from './util';
|
|
4
|
+
|
|
5
|
+
export type HeaderMessageProps = {
|
|
6
|
+
maxFiles: number;
|
|
7
|
+
maxSize: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const HeaderMessage = ({ maxFiles, maxSize }: HeaderMessageProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<Typography variant="h6">
|
|
13
|
+
Attach up to {maxFiles} file(s), with a maximum of {formatBytes(maxSize)}
|
|
14
|
+
</Typography>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import Upload from '@availity/upload-core';
|
|
3
|
+
|
|
4
|
+
import { UploadProgressBar } from './UploadProgressBar';
|
|
5
|
+
|
|
6
|
+
describe('UploadProgressBar', () => {
|
|
7
|
+
test('should render successfully', () => {
|
|
8
|
+
const mockUpload: unknown = {
|
|
9
|
+
onProgress: [],
|
|
10
|
+
onError: [],
|
|
11
|
+
onSuccess: [],
|
|
12
|
+
errorMessage: '',
|
|
13
|
+
file: {
|
|
14
|
+
name: 'test',
|
|
15
|
+
},
|
|
16
|
+
percentage: 50,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
render(<UploadProgressBar upload={mockUpload as Upload} />);
|
|
20
|
+
|
|
21
|
+
expect(screen.getByText('50%')).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
// import { ChangeEvent, FormEventHandler, useState } from 'react';
|
|
3
|
+
import { LinearProgress } from '@availity/mui-progress';
|
|
4
|
+
import type Upload from '@availity/upload-core';
|
|
5
|
+
import { Typography } from '@availity/mui-typography';
|
|
6
|
+
import { WarningTriangleIcon } from '@availity/mui-icon';
|
|
7
|
+
|
|
8
|
+
export type UploadProgressBarProps = {
|
|
9
|
+
/** The upload instance returned by creating a new Upload via @availity/upload-core. */
|
|
10
|
+
upload: Upload;
|
|
11
|
+
/** Callback function to hook into the onProgress within the Upload instance provided in the upload prop. */
|
|
12
|
+
onProgress?: (upload: Upload) => void;
|
|
13
|
+
/** Callback function to hook into the onSuccess within the Upload instance provided in the upload prop. */
|
|
14
|
+
onSuccess?: (upload: Upload) => void;
|
|
15
|
+
/** Callback function to hook into the onError within the Upload instance provided in the upload prop. */
|
|
16
|
+
onError?: (upload: Upload) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const UploadProgressBar = ({ upload, onProgress, onError, onSuccess }: UploadProgressBarProps) => {
|
|
20
|
+
const [statePercentage, setStatePercentage] = useState(upload.percentage || 0);
|
|
21
|
+
const [error, setError] = useState(false);
|
|
22
|
+
// const [password, setPassword] = useState('');
|
|
23
|
+
// const [modalOpen, setModalOpen] = useState(false);
|
|
24
|
+
|
|
25
|
+
const handleOnProgress = () => {
|
|
26
|
+
setStatePercentage(upload.percentage);
|
|
27
|
+
setError(false);
|
|
28
|
+
|
|
29
|
+
if (onProgress) onProgress(upload);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleOnError = () => {
|
|
33
|
+
setError(true);
|
|
34
|
+
|
|
35
|
+
if (onError) onError(upload);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleOnSuccess = () => {
|
|
39
|
+
setStatePercentage(100);
|
|
40
|
+
setError(false);
|
|
41
|
+
|
|
42
|
+
if (onSuccess) onSuccess(upload);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// const toggleModal = () => {
|
|
46
|
+
// setModalOpen((prev) => !prev);
|
|
47
|
+
// setPassword('');
|
|
48
|
+
// };
|
|
49
|
+
|
|
50
|
+
// const verifyPassword: FormEventHandler<HTMLFormElement> = (event) => {
|
|
51
|
+
// event.preventDefault();
|
|
52
|
+
// event.stopPropagation();
|
|
53
|
+
// upload.sendPassword(password);
|
|
54
|
+
// toggleModal();
|
|
55
|
+
// };
|
|
56
|
+
|
|
57
|
+
// const handlePasswordChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
58
|
+
// setPassword(event.target.value);
|
|
59
|
+
// };
|
|
60
|
+
|
|
61
|
+
upload.onProgress.push(handleOnProgress);
|
|
62
|
+
upload.onSuccess.push(handleOnSuccess);
|
|
63
|
+
upload.onError.push(handleOnError);
|
|
64
|
+
|
|
65
|
+
return upload.errorMessage ? (
|
|
66
|
+
<>
|
|
67
|
+
<Typography color="text.error">
|
|
68
|
+
<WarningTriangleIcon /> {upload.errorMessage}
|
|
69
|
+
</Typography>
|
|
70
|
+
{/* {upload.status === 'encrypted' && (
|
|
71
|
+
<div className="pwRequired">
|
|
72
|
+
<Button color="primary" onClick={toggleModal}>
|
|
73
|
+
Enter password
|
|
74
|
+
</Button>
|
|
75
|
+
<Modal isOpen={modalOpen} toggle={toggleModal}>
|
|
76
|
+
<form onSubmit={verifyPassword}>
|
|
77
|
+
<ModalHeader toggle={toggleModal}>Enter Password</ModalHeader>
|
|
78
|
+
<ModalBody>
|
|
79
|
+
<Label for="upload-password">Password</Label>
|
|
80
|
+
<Input id="upload-password" onChange={handlePasswordChange} type="password" placeholder="password" />
|
|
81
|
+
</ModalBody>
|
|
82
|
+
<ModalFooter>
|
|
83
|
+
<Button color="primary">Ok</Button>
|
|
84
|
+
</ModalFooter>
|
|
85
|
+
</form>
|
|
86
|
+
</Modal>
|
|
87
|
+
</div>
|
|
88
|
+
)} */}
|
|
89
|
+
</>
|
|
90
|
+
) : (
|
|
91
|
+
<LinearProgress value={statePercentage} aria-label={`${upload.file.name}-progress`} />
|
|
92
|
+
);
|
|
93
|
+
};
|