@availity/mui-file-selector 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +167 -15
- package/dist/index.d.mts +208 -22
- package/dist/index.d.ts +208 -22
- package/dist/index.js +234 -97
- package/dist/index.mjs +226 -97
- package/package.json +11 -11
- package/src/index.ts +7 -0
- package/src/lib/Dropzone.test.tsx +2 -2
- package/src/lib/Dropzone.tsx +92 -32
- package/src/lib/FileList.test.tsx +25 -1
- package/src/lib/FileList.tsx +66 -28
- package/src/lib/FilePickerBtn.test.tsx +3 -1
- package/src/lib/FilePickerBtn.tsx +23 -21
- package/src/lib/FileSelector.stories.tsx +66 -4
- package/src/lib/FileSelector.test.tsx +11 -1
- package/src/lib/FileSelector.tsx +109 -90
- package/src/lib/FileTypesMessage.test.tsx +2 -2
- package/src/lib/FileTypesMessage.tsx +11 -6
- package/src/lib/HeaderMessage.tsx +1 -1
- package/src/lib/useFileDelivery.tsx +3 -1
- package/src/lib/useUploadCore.tsx +10 -3
- package/tsconfig.spec.json +1 -1
package/src/lib/Dropzone.tsx
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { Dispatch, MouseEvent } from 'react';
|
|
3
|
+
import { styled } from '@mui/material/styles';
|
|
4
|
+
import MuiBox from '@mui/material/Box';
|
|
5
|
+
import { useDropzone } from 'react-dropzone';
|
|
6
|
+
import type { DropEvent, FileError, FileRejection } from 'react-dropzone';
|
|
3
7
|
import { useFormContext } from 'react-hook-form';
|
|
4
8
|
import { Divider } from '@availity/mui-divider';
|
|
5
|
-
import { CloudUploadIcon } from '@availity/mui-icon';
|
|
9
|
+
import { CloudUploadIcon, PlusIcon } from '@availity/mui-icon';
|
|
6
10
|
import { Box, Stack } from '@availity/mui-layout';
|
|
7
11
|
import { Typography } from '@availity/mui-typography';
|
|
8
12
|
|
|
@@ -50,6 +54,10 @@ export type DropzoneProps = {
|
|
|
50
54
|
* Whether the dropzone is disabled
|
|
51
55
|
*/
|
|
52
56
|
disabled?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Whether to enable the dropzone area
|
|
59
|
+
*/
|
|
60
|
+
enableDropArea?: boolean;
|
|
53
61
|
/**
|
|
54
62
|
* Maximum number of files that can be uploaded
|
|
55
63
|
*/
|
|
@@ -65,11 +73,15 @@ export type DropzoneProps = {
|
|
|
65
73
|
/**
|
|
66
74
|
* Handler called when the file input's value changes
|
|
67
75
|
*/
|
|
68
|
-
onChange?: (event:
|
|
76
|
+
onChange?: (event: DropEvent) => void;
|
|
69
77
|
/**
|
|
70
78
|
* Handler called when the file picker button is clicked
|
|
71
79
|
*/
|
|
72
80
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
81
|
+
/**
|
|
82
|
+
* More sophisticated version of "onChange". This is the recommend function to use for changes to the form state
|
|
83
|
+
*/
|
|
84
|
+
onDrop?: (acceptedFiles: File[], fileRejections: (FileRejection & { id: number })[], event: DropEvent) => void;
|
|
73
85
|
/**
|
|
74
86
|
* Callback to handle rejected files that don't meet validation criteria
|
|
75
87
|
*/
|
|
@@ -78,58 +90,76 @@ export type DropzoneProps = {
|
|
|
78
90
|
* Callback to update the total size of all uploaded files
|
|
79
91
|
*/
|
|
80
92
|
setTotalSize: Dispatch<React.SetStateAction<number>>;
|
|
93
|
+
/**
|
|
94
|
+
* Validation function used for custom validation that is not covered with the other props
|
|
95
|
+
* */
|
|
96
|
+
validator?: (file: File) => FileError | FileError[] | null;
|
|
81
97
|
};
|
|
82
98
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// onFileDelivery?: (upload: Upload) => void;
|
|
99
|
+
const DropzoneContainer = styled(Box, { name: 'AvDropzoneContainer', slot: 'root' })({
|
|
100
|
+
'.MuiDivider-root': {
|
|
101
|
+
width: '196px',
|
|
102
|
+
marginLeft: 'auto',
|
|
103
|
+
marginRight: 'auto',
|
|
104
|
+
},
|
|
105
|
+
}) as typeof MuiBox;
|
|
91
106
|
|
|
92
107
|
export const Dropzone = ({
|
|
93
108
|
allowedFileTypes = [],
|
|
94
109
|
disabled,
|
|
110
|
+
enableDropArea = true,
|
|
95
111
|
maxFiles,
|
|
96
112
|
maxSize,
|
|
97
113
|
multiple,
|
|
98
114
|
name,
|
|
99
115
|
onChange,
|
|
100
116
|
onClick,
|
|
117
|
+
onDrop,
|
|
101
118
|
setFileRejections,
|
|
102
119
|
setTotalSize,
|
|
120
|
+
validator,
|
|
103
121
|
}: DropzoneProps) => {
|
|
104
|
-
const { setValue, watch } = useFormContext();
|
|
122
|
+
const { getValues, setValue, watch } = useFormContext();
|
|
105
123
|
|
|
106
|
-
const
|
|
124
|
+
const handleValidation = useCallback(
|
|
107
125
|
(file: File) => {
|
|
108
126
|
const previous: File[] = watch(name) ?? [];
|
|
127
|
+
const errors: FileError[] = [];
|
|
109
128
|
|
|
110
129
|
const isDuplicate = previous.some((prev) => prev.name === file.name);
|
|
111
130
|
if (isDuplicate) {
|
|
112
|
-
|
|
131
|
+
errors.push({
|
|
113
132
|
code: 'duplicate-name',
|
|
114
133
|
message: 'A file with this name already exists',
|
|
115
|
-
};
|
|
134
|
+
});
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
const hasMaxFiles = maxFiles && previous.length >= maxFiles;
|
|
119
138
|
if (hasMaxFiles) {
|
|
120
|
-
|
|
139
|
+
errors.push({
|
|
121
140
|
code: 'too-many-files',
|
|
122
141
|
message: `Too many files. You may only upload ${maxFiles} file(s).`,
|
|
123
|
-
};
|
|
142
|
+
});
|
|
124
143
|
}
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
if (validator) {
|
|
146
|
+
const validatorErrors = validator(file);
|
|
147
|
+
if (validatorErrors) {
|
|
148
|
+
if (Array.isArray(validatorErrors)) {
|
|
149
|
+
errors.push(...validatorErrors);
|
|
150
|
+
} else {
|
|
151
|
+
errors.push(validatorErrors);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return errors.length > 0 ? errors : null;
|
|
127
157
|
},
|
|
128
|
-
[maxFiles]
|
|
158
|
+
[maxFiles, validator]
|
|
129
159
|
);
|
|
130
160
|
|
|
131
|
-
const
|
|
132
|
-
(acceptedFiles: File[], fileRejections: (FileRejection & { id: number })[]) => {
|
|
161
|
+
const handleOnDrop = useCallback(
|
|
162
|
+
(acceptedFiles: File[], fileRejections: (FileRejection & { id: number })[], event: DropEvent) => {
|
|
133
163
|
let newSize = 0;
|
|
134
164
|
for (const file of acceptedFiles) {
|
|
135
165
|
newSize += file.size;
|
|
@@ -149,6 +179,7 @@ export const Dropzone = ({
|
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
if (setFileRejections) setFileRejections(fileRejections);
|
|
182
|
+
if (onDrop) onDrop(acceptedFiles, fileRejections, event);
|
|
152
183
|
},
|
|
153
184
|
[setFileRejections]
|
|
154
185
|
);
|
|
@@ -156,13 +187,13 @@ export const Dropzone = ({
|
|
|
156
187
|
const accept = allowedFileTypes.join(',');
|
|
157
188
|
|
|
158
189
|
const { getRootProps, getInputProps } = useDropzone({
|
|
159
|
-
onDrop,
|
|
190
|
+
onDrop: handleOnDrop,
|
|
160
191
|
maxSize,
|
|
161
192
|
maxFiles,
|
|
162
193
|
disabled,
|
|
163
194
|
multiple,
|
|
164
195
|
accept,
|
|
165
|
-
validator,
|
|
196
|
+
validator: handleValidation,
|
|
166
197
|
});
|
|
167
198
|
|
|
168
199
|
const inputProps = getInputProps({
|
|
@@ -171,18 +202,30 @@ export const Dropzone = ({
|
|
|
171
202
|
onChange,
|
|
172
203
|
});
|
|
173
204
|
|
|
205
|
+
// Remove role and tabIndex for accessibility
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
207
|
+
const { role, tabIndex, ...rootProps } = getRootProps();
|
|
208
|
+
|
|
174
209
|
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
175
210
|
if (inputProps.onChange) {
|
|
176
211
|
inputProps.onChange(event);
|
|
177
212
|
}
|
|
178
213
|
};
|
|
179
214
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
const handleOnClick = (event: MouseEvent<HTMLButtonElement>) => {
|
|
216
|
+
if (!enableDropArea && rootProps.onClick) rootProps.onClick(event);
|
|
217
|
+
if (onClick) onClick;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const getFieldValue = () => {
|
|
221
|
+
const field = getValues();
|
|
222
|
+
return field[name] || [];
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const hasFiles = getFieldValue().length > 0;
|
|
183
226
|
|
|
184
|
-
return (
|
|
185
|
-
<
|
|
227
|
+
return enableDropArea ? (
|
|
228
|
+
<DropzoneContainer sx={outerBoxStyles} {...rootProps}>
|
|
186
229
|
<Box sx={innerBoxStyles}>
|
|
187
230
|
<Stack spacing={2} alignItems="center" justifyContent="center">
|
|
188
231
|
<>
|
|
@@ -190,7 +233,9 @@ export const Dropzone = ({
|
|
|
190
233
|
<Typography variant="subtitle2" fontWeight="700">
|
|
191
234
|
Drag and Drop Files Here
|
|
192
235
|
</Typography>
|
|
193
|
-
<Divider>
|
|
236
|
+
<Divider flexItem>
|
|
237
|
+
<Typography variant="subtitle2">OR</Typography>
|
|
238
|
+
</Divider>
|
|
194
239
|
<FilePickerBtn
|
|
195
240
|
name={name}
|
|
196
241
|
color="primary"
|
|
@@ -199,10 +244,25 @@ export const Dropzone = ({
|
|
|
199
244
|
onClick={onClick}
|
|
200
245
|
inputProps={inputProps}
|
|
201
246
|
onChange={handleOnChange}
|
|
202
|
-
|
|
247
|
+
>
|
|
248
|
+
Browse Files
|
|
249
|
+
</FilePickerBtn>
|
|
203
250
|
</>
|
|
204
251
|
</Stack>
|
|
205
252
|
</Box>
|
|
206
|
-
</
|
|
253
|
+
</DropzoneContainer>
|
|
254
|
+
) : (
|
|
255
|
+
<FilePickerBtn
|
|
256
|
+
name={name}
|
|
257
|
+
color="tertiary"
|
|
258
|
+
disabled={disabled}
|
|
259
|
+
maxSize={maxSize}
|
|
260
|
+
onClick={handleOnClick}
|
|
261
|
+
inputProps={inputProps}
|
|
262
|
+
onChange={handleOnChange}
|
|
263
|
+
startIcon={<PlusIcon />}
|
|
264
|
+
>
|
|
265
|
+
{hasFiles ? 'Add More Files' : 'Add File(s)'}
|
|
266
|
+
</FilePickerBtn>
|
|
207
267
|
);
|
|
208
268
|
};
|
|
@@ -44,7 +44,31 @@ describe('FileList', () => {
|
|
|
44
44
|
expect(screen.getByText('mock.txt')).toBeDefined();
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
fireEvent.click(screen.
|
|
47
|
+
fireEvent.click(screen.getByLabelText('remove file'));
|
|
48
48
|
expect(mockRemove).toHaveBeenCalled();
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
test('should not display remove button when disableRemove is set to true', async () => {
|
|
52
|
+
const mockRemove = jest.fn();
|
|
53
|
+
const mockFile = new File(['file content'], 'mock.txt', { type: 'text/plain' });
|
|
54
|
+
|
|
55
|
+
render(
|
|
56
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
57
|
+
<FileList
|
|
58
|
+
files={[mockFile]}
|
|
59
|
+
options={{ bucketId: '123', customerId: '123', clientId: '123' }}
|
|
60
|
+
onRemoveFile={(id) => {
|
|
61
|
+
mockRemove(id);
|
|
62
|
+
}}
|
|
63
|
+
disableRemove
|
|
64
|
+
/>
|
|
65
|
+
</QueryClientProvider>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(screen.getByText('mock.txt')).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(() => screen.getByLabelText('remove file')).toThrow();
|
|
73
|
+
});
|
|
50
74
|
});
|
package/src/lib/FileList.tsx
CHANGED
|
@@ -7,10 +7,13 @@ import { Divider } from '@availity/mui-divider';
|
|
|
7
7
|
|
|
8
8
|
import { UploadProgressBar } from './UploadProgressBar';
|
|
9
9
|
import { formatBytes, getFileExtIcon } from './util';
|
|
10
|
-
import { useUploadCore
|
|
10
|
+
import { useUploadCore } from './useUploadCore';
|
|
11
|
+
import type { Options, UploadQueryOptions } from './useUploadCore';
|
|
11
12
|
|
|
12
|
-
type FileRowProps = {
|
|
13
|
-
/**
|
|
13
|
+
export type FileRowProps = {
|
|
14
|
+
/**
|
|
15
|
+
* The File object containing information about the uploaded file
|
|
16
|
+
* */
|
|
14
17
|
file: File;
|
|
15
18
|
/**
|
|
16
19
|
* Callback function called when a file is removed
|
|
@@ -18,29 +21,57 @@ type FileRowProps = {
|
|
|
18
21
|
* @param upload - The Upload instance associated with the file
|
|
19
22
|
*/
|
|
20
23
|
onRemoveFile: (id: string, upload: Upload) => void;
|
|
21
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Configuration options for the upload call
|
|
26
|
+
* */
|
|
22
27
|
options: Options;
|
|
28
|
+
/**
|
|
29
|
+
* Query options from `react-query` for the upload call
|
|
30
|
+
* */
|
|
31
|
+
queryOptions?: UploadQueryOptions;
|
|
32
|
+
customFileRow?: React.ElementType<{
|
|
33
|
+
upload?: Upload;
|
|
34
|
+
options: Options;
|
|
35
|
+
onRemoveFile: (id: string, upload: Upload) => void;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the remove button is disabled
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
disableRemove?: boolean;
|
|
23
42
|
};
|
|
24
43
|
|
|
25
|
-
const FileRow = ({
|
|
44
|
+
export const FileRow = ({
|
|
45
|
+
file,
|
|
46
|
+
options,
|
|
47
|
+
onRemoveFile,
|
|
48
|
+
queryOptions,
|
|
49
|
+
customFileRow: CustomRow,
|
|
50
|
+
disableRemove = false,
|
|
51
|
+
}: FileRowProps) => {
|
|
26
52
|
const Icon = getFileExtIcon(file.name);
|
|
27
53
|
|
|
28
|
-
const { data: upload } = useUploadCore(file, options);
|
|
54
|
+
const { data: upload } = useUploadCore(file, options, queryOptions);
|
|
55
|
+
|
|
56
|
+
if (CustomRow) return <CustomRow upload={upload} options={options} onRemoveFile={onRemoveFile} />;
|
|
29
57
|
|
|
30
58
|
if (!upload) return null;
|
|
31
59
|
|
|
32
60
|
return (
|
|
33
61
|
<ListItem
|
|
62
|
+
disableGutters
|
|
34
63
|
secondaryAction={
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
!disableRemove && (
|
|
65
|
+
<IconButton
|
|
66
|
+
title="remove file"
|
|
67
|
+
edge="end"
|
|
68
|
+
onClick={() => {
|
|
69
|
+
onRemoveFile(upload.id, upload);
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<DeleteIcon />
|
|
73
|
+
</IconButton>
|
|
74
|
+
)
|
|
44
75
|
}
|
|
45
76
|
>
|
|
46
77
|
<Grid container spacing={2} alignItems="center" justifyContent="space-between" width="100%">
|
|
@@ -69,25 +100,32 @@ export type FileListProps = {
|
|
|
69
100
|
* Array of File objects to be displayed in the list
|
|
70
101
|
*/
|
|
71
102
|
files: File[];
|
|
72
|
-
|
|
73
|
-
* Callback function called when a file is removed from the list
|
|
74
|
-
* @param id - The unique identifier of the file being removed
|
|
75
|
-
* @param upload - The Upload instance associated with the file
|
|
76
|
-
*/
|
|
77
|
-
onRemoveFile: (id: string, upload: Upload) => void;
|
|
78
|
-
/**
|
|
79
|
-
* Configuration options applied to all file uploads in the list
|
|
80
|
-
*/
|
|
81
|
-
options: Options;
|
|
82
|
-
};
|
|
103
|
+
} & Omit<FileRowProps, 'file'>;
|
|
83
104
|
|
|
84
|
-
export const FileList = ({
|
|
105
|
+
export const FileList = ({
|
|
106
|
+
files,
|
|
107
|
+
options,
|
|
108
|
+
onRemoveFile,
|
|
109
|
+
queryOptions,
|
|
110
|
+
customFileRow,
|
|
111
|
+
disableRemove,
|
|
112
|
+
}: FileListProps): JSX.Element | null => {
|
|
85
113
|
if (files.length === 0) return null;
|
|
86
114
|
|
|
87
115
|
return (
|
|
88
116
|
<List>
|
|
89
117
|
{files.map((file) => {
|
|
90
|
-
return
|
|
118
|
+
return (
|
|
119
|
+
<FileRow
|
|
120
|
+
key={file.name}
|
|
121
|
+
file={file}
|
|
122
|
+
options={options}
|
|
123
|
+
onRemoveFile={onRemoveFile}
|
|
124
|
+
queryOptions={queryOptions}
|
|
125
|
+
customFileRow={customFileRow}
|
|
126
|
+
disableRemove={disableRemove}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
91
129
|
})}
|
|
92
130
|
</List>
|
|
93
131
|
);
|
|
@@ -10,7 +10,9 @@ const TestForm = () => {
|
|
|
10
10
|
|
|
11
11
|
return (
|
|
12
12
|
<FormProvider {...methods}>
|
|
13
|
-
<FilePickerBtn name="test" inputProps={{ ref }} onChange={jest.fn()}
|
|
13
|
+
<FilePickerBtn name="test" inputProps={{ ref }} onChange={jest.fn()}>
|
|
14
|
+
Browse Files
|
|
15
|
+
</FilePickerBtn>
|
|
14
16
|
</FormProvider>
|
|
15
17
|
);
|
|
16
18
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ChangeEvent, RefObject } from 'react';
|
|
1
|
+
import type { ChangeEvent, RefObject } from 'react';
|
|
2
2
|
import type { DropzoneInputProps } from 'react-dropzone';
|
|
3
|
-
import {
|
|
3
|
+
import { Controller } from 'react-hook-form';
|
|
4
4
|
import { Button, ButtonProps } from '@availity/mui-button';
|
|
5
5
|
import { Input } from '@availity/mui-form-utils';
|
|
6
6
|
|
|
7
|
-
type FilePickerBtnProps = {
|
|
7
|
+
export type FilePickerBtnProps = {
|
|
8
8
|
/**
|
|
9
9
|
* Name attribute for the input field, used by react-hook-form for form state management.
|
|
10
10
|
*/
|
|
@@ -29,7 +29,7 @@ type FilePickerBtnProps = {
|
|
|
29
29
|
|
|
30
30
|
export const FilePickerBtn = ({
|
|
31
31
|
name,
|
|
32
|
-
children
|
|
32
|
+
children,
|
|
33
33
|
color,
|
|
34
34
|
inputId,
|
|
35
35
|
inputProps = {},
|
|
@@ -38,27 +38,29 @@ export const FilePickerBtn = ({
|
|
|
38
38
|
onClick,
|
|
39
39
|
...rest
|
|
40
40
|
}: FilePickerBtnProps) => {
|
|
41
|
-
const { register } = useFormContext();
|
|
42
|
-
|
|
43
41
|
const { accept, multiple, ref, style, type: inputType } = inputProps;
|
|
44
42
|
|
|
45
|
-
const field = register(name);
|
|
46
|
-
|
|
47
43
|
return (
|
|
48
44
|
<>
|
|
49
|
-
<
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
45
|
+
<Controller
|
|
46
|
+
name={name}
|
|
47
|
+
defaultValue={[]}
|
|
48
|
+
render={({ field }) => (
|
|
49
|
+
<Input
|
|
50
|
+
{...field}
|
|
51
|
+
onChange={onChange}
|
|
52
|
+
value={[]}
|
|
53
|
+
inputRef={ref}
|
|
54
|
+
type={inputType}
|
|
55
|
+
sx={style}
|
|
56
|
+
inputProps={{
|
|
57
|
+
accept,
|
|
58
|
+
size: maxSize ?? undefined,
|
|
59
|
+
multiple,
|
|
60
|
+
}}
|
|
61
|
+
id={inputId}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
62
64
|
/>
|
|
63
65
|
<Button color={color} {...rest} onClick={onClick} fullWidth={false}>
|
|
64
66
|
{children}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// Each exported component in the package should have its own stories file
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
-
import {
|
|
4
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
5
|
+
import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query';
|
|
6
|
+
import type { default as Upload } from '@availity/upload-core';
|
|
4
7
|
import { Paper } from '@availity/mui-paper';
|
|
8
|
+
import { Alert, AlertProps, AlertTitle } from '@availity/mui-alert';
|
|
9
|
+
import { Grid } from '@availity/mui-layout';
|
|
10
|
+
import { Button } from '@availity/mui-button';
|
|
11
|
+
import Collapse from '@mui/material/Collapse';
|
|
5
12
|
|
|
6
13
|
import { FileSelector, FileSelectorProps } from './FileSelector';
|
|
7
14
|
|
|
@@ -30,22 +37,77 @@ const meta: Meta<typeof FileSelector> = {
|
|
|
30
37
|
|
|
31
38
|
export default meta;
|
|
32
39
|
|
|
40
|
+
const DismissableAlert = (props: AlertProps) => {
|
|
41
|
+
const [visible, setVisible] = useState(true);
|
|
42
|
+
|
|
43
|
+
const onClose = () => {
|
|
44
|
+
setVisible(false);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Collapse in={visible}>
|
|
49
|
+
<Alert onClose={onClose} {...props} />
|
|
50
|
+
</Collapse>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
33
54
|
export const _FileSelector: StoryObj<typeof FileSelector> = {
|
|
34
55
|
render: (props: FileSelectorProps) => {
|
|
56
|
+
const methods = useForm({
|
|
57
|
+
defaultValues: {
|
|
58
|
+
[props.name]: [] as File[],
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const client = useQueryClient();
|
|
63
|
+
|
|
64
|
+
const files = methods.watch(props.name);
|
|
65
|
+
|
|
66
|
+
const handleOnSubmit = (values: Record<string, File[]>) => {
|
|
67
|
+
if (values[props.name].length === 0) return;
|
|
68
|
+
|
|
69
|
+
const queries = client.getQueriesData<Upload>(['upload']);
|
|
70
|
+
const uploads = [];
|
|
71
|
+
for (const [, data] of queries) {
|
|
72
|
+
if (data) uploads.push(data);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
35
76
|
return (
|
|
36
77
|
<Paper sx={{ padding: '2rem' }}>
|
|
37
|
-
<
|
|
78
|
+
<FormProvider {...methods}>
|
|
79
|
+
<form onSubmit={methods.handleSubmit(handleOnSubmit)}>
|
|
80
|
+
<FileSelector {...props}>
|
|
81
|
+
<DismissableAlert severity="warning">
|
|
82
|
+
<AlertTitle>Make an Appeal</AlertTitle>
|
|
83
|
+
This is an example alert. It is not part of the component. `children` you pass to the component will
|
|
84
|
+
show up here.
|
|
85
|
+
</DismissableAlert>
|
|
86
|
+
</FileSelector>
|
|
87
|
+
{files.length > 0 && (
|
|
88
|
+
<Grid xs={12} justifyContent="end" display="flex" paddingTop={2.5}>
|
|
89
|
+
<Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
|
90
|
+
Submit
|
|
91
|
+
</Button>
|
|
92
|
+
</Grid>
|
|
93
|
+
)}
|
|
94
|
+
</form>
|
|
95
|
+
</FormProvider>
|
|
38
96
|
</Paper>
|
|
39
97
|
);
|
|
40
98
|
},
|
|
41
99
|
args: {
|
|
42
100
|
name: 'file-selector',
|
|
43
|
-
allowedFileTypes: [],
|
|
101
|
+
allowedFileTypes: ['.txt', '.png', '.pdf'],
|
|
44
102
|
clientId: '123',
|
|
45
103
|
customerId: '456',
|
|
46
104
|
bucketId: '789',
|
|
47
|
-
|
|
105
|
+
uploadOptions: {
|
|
106
|
+
retryDelays: [],
|
|
107
|
+
},
|
|
108
|
+
maxFiles: 5,
|
|
48
109
|
maxSize: 1 * 1024 * 1024, // 1MB
|
|
110
|
+
enableDropArea: true,
|
|
49
111
|
isCloud: true,
|
|
50
112
|
multiple: true,
|
|
51
113
|
},
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import { render } from '@testing-library/react';
|
|
3
|
+
import { useForm, FormProvider } from 'react-hook-form';
|
|
2
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
5
|
|
|
4
6
|
import { FileSelector } from './FileSelector';
|
|
5
7
|
|
|
8
|
+
const TestForm = ({ children }: { children: ReactNode }) => {
|
|
9
|
+
const methods = useForm();
|
|
10
|
+
|
|
11
|
+
return <FormProvider {...methods}>{children}</FormProvider>;
|
|
12
|
+
};
|
|
13
|
+
|
|
6
14
|
describe('FileSelector', () => {
|
|
7
15
|
test('should render successfully', () => {
|
|
8
16
|
const client = new QueryClient();
|
|
9
17
|
const { getByText } = render(
|
|
10
18
|
<QueryClientProvider client={client}>
|
|
11
|
-
<
|
|
19
|
+
<TestForm>
|
|
20
|
+
<FileSelector name="test" bucketId="test" customerId="123" clientId="test" maxSize={1000} maxFiles={1} />
|
|
21
|
+
</TestForm>
|
|
12
22
|
</QueryClientProvider>
|
|
13
23
|
);
|
|
14
24
|
|