@availity/mui-file-selector 1.0.0 → 1.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.
@@ -1,17 +1,19 @@
1
- import { ChangeEvent, ReactNode, useState } from 'react';
2
- import { useForm, FormProvider } from 'react-hook-form';
3
- import type { FileRejection } from 'react-dropzone/typings/react-dropzone';
1
+ import { useState } from 'react';
2
+ import type { ChangeEvent, ElementType, ReactNode } from 'react';
3
+ import { useFormContext } from 'react-hook-form';
4
+ import type { DropEvent, FileError, FileRejection } from 'react-dropzone/typings/react-dropzone';
4
5
  import { useQueryClient } from '@tanstack/react-query';
5
- import Upload, { UploadOptions } from '@availity/upload-core';
6
- import { Button } from '@availity/mui-button';
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
9
 
10
10
  import { Dropzone } from './Dropzone';
11
11
  import { ErrorAlert } from './ErrorAlert';
12
12
  import { FileList } from './FileList';
13
+ import type { FileListProps } from './FileList';
13
14
  import { FileTypesMessage } from './FileTypesMessage';
14
- import { Options } from './useUploadCore';
15
+ import { HeaderMessage } from './HeaderMessage';
16
+ import type { Options, UploadQueryOptions } from './useUploadCore';
15
17
 
16
18
  const CLOUD_URL = '/cloud/web/appl/vault/upload/v1/resumable';
17
19
 
@@ -53,10 +55,18 @@ export type FileSelectorProps = {
53
55
  * @default false
54
56
  */
55
57
  disabled?: boolean;
58
+ /**
59
+ * Whether to enable the dropzone area
60
+ */
61
+ enableDropArea?: boolean;
56
62
  /**
57
63
  * Custom endpoint URL for file uploads. If not provided, default endpoint will be used
58
64
  */
59
65
  endpoint?: string;
66
+ /**
67
+ * Componet to render the File information. This should return a `ListItem`
68
+ */
69
+ customFileRow?: ElementType<FileListProps>;
60
70
  /**
61
71
  * Whether to use the cloud upload endpoint
62
72
  * When true, uses '/cloud/web/appl/vault/upload/v1/resumable'
@@ -70,7 +80,7 @@ export type FileSelectorProps = {
70
80
  /**
71
81
  * Maximum number of files that can be uploaded simultaneously
72
82
  */
73
- maxFiles?: number;
83
+ maxFiles: number;
74
84
  /**
75
85
  * Maximum file size allowed per file in bytes
76
86
  * Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
@@ -87,25 +97,9 @@ export type FileSelectorProps = {
87
97
  */
88
98
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
89
99
  /**
90
- * Callback fired when the form is submitted
91
- * @param uploads - Array of Upload instances for the submitted files
92
- * @param values - Object containing the form values, with files indexed by the name prop
100
+ *
93
101
  */
94
- onSubmit?: (uploads: Upload[], values: Record<string, File[]>) => void;
95
- /**
96
- * Callback fired when a file is successfully uploaded
97
- */
98
- onSuccess?: () => void;
99
- /**
100
- * Callback fired when an error occurs during upload
101
- */
102
- onError?: (error: Error) => void;
103
- /**
104
- * Array of functions to execute before file upload begins.
105
- * Each function should return a boolean indicating whether to proceed with the upload.
106
- * @default []
107
- */
108
- onFilePreUpload?: (() => boolean)[];
102
+ onDrop?: (acceptedFiles: File[], fileRejections: (FileRejection & { id: number })[], event: DropEvent) => void;
109
103
  /**
110
104
  * Callback fired when a file is removed from the upload list
111
105
  * @param files - Array of remaining files
@@ -113,19 +107,24 @@ export type FileSelectorProps = {
113
107
  */
114
108
  onUploadRemove?: (files: File[], removedUploadId: string) => void;
115
109
  /**
116
- * Array of delays (in milliseconds) between upload retry attempts
110
+ * Query options from `react-query` for the upload call
111
+ * */
112
+ queryOptions?: UploadQueryOptions;
113
+ /**
114
+ * Options that are passed to the Upload class from `@availity/upload-core`
117
115
  */
118
- retryDelays?: UploadOptions['retryDelays'];
116
+ uploadOptions?: Partial<Options>;
117
+ /**
118
+ * Validation function used for custom validation that is not covered with the other props
119
+ * */
120
+ validator?: (file: File) => FileError | FileError[] | null;
121
+ /**
122
+ * Whether the remove button is disabled
123
+ * @default false
124
+ */
125
+ disableRemove?: boolean;
119
126
  };
120
127
 
121
- // Below props were removed from availity-react version. Perserving here in case needed later
122
- // deliverFileOnSubmit?: boolean;
123
- // deliveryChannel?: string;
124
- // fileDeliveryMetadata?: Record<string, unknown> | ((file: Upload) => Record<string, unknown>);
125
- // onDeliveryError?: (error: unknown) => void;
126
- // onDeliverySuccess?: () => void;
127
- // onFileDelivery?: (upload: Upload) => void;
128
-
129
128
  export const FileSelector = ({
130
129
  name,
131
130
  allowedFileNameCharacters,
@@ -134,7 +133,9 @@ export const FileSelector = ({
134
133
  clientId,
135
134
  children,
136
135
  customerId,
136
+ customFileRow,
137
137
  disabled = false,
138
+ enableDropArea = true,
138
139
  endpoint,
139
140
  isCloud,
140
141
  label = 'Upload file',
@@ -142,48 +143,51 @@ export const FileSelector = ({
142
143
  maxSize,
143
144
  multiple = true,
144
145
  onChange,
145
- onSubmit,
146
- onSuccess,
147
- onError,
148
- onFilePreUpload = [],
146
+ onDrop,
149
147
  onUploadRemove,
150
- retryDelays,
148
+ queryOptions,
149
+ uploadOptions,
150
+ validator,
151
+ disableRemove,
151
152
  }: FileSelectorProps) => {
152
153
  const [totalSize, setTotalSize] = useState(0);
153
154
  const [fileRejections, setFileRejections] = useState<(FileRejection & { id: number })[]>([]);
154
155
 
155
156
  const client = useQueryClient();
156
-
157
- const methods = useForm({
158
- defaultValues: {
159
- [name]: [] as File[],
160
- },
161
- });
157
+ const formMethods = useFormContext();
162
158
 
163
159
  const options: Options = {
160
+ ...uploadOptions,
164
161
  bucketId,
165
162
  customerId,
166
163
  clientId,
167
164
  fileTypes: allowedFileTypes,
168
165
  maxSize,
169
166
  allowedFileNameCharacters,
170
- onError,
171
- onSuccess,
172
- retryDelays,
173
167
  };
174
168
 
175
- if (onFilePreUpload) options.onPreStart = onFilePreUpload;
169
+ // Endpoint is set by default in upload-core so check if it exists before passing `undefined`
176
170
  if (endpoint) options.endpoint = endpoint;
171
+ // Override endpoint if using the cloud
177
172
  if (isCloud) options.endpoint = CLOUD_URL;
178
173
 
179
174
  const handleOnRemoveFile = (uploadId: string, upload: Upload) => {
180
- const prevFiles = methods.watch(name);
175
+ const prevFiles: File[] = formMethods.getValues(name);
181
176
  const newFiles = prevFiles.filter((file) => file.name !== upload.file.name);
182
177
 
183
178
  if (newFiles.length !== prevFiles.length) {
184
179
  const removedFile = prevFiles.find((file) => file.name === upload.file.name);
185
180
 
186
- methods.setValue(name, newFiles);
181
+ // Stop upload
182
+ try {
183
+ upload.abort();
184
+ } catch {
185
+ console.error('Encountered an issue stopping the file upload');
186
+ }
187
+
188
+ // Remove from context and cache
189
+ formMethods.setValue(name, newFiles);
190
+ client.removeQueries(['upload', upload.file.name]);
187
191
 
188
192
  if (removedFile?.size) setTotalSize(totalSize - removedFile.size);
189
193
 
@@ -191,19 +195,7 @@ export const FileSelector = ({
191
195
  }
192
196
  };
193
197
 
194
- const files = methods.watch(name);
195
-
196
- const handleOnSubmit = (values: Record<string, File[]>) => {
197
- if (values[name].length === 0) return;
198
-
199
- const queries = client.getQueriesData<Upload>(['upload']);
200
- const uploads = [];
201
- for (const [, data] of queries) {
202
- if (data) uploads.push(data);
203
- }
204
-
205
- if (onSubmit) onSubmit(uploads, values);
206
- };
198
+ const files = formMethods.watch(name);
207
199
 
208
200
  const handleRemoveRejection = (id: number) => {
209
201
  const rejections = fileRejections.filter((value) => value.id !== id);
@@ -211,44 +203,71 @@ export const FileSelector = ({
211
203
  };
212
204
 
213
205
  return (
214
- <FormProvider {...methods}>
215
- <form onSubmit={methods.handleSubmit(handleOnSubmit)}>
206
+ <>
207
+ {enableDropArea ? (
216
208
  <>
217
- <Typography marginBottom="4px">{label}</Typography>
209
+ {label ? <Typography marginBottom="4px">{label}</Typography> : null}
218
210
  <Dropzone
219
211
  name={name}
220
212
  allowedFileTypes={allowedFileTypes}
221
213
  disabled={disabled}
214
+ enableDropArea={enableDropArea}
222
215
  maxFiles={maxFiles}
223
216
  maxSize={maxSize}
224
217
  multiple={multiple}
225
218
  onChange={onChange}
219
+ onDrop={onDrop}
226
220
  setFileRejections={setFileRejections}
227
221
  setTotalSize={setTotalSize}
222
+ validator={validator}
228
223
  />
229
- <FileTypesMessage allowedFileTypes={allowedFileTypes} maxFileSize={maxSize} />
224
+ <FileTypesMessage allowedFileTypes={allowedFileTypes} maxFileSize={maxSize} variant="caption" />
225
+ {children}
230
226
  </>
231
- {children}
232
- {fileRejections.length > 0
233
- ? fileRejections.map((rejection) => (
234
- <ErrorAlert
235
- key={rejection.id}
236
- errors={rejection.errors}
237
- fileName={rejection.file.name}
238
- id={rejection.id}
239
- onClose={() => handleRemoveRejection(rejection.id)}
240
- />
241
- ))
242
- : null}
243
- <FileList files={files} options={options} onRemoveFile={handleOnRemoveFile} />
244
- {files.length > 0 && (
245
- <Grid size={{ xs: 12 }} justifyContent="end" display="flex" paddingTop={2.5}>
246
- <Button type="submit" sx={{ marginLeft: 'auto', marginRight: 0 }}>
247
- Submit
248
- </Button>
227
+ ) : (
228
+ <Grid container rowSpacing={3} flexDirection="column">
229
+ <Grid>
230
+ <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
231
+ <FileTypesMessage allowedFileTypes={allowedFileTypes} variant="body2" />
232
+ </Grid>
233
+ {children ? <Grid>{children}</Grid> : null}
234
+ <Grid>
235
+ <Dropzone
236
+ name={name}
237
+ allowedFileTypes={allowedFileTypes}
238
+ disabled={disabled}
239
+ enableDropArea={enableDropArea}
240
+ maxFiles={maxFiles}
241
+ maxSize={maxSize}
242
+ multiple={multiple}
243
+ onChange={onChange}
244
+ onDrop={onDrop}
245
+ setFileRejections={setFileRejections}
246
+ setTotalSize={setTotalSize}
247
+ validator={validator}
248
+ />
249
249
  </Grid>
250
- )}
251
- </form>
252
- </FormProvider>
250
+ </Grid>
251
+ )}
252
+ {fileRejections.length > 0
253
+ ? fileRejections.map((rejection) => (
254
+ <ErrorAlert
255
+ key={rejection.id}
256
+ errors={rejection.errors}
257
+ fileName={rejection.file.name}
258
+ id={rejection.id}
259
+ onClose={() => handleRemoveRejection(rejection.id)}
260
+ />
261
+ ))
262
+ : null}
263
+ <FileList
264
+ files={files || []}
265
+ options={options}
266
+ onRemoveFile={handleOnRemoveFile}
267
+ queryOptions={queryOptions}
268
+ customFileRow={customFileRow}
269
+ disableRemove={disableRemove}
270
+ />
271
+ </>
253
272
  );
254
273
  };
@@ -4,13 +4,13 @@ import { FileTypesMessage } from './FileTypesMessage';
4
4
 
5
5
  describe('FileTypesMessage', () => {
6
6
  test('should render successfully', () => {
7
- render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} />);
7
+ render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} variant="caption" />);
8
8
 
9
9
  expect(screen.getByText(/All file types allowed/)).toBeTruthy();
10
10
  });
11
11
 
12
12
  test('should show file size', () => {
13
- render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} />);
13
+ render(<FileTypesMessage allowedFileTypes={[]} maxFileSize={1000} variant="body2" />);
14
14
 
15
15
  expect(screen.getByText(/Maximum file size is/)).toBeTruthy();
16
16
  });
@@ -2,26 +2,31 @@ import { Typography } from '@availity/mui-typography';
2
2
 
3
3
  import { formatBytes } from './util';
4
4
 
5
- type FileTypesMessageProps = {
5
+ export type FileTypesMessageProps = {
6
6
  /**
7
7
  * Allowed file type extensions. Each extension should be prefixed with a ".". eg: .txt, .pdf, .png
8
8
  */
9
- allowedFileTypes: `.${string}`[];
9
+ allowedFileTypes?: `.${string}`[];
10
10
  /**
11
11
  * Maximum size per file in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
12
12
  */
13
- maxFileSize: number;
13
+ maxFileSize?: number;
14
+ variant?: 'caption' | 'body2';
14
15
  };
15
16
 
16
- export const FileTypesMessage = ({ allowedFileTypes, maxFileSize }: FileTypesMessageProps) => {
17
+ export const FileTypesMessage = ({
18
+ allowedFileTypes = [],
19
+ maxFileSize,
20
+ variant = 'caption',
21
+ }: FileTypesMessageProps) => {
17
22
  const fileSizeMsg = typeof maxFileSize === 'number' ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null;
18
23
  const fileTypesMsg =
19
24
  allowedFileTypes.length > 0
20
- ? `Supported file types include: ${allowedFileTypes.join(', ')}.`
25
+ ? `Supported file types include: ${allowedFileTypes.join(', ')}`
21
26
  : 'All file types allowed.';
22
27
 
23
28
  return (
24
- <Typography variant="caption">
29
+ <Typography variant={variant}>
25
30
  {fileSizeMsg}
26
31
  {fileTypesMsg}
27
32
  </Typography>
@@ -16,7 +16,7 @@ export type HeaderMessageProps = {
16
16
  export const HeaderMessage = ({ maxFiles, maxSize }: HeaderMessageProps) => {
17
17
  return (
18
18
  <Typography variant="h6">
19
- Attach up to {maxFiles} file(s), with a maximum of {formatBytes(maxSize)}
19
+ Attach up to {maxFiles} file(s), with a maximum individual size of {formatBytes(maxSize)}
20
20
  </Typography>
21
21
  );
22
22
  };
@@ -77,7 +77,9 @@ export function useFileDelivery({
77
77
  uploads.length > 0 &&
78
78
  validate(errors);
79
79
 
80
- return useQuery(['file-delivery', customerId, clientId, bucketId], () => callFileDelivery(uploads), {
80
+ return useQuery({
81
+ queryKey: ['file-delivery', customerId, clientId, bucketId],
82
+ queryFn: () => callFileDelivery(uploads),
81
83
  enabled: isQueryEnabled,
82
84
  retry: false,
83
85
  });
@@ -1,11 +1,15 @@
1
1
  import { useQuery } from '@tanstack/react-query';
2
- import Upload, { UploadOptions } from '@availity/upload-core';
2
+ import type { UseQueryOptions } from '@tanstack/react-query';
3
+ import Upload from '@availity/upload-core';
4
+ import type { UploadOptions } from '@availity/upload-core';
3
5
 
4
6
  export type Options = {
5
7
  onError?: (error: Error) => void;
6
8
  onSuccess?: () => void;
7
9
  } & UploadOptions;
8
10
 
11
+ export type UploadQueryOptions = UseQueryOptions<Upload, Error, Upload, [string, string, Options]>;
12
+
9
13
  async function startUpload(file: File, options: Options) {
10
14
  const { onSuccess, onError, ...uploadOptions } = options;
11
15
  const upload = new Upload(file, uploadOptions);
@@ -20,12 +24,15 @@ async function startUpload(file: File, options: Options) {
20
24
  return upload;
21
25
  }
22
26
 
23
- export function useUploadCore(file: File, options: Options) {
27
+ export function useUploadCore(file: File, options: Options, queryOptions?: UploadQueryOptions) {
24
28
  const isQueryEnabled = !!file;
25
29
 
26
- return useQuery(['upload', file.name, options], () => startUpload(file, options), {
30
+ return useQuery({
31
+ queryKey: ['upload', file.name, options],
32
+ queryFn: () => startUpload(file, options),
27
33
  enabled: isQueryEnabled,
28
34
  retry: false,
29
35
  refetchOnWindowFocus: false,
36
+ ...queryOptions,
30
37
  });
31
38
  }