@availity/mui-file-selector 1.7.0 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@availity/mui-file-selector",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Availity MUI file-selector Component - part of the @availity/element design system",
5
5
  "keywords": [
6
6
  "react",
@@ -51,6 +51,7 @@
51
51
  "@availity/mui-progress": "^1.0.3",
52
52
  "@availity/mui-typography": "^1.0.2",
53
53
  "@availity/upload-core": "^8.0.0",
54
+ "@tanstack/react-query": "^4.36.1",
54
55
  "react-dropzone": "^11.7.1",
55
56
  "react-hook-form": "^7.55.0",
56
57
  "tus-js-client": "4.3.1",
@@ -11,6 +11,7 @@ import { Box, Stack } from '@availity/mui-layout';
11
11
  import { Typography } from '@availity/mui-typography';
12
12
 
13
13
  import { FilePickerBtn } from './FilePickerBtn';
14
+ import { formatBytes } from './util';
14
15
 
15
16
  export const outerBoxStyles = {
16
17
  backgroundColor: 'background.secondary',
@@ -66,6 +67,10 @@ export type DropzoneProps = {
66
67
  * Maximum size of each file in bytes
67
68
  */
68
69
  maxSize?: number;
70
+ /**
71
+ * Maximum size of total upload in bytes
72
+ */
73
+ maxTotalSize?: number;
69
74
  /**
70
75
  * Whether multiple file selection is allowed
71
76
  */
@@ -110,6 +115,7 @@ export const Dropzone = ({
110
115
  enableDropArea = true,
111
116
  maxFiles,
112
117
  maxSize,
118
+ maxTotalSize,
113
119
  multiple,
114
120
  name,
115
121
  onChange,
@@ -169,8 +175,64 @@ export const Dropzone = ({
169
175
 
170
176
  const previous = watch(name) ?? [];
171
177
 
178
+ if (maxTotalSize) {
179
+ // Calculate current total size
180
+ const currentTotalSize = previous.reduce((sum: number, file: File) => sum + file.size, 0);
181
+ let newSize = 0;
182
+
183
+ const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
184
+ let sizeCounter = 0;
185
+
186
+ // Find the index where we exceed the total size limit
187
+ const cutoffIndex = acceptedFiles.findIndex((file) => {
188
+ sizeCounter += file.size;
189
+ return sizeCounter > availableSize;
190
+ });
191
+
192
+ // If we found files that exceed the limit
193
+ if (cutoffIndex !== -1) {
194
+ // Files that fit within the size limit
195
+ const filesToAdd = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
196
+
197
+ // Create rejection for excess files
198
+ fileRejections.push({
199
+ file: acceptedFiles[cutoffIndex],
200
+ errors: [
201
+ {
202
+ code: 'upload-too-large',
203
+ message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`,
204
+ },
205
+ ],
206
+ id: counter.increment(),
207
+ });
208
+
209
+ // Update acceptedFiles to only include files that fit
210
+ acceptedFiles = filesToAdd;
211
+ }
212
+
213
+ // Calculate size of accepted files for the state update
214
+ newSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
215
+ setTotalSize((prev) => prev + newSize);
216
+ }
217
+
172
218
  // Set accepted files to form context
173
- setValue(name, previous.concat(acceptedFiles));
219
+ const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
220
+ const filesToAdd = acceptedFiles.slice(0, remainingSlots);
221
+ setValue(name, previous.concat(filesToAdd));
222
+
223
+ // Add rejections for excess files if needed
224
+ if (maxFiles && acceptedFiles.length > remainingSlots) {
225
+ fileRejections.push({
226
+ file: acceptedFiles[remainingSlots], // Use the first excess file
227
+ errors: [
228
+ {
229
+ code: 'too-many-files',
230
+ message: `Too many files. You may only upload ${maxFiles} file(s).`,
231
+ },
232
+ ],
233
+ id: counter.increment(),
234
+ });
235
+ }
174
236
 
175
237
  if (fileRejections.length > 0) {
176
238
  const TOO_MANY_FILES_CODE = 'too-many-files';
@@ -12,7 +12,7 @@ import type { UploadOptions } from '@availity/upload-core';
12
12
  import type { OnSuccessPayload } from 'tus-js-client';
13
13
 
14
14
  import { FilePickerBtn } from './FilePickerBtn';
15
- import { dedupeErrors } from './util';
15
+ import { dedupeErrors, formatBytes } from './util';
16
16
  import { createCounter, DropzoneContainer, innerBoxStyles, outerBoxStyles } from './Dropzone';
17
17
  import type { DropzoneProps } from './Dropzone';
18
18
 
@@ -59,6 +59,7 @@ export const Dropzone2 = ({
59
59
  enableDropArea = true,
60
60
  maxFiles,
61
61
  maxSize,
62
+ maxTotalSize,
62
63
  multiple,
63
64
  name,
64
65
  onChange,
@@ -121,10 +122,67 @@ export const Dropzone2 = ({
121
122
 
122
123
  const previous = watch(name) ?? [];
123
124
 
125
+ if (maxTotalSize) {
126
+ // Calculate current total size
127
+ const currentTotalSize = previous.reduce((sum: number, upload: Upload) => sum + upload.file.size, 0);
128
+ console.log({ previous });
129
+ let newSize = 0;
130
+
131
+ const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
132
+ let sizeCounter = 0;
133
+
134
+ // Find the index where we exceed the total size limit
135
+ const cutoffIndex = acceptedFiles.findIndex((file) => {
136
+ sizeCounter += file.size;
137
+ return sizeCounter > availableSize;
138
+ });
139
+
140
+ // If we found files that exceed the limit
141
+ if (cutoffIndex !== -1) {
142
+ // Files that fit within the size limit
143
+ const filesToAdd = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
144
+
145
+ // Create rejection for excess files
146
+ fileRejections.push({
147
+ file: acceptedFiles[cutoffIndex],
148
+ errors: [
149
+ {
150
+ code: 'upload-too-large',
151
+ message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`,
152
+ },
153
+ ],
154
+ id: counter.increment(),
155
+ });
156
+
157
+ // Update acceptedFiles to only include files that fit
158
+ acceptedFiles = filesToAdd;
159
+ }
160
+
161
+ // Calculate size of accepted files for the state update
162
+ newSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
163
+ setTotalSize((prev) => prev + newSize);
164
+ }
165
+
124
166
  // Set accepted files to form context
125
- const uploads = acceptedFiles.map((file) => startUpload(file, uploadOptions));
167
+ const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
168
+ const filesToAdd = acceptedFiles.slice(0, remainingSlots);
169
+ const uploads = filesToAdd.map((file) => startUpload(file, uploadOptions));
126
170
  setValue(name, previous.concat(await Promise.all(uploads)));
127
171
 
172
+ // Add rejections for excess files if needed
173
+ if (maxFiles && acceptedFiles.length > remainingSlots) {
174
+ fileRejections.push({
175
+ file: acceptedFiles[remainingSlots], // Use the first excess file
176
+ errors: [
177
+ {
178
+ code: 'too-many-files',
179
+ message: `Too many files. You may only upload ${maxFiles} file(s).`,
180
+ },
181
+ ],
182
+ id: counter.increment(),
183
+ });
184
+ }
185
+
128
186
  if (fileRejections.length > 0) {
129
187
  const TOO_MANY_FILES_CODE = 'too-many-files';
130
188
  let hasTooManyFiles = false;
@@ -5,6 +5,7 @@ import { formatBytes } from './util';
5
5
 
6
6
  const codes: Record<string, string> = {
7
7
  'file-too-large': 'File exceeds maximum size',
8
+ 'upload-too-large': 'File causes maximum total upload size to be exceeded',
8
9
  'file-invalid-type': 'File has an invalid type',
9
10
  'file-too-small': 'File is smaller than minimum size',
10
11
  'too-many-file': 'Too many files',
@@ -59,6 +59,11 @@ export type FileSelectorProps = {
59
59
  * Overrides the standard file size message
60
60
  */
61
61
  customSizeMessage?: React.ReactNode;
62
+
63
+ /**
64
+ * Overrides the standard total upload size message
65
+ */
66
+ customTotalSizeMessage?: React.ReactNode;
62
67
  /**
63
68
  * Overrides the standard file types message
64
69
  */
@@ -98,10 +103,16 @@ export type FileSelectorProps = {
98
103
  * Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
99
104
  */
100
105
  maxSize: number;
106
+ /**
107
+ * Maximum size allowed for total upload in bytes
108
+ * Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
109
+ */
110
+ maxTotalSize?: number;
101
111
  /**
102
112
  * Whether multiple file selection is allowed
103
113
  * @default true
104
114
  */
115
+
105
116
  multiple?: boolean;
106
117
  /**
107
118
  * Callback fired when files are selected
@@ -145,6 +156,7 @@ export const FileSelector = ({
145
156
  clientId,
146
157
  children,
147
158
  customSizeMessage,
159
+ customTotalSizeMessage,
148
160
  customTypesMessage,
149
161
  customerId,
150
162
  customFileRow,
@@ -155,6 +167,7 @@ export const FileSelector = ({
155
167
  label = 'Upload file',
156
168
  maxFiles,
157
169
  maxSize,
170
+ maxTotalSize,
158
171
  multiple = true,
159
172
  onChange,
160
173
  onDrop,
@@ -240,6 +253,7 @@ export const FileSelector = ({
240
253
  enableDropArea={enableDropArea}
241
254
  maxFiles={maxFiles}
242
255
  maxSize={maxSize}
256
+ maxTotalSize={maxTotalSize}
243
257
  multiple={multiple}
244
258
  onChange={onChange}
245
259
  onDrop={onDrop}
@@ -250,7 +264,9 @@ export const FileSelector = ({
250
264
  <FileTypesMessage
251
265
  allowedFileTypes={allowedFileTypes}
252
266
  maxFileSize={maxSize}
267
+ maxTotalSize={maxTotalSize}
253
268
  customSizeMessage={customSizeMessage}
269
+ customTotalSizeMessage={customTotalSizeMessage}
254
270
  customTypesMessage={customTypesMessage}
255
271
  variant="caption"
256
272
  />
@@ -259,9 +275,10 @@ export const FileSelector = ({
259
275
  ) : (
260
276
  <Grid container rowSpacing={3} flexDirection="column">
261
277
  <Grid>
262
- <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
278
+ <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} maxTotalSize={maxTotalSize} />
263
279
  <FileTypesMessage
264
280
  allowedFileTypes={allowedFileTypes}
281
+ customTotalSizeMessage={customTotalSizeMessage}
265
282
  customSizeMessage={customSizeMessage}
266
283
  customTypesMessage={customTypesMessage}
267
284
  variant="body2"
@@ -276,6 +293,7 @@ export const FileSelector = ({
276
293
  enableDropArea={enableDropArea}
277
294
  maxFiles={maxFiles}
278
295
  maxSize={maxSize}
296
+ maxTotalSize={maxTotalSize}
279
297
  multiple={multiple}
280
298
  onChange={onChange}
281
299
  onDrop={onDrop}
@@ -37,6 +37,7 @@ export const FileSelector2 = ({
37
37
  clientId,
38
38
  children,
39
39
  customSizeMessage,
40
+ customTotalSizeMessage,
40
41
  customTypesMessage,
41
42
  customerId,
42
43
  customFileRow,
@@ -47,6 +48,7 @@ export const FileSelector2 = ({
47
48
  label = 'Upload file',
48
49
  maxFiles,
49
50
  maxSize,
51
+ maxTotalSize,
50
52
  multiple = true,
51
53
  onChange,
52
54
  onDrop,
@@ -129,6 +131,7 @@ export const FileSelector2 = ({
129
131
  enableDropArea={enableDropArea}
130
132
  maxFiles={maxFiles}
131
133
  maxSize={maxSize}
134
+ maxTotalSize={maxTotalSize}
132
135
  multiple={multiple}
133
136
  onChange={onChange}
134
137
  onDrop={onDrop}
@@ -140,7 +143,9 @@ export const FileSelector2 = ({
140
143
  <FileTypesMessage
141
144
  allowedFileTypes={allowedFileTypes}
142
145
  maxFileSize={maxSize}
146
+ maxTotalSize={maxTotalSize}
143
147
  customSizeMessage={customSizeMessage}
148
+ customTotalSizeMessage={customTotalSizeMessage}
144
149
  customTypesMessage={customTypesMessage}
145
150
  variant="caption"
146
151
  />
@@ -149,10 +154,11 @@ export const FileSelector2 = ({
149
154
  ) : (
150
155
  <Grid container rowSpacing={3} flexDirection="column">
151
156
  <Grid>
152
- <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
157
+ <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} maxTotalSize={maxTotalSize} />
153
158
  <FileTypesMessage
154
159
  allowedFileTypes={allowedFileTypes}
155
160
  customSizeMessage={customSizeMessage}
161
+ customTotalSizeMessage={customTotalSizeMessage}
156
162
  customTypesMessage={customTypesMessage}
157
163
  variant="body2"
158
164
  />
@@ -166,6 +172,7 @@ export const FileSelector2 = ({
166
172
  enableDropArea={enableDropArea}
167
173
  maxFiles={maxFiles}
168
174
  maxSize={maxSize}
175
+ maxTotalSize={maxTotalSize}
169
176
  multiple={multiple}
170
177
  onChange={onChange}
171
178
  onDrop={onDrop}
@@ -11,6 +11,10 @@ export type FileTypesMessageProps = {
11
11
  * Overrides the standard file size message
12
12
  */
13
13
  customSizeMessage?: React.ReactNode;
14
+ /**
15
+ * Overrides the standard total upload size message
16
+ */
17
+ customTotalSizeMessage?: React.ReactNode;
14
18
  /**
15
19
  * Overrides the standard file types message
16
20
  */
@@ -19,19 +23,28 @@ export type FileTypesMessageProps = {
19
23
  * Maximum size per file in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
20
24
  */
21
25
  maxFileSize?: number;
26
+ /**
27
+ * Maximum size of the total upload in bytes. This will be formatted. eg: 1024 * 20 = 20 KB
28
+ */
29
+ maxTotalSize?: number;
22
30
  variant?: 'caption' | 'body2';
23
31
  };
24
32
 
25
33
  export const FileTypesMessage = ({
26
34
  allowedFileTypes = [],
27
35
  customSizeMessage,
36
+ customTotalSizeMessage,
28
37
  customTypesMessage,
29
38
  maxFileSize,
39
+ maxTotalSize,
30
40
  variant = 'caption',
31
41
  }: FileTypesMessageProps) => {
32
42
  const fileSizeMsg =
33
43
  customSizeMessage ||
34
44
  (typeof maxFileSize === 'number' ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null);
45
+ const totalFileSizeMsg =
46
+ customTotalSizeMessage ||
47
+ (typeof maxTotalSize === 'number' ? `Maximum total upload size is ${formatBytes(maxTotalSize)}. ` : null);
35
48
  const fileTypesMsg =
36
49
  customTypesMessage ||
37
50
  (allowedFileTypes.length > 0
@@ -40,6 +53,7 @@ export const FileTypesMessage = ({
40
53
  return (
41
54
  <Typography variant={variant}>
42
55
  {fileSizeMsg}
56
+ {totalFileSizeMsg}
43
57
  {fileTypesMsg}
44
58
  </Typography>
45
59
  );
@@ -8,15 +8,20 @@ export type HeaderMessageProps = {
8
8
  */
9
9
  maxFiles: number;
10
10
  /**
11
- * Maximum combined total size of all files
11
+ * Maximum size of each file
12
12
  */
13
13
  maxSize: number;
14
+ /**
15
+ * Maximum combined total size of all files
16
+ */
17
+ maxTotalSize?: number;
14
18
  };
15
19
 
16
- export const HeaderMessage = ({ maxFiles, maxSize }: HeaderMessageProps) => {
20
+ export const HeaderMessage = ({ maxFiles, maxSize, maxTotalSize }: HeaderMessageProps) => {
17
21
  return (
18
22
  <Typography variant="h6">
19
- Attach up to {maxFiles} file(s), with a maximum individual size of {formatBytes(maxSize)}
23
+ Attach up to {maxFiles} file(s), with a maximum individual size of {formatBytes(maxSize)}{' '}
24
+ {maxTotalSize && `and a maximum total size of ${formatBytes(maxTotalSize)}`}
20
25
  </Typography>
21
26
  );
22
27
  };