@availity/mui-file-selector 0.1.2 → 0.2.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 +28 -0
- package/dist/index.d.mts +12 -4
- package/dist/index.d.ts +12 -4
- package/package.json +9 -8
- package/src/lib/Dropzone.test.tsx +1 -1
- package/src/lib/Dropzone.tsx +122 -109
- package/src/lib/ErrorAlert.test.tsx +11 -0
- package/src/lib/ErrorAlert.tsx +46 -0
- package/src/lib/FileList.test.tsx +42 -8
- package/src/lib/FileList.tsx +44 -23
- package/src/lib/FilePickerBtn.test.tsx +1 -1
- package/src/lib/FilePickerBtn.tsx +23 -29
- package/src/lib/FileSelector.stories.tsx +9 -17
- package/src/lib/FileSelector.tsx +162 -56
- package/src/lib/FileTypesMessage.test.tsx +6 -0
- package/src/lib/FileTypesMessage.tsx +6 -0
- package/src/lib/HeaderMessage.tsx +6 -0
- package/src/lib/UploadProgressBar.test.tsx +17 -0
- package/src/lib/UploadProgressBar.tsx +12 -4
- package/src/lib/useFileDelivery.tsx +11 -2
- package/src/lib/useUploadCore.tsx +8 -12
- package/src/lib/util.ts +1 -1
- package/src/lib/FilePickerInput.md +0 -82
- package/src/lib/FileRow.md +0 -70
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [0.2.0](https://github.com/Availity/element/compare/@availity/mui-file-selector@0.1.3...@availity/mui-file-selector@0.2.0) (2024-12-16)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **mui-file-selector:** add support for dropping files and fix removing ([66af280](https://github.com/Availity/element/commit/66af28076ed7149bc47c6b9758bb9a2e5461f201))
|
|
11
|
+
* **mui-file-selector:** update upload-core ([0da6ec7](https://github.com/Availity/element/commit/0da6ec7672f1d7a884f42d03772ec20249a3309b))
|
|
12
|
+
* pass uploads to onSubmit ([e894489](https://github.com/Availity/element/commit/e8944899a6a6ed4b4bb192a51b707a9aa88b6833))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* add better error handling ([c2db0c0](https://github.com/Availity/element/commit/c2db0c0a1d06bebbb03bc38309d65829487717fb))
|
|
18
|
+
* fixes for file selector ([add8801](https://github.com/Availity/element/commit/add88013c18816ccdaeeb9366768089d68aded7f))
|
|
19
|
+
* fixes for tests ([e112880](https://github.com/Availity/element/commit/e11288079f203464f55f35cbaed47f86f4db3755))
|
|
20
|
+
* **mui-file-selector:** update upload-core ([e24b673](https://github.com/Availity/element/commit/e24b67337a8b1a71441c6954e84a6bd3a1b8e323))
|
|
21
|
+
* update upload-core ([e11a6ad](https://github.com/Availity/element/commit/e11a6ad155743afb221739686b608e581099c37a))
|
|
22
|
+
* update upload-core ([5d820db](https://github.com/Availity/element/commit/5d820db3f146e9e0015daa0f5e9a9d9316cd6807))
|
|
23
|
+
|
|
24
|
+
## [0.1.3](https://github.com/Availity/element/compare/@availity/mui-file-selector@0.1.2...@availity/mui-file-selector@0.1.3) (2024-12-09)
|
|
25
|
+
|
|
26
|
+
### Dependency Updates
|
|
27
|
+
|
|
28
|
+
* `mui-button` updated to version `0.1.2`
|
|
29
|
+
* `mui-form-utils` updated to version `0.1.2`
|
|
30
|
+
* `mui-icon` updated to version `0.1.2`
|
|
31
|
+
* `mui-list` updated to version `0.1.2`
|
|
32
|
+
* `mui-progress` updated to version `0.1.2`
|
|
5
33
|
## [0.1.2](https://github.com/Availity/element/compare/@availity/mui-file-selector@0.1.1...@availity/mui-file-selector@0.1.2) (2024-11-20)
|
|
6
34
|
|
|
7
35
|
### Dependency Updates
|
package/dist/index.d.mts
CHANGED
|
@@ -2,13 +2,21 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import Upload from '@availity/upload-core';
|
|
3
3
|
|
|
4
4
|
type UploadProgressBarProps = {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The upload instance returned by creating a new Upload via @availity/upload-core.
|
|
7
|
+
*/
|
|
6
8
|
upload: Upload;
|
|
7
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Callback function to hook into the onProgress within the Upload instance provided in the upload prop.
|
|
11
|
+
*/
|
|
8
12
|
onProgress?: (upload: Upload) => void;
|
|
9
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Callback function to hook into the onSuccess within the Upload instance provided in the upload prop.
|
|
15
|
+
*/
|
|
10
16
|
onSuccess?: (upload: Upload) => void;
|
|
11
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Callback function to hook into the onError within the Upload instance provided in the upload prop.
|
|
19
|
+
*/
|
|
12
20
|
onError?: (upload: Upload) => void;
|
|
13
21
|
};
|
|
14
22
|
declare const UploadProgressBar: ({ upload, onProgress, onError, onSuccess }: UploadProgressBarProps) => react_jsx_runtime.JSX.Element;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,13 +2,21 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import Upload from '@availity/upload-core';
|
|
3
3
|
|
|
4
4
|
type UploadProgressBarProps = {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The upload instance returned by creating a new Upload via @availity/upload-core.
|
|
7
|
+
*/
|
|
6
8
|
upload: Upload;
|
|
7
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Callback function to hook into the onProgress within the Upload instance provided in the upload prop.
|
|
11
|
+
*/
|
|
8
12
|
onProgress?: (upload: Upload) => void;
|
|
9
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Callback function to hook into the onSuccess within the Upload instance provided in the upload prop.
|
|
15
|
+
*/
|
|
10
16
|
onSuccess?: (upload: Upload) => void;
|
|
11
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Callback function to hook into the onError within the Upload instance provided in the upload prop.
|
|
19
|
+
*/
|
|
12
20
|
onError?: (upload: Upload) => void;
|
|
13
21
|
};
|
|
14
22
|
declare const UploadProgressBar: ({ upload, onProgress, onError, onSuccess }: UploadProgressBarProps) => react_jsx_runtime.JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@availity/mui-file-selector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Availity MUI file-selector Component - part of the @availity/element design system",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -33,23 +33,24 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@availity/api-axios": "^9.0.4",
|
|
36
|
-
"@availity/mui-
|
|
36
|
+
"@availity/mui-alert": "^0.7.0",
|
|
37
|
+
"@availity/mui-button": "^0.6.12",
|
|
37
38
|
"@availity/mui-divider": "^0.4.0",
|
|
38
|
-
"@availity/mui-form-utils": "^0.15.
|
|
39
|
-
"@availity/mui-icon": "^0.11.
|
|
39
|
+
"@availity/mui-form-utils": "^0.15.1",
|
|
40
|
+
"@availity/mui-icon": "^0.11.1",
|
|
40
41
|
"@availity/mui-layout": "^0.2.0",
|
|
41
|
-
"@availity/mui-list": "^0.2.
|
|
42
|
-
"@availity/mui-progress": "^0.4.
|
|
42
|
+
"@availity/mui-list": "^0.2.2",
|
|
43
|
+
"@availity/mui-progress": "^0.4.1",
|
|
43
44
|
"@availity/mui-typography": "^0.2.1",
|
|
44
|
-
"@availity/upload-core": "
|
|
45
|
+
"@availity/upload-core": "7.0.0-alpha.5",
|
|
45
46
|
"@tanstack/react-query": "^4.36.1",
|
|
46
47
|
"react-dropzone": "^11.7.1",
|
|
47
48
|
"react-hook-form": "^7.51.3",
|
|
49
|
+
"tus-js-client": "4.2.3",
|
|
48
50
|
"uuid": "^9.0.1"
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
53
|
"@mui/material": "^5.15.15",
|
|
52
|
-
"@types/tus-js-client": "^1.8.0",
|
|
53
54
|
"react": "18.2.0",
|
|
54
55
|
"react-dom": "18.2.0",
|
|
55
56
|
"tsup": "^8.0.2",
|
|
@@ -18,7 +18,7 @@ describe('Dropzone', () => {
|
|
|
18
18
|
render(
|
|
19
19
|
<QueryClientProvider client={client}>
|
|
20
20
|
<TestForm>
|
|
21
|
-
<Dropzone name="test"
|
|
21
|
+
<Dropzone name="test" maxSize={1000} setTotalSize={jest.fn()} />
|
|
22
22
|
</TestForm>
|
|
23
23
|
</QueryClientProvider>
|
|
24
24
|
);
|
package/src/lib/Dropzone.tsx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useDropzone, FileRejection
|
|
3
|
-
import { v4 as uuid } from 'uuid';
|
|
1
|
+
import { Dispatch, MouseEvent, useCallback, ChangeEvent } from 'react';
|
|
2
|
+
import { useDropzone, FileRejection } from 'react-dropzone';
|
|
4
3
|
import { Divider } from '@availity/mui-divider';
|
|
5
4
|
import { CloudDownloadIcon } from '@availity/mui-icon';
|
|
6
5
|
import { Box, Stack } from '@availity/mui-layout';
|
|
7
6
|
import { Typography } from '@availity/mui-typography';
|
|
8
|
-
import Upload, { Options } from '@availity/upload-core';
|
|
9
7
|
|
|
10
8
|
import { FilePickerBtn } from './FilePickerBtn';
|
|
9
|
+
import { useFormContext } from 'react-hook-form';
|
|
11
10
|
|
|
12
11
|
const outerBoxStyles = {
|
|
13
12
|
backgroundColor: 'background.canvas',
|
|
@@ -21,141 +20,158 @@ const innerBoxStyles = {
|
|
|
21
20
|
height: '100%',
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
/** Counter for creating unique id */
|
|
24
|
+
const createCounter = () => {
|
|
25
|
+
let id = 0;
|
|
26
|
+
const increment = () => (id += 1);
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
increment,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const counter = createCounter();
|
|
25
34
|
|
|
26
35
|
export type DropzoneProps = {
|
|
36
|
+
/**
|
|
37
|
+
* Name given to the input field. Used by react-hook-form
|
|
38
|
+
*/
|
|
27
39
|
name: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
allowedFileNameCharacters?: string;
|
|
40
|
+
/**
|
|
41
|
+
* List of allowed file extensions (e.g. ['.pdf', '.doc']). Each extension must start with a dot
|
|
42
|
+
*/
|
|
32
43
|
allowedFileTypes?: `.${string}`[];
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Whether the dropzone is disabled
|
|
46
|
+
*/
|
|
35
47
|
disabled?: boolean;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
isCloud?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Maximum number of files that can be uploaded
|
|
50
|
+
*/
|
|
40
51
|
maxFiles?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Maximum size of each file in bytes
|
|
54
|
+
*/
|
|
41
55
|
maxSize?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Whether multiple file selection is allowed
|
|
58
|
+
*/
|
|
42
59
|
multiple?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Handler called when the file input's value changes
|
|
62
|
+
*/
|
|
43
63
|
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Handler called when the file picker button is clicked
|
|
66
|
+
*/
|
|
44
67
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Callback to handle rejected files that don't meet validation criteria
|
|
70
|
+
*/
|
|
71
|
+
setFileRejections?: (fileRejections: (FileRejection & { id: number })[]) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Callback to update the total size of all uploaded files
|
|
74
|
+
*/
|
|
75
|
+
setTotalSize: Dispatch<React.SetStateAction<number>>;
|
|
49
76
|
};
|
|
50
77
|
|
|
78
|
+
// The types below were props used in the availity-react implementation.
|
|
79
|
+
// Perserving this here in case it needs to be added back
|
|
80
|
+
// deliverFileOnSubmit?: boolean;
|
|
81
|
+
// deliveryChannel?: string;
|
|
82
|
+
// fileDeliveryMetadata?: Record<string, unknown> | ((file: Upload) => Record<string, unknown>);
|
|
83
|
+
// onDeliveryError?: (responses: unknown[]) => void;
|
|
84
|
+
// onDeliverySuccess?: (responses: unknown[]) => void;
|
|
85
|
+
// onFileDelivery?: (upload: Upload) => void;
|
|
86
|
+
|
|
51
87
|
export const Dropzone = ({
|
|
52
|
-
allowedFileNameCharacters,
|
|
53
88
|
allowedFileTypes = [],
|
|
54
|
-
bucketId,
|
|
55
|
-
clientId,
|
|
56
|
-
customerId,
|
|
57
|
-
deliveryChannel,
|
|
58
|
-
// deliverFileOnSubmit,
|
|
59
|
-
fileDeliveryMetadata,
|
|
60
89
|
disabled,
|
|
61
|
-
endpoint,
|
|
62
|
-
getDropRejectionMessages,
|
|
63
|
-
isCloud,
|
|
64
90
|
maxFiles,
|
|
65
91
|
maxSize,
|
|
66
92
|
multiple,
|
|
67
93
|
name,
|
|
68
94
|
onChange,
|
|
69
95
|
onClick,
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
setFileRejections,
|
|
97
|
+
setTotalSize,
|
|
72
98
|
}: DropzoneProps) => {
|
|
73
|
-
const
|
|
74
|
-
const [files, setFiles] = useState<Upload[]>([]);
|
|
99
|
+
const { setValue, watch } = useFormContext();
|
|
75
100
|
|
|
76
|
-
const
|
|
77
|
-
(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
const validator = useCallback(
|
|
102
|
+
(file: File) => {
|
|
103
|
+
const previous: File[] = watch(name) ?? [];
|
|
104
|
+
|
|
105
|
+
const isDuplicate = previous.some((prev) => prev.name === file.name);
|
|
106
|
+
if (isDuplicate) {
|
|
107
|
+
return {
|
|
108
|
+
code: 'duplicate-name',
|
|
109
|
+
message: 'A file with this name already exists',
|
|
110
|
+
};
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
fileTypes: allowedFileTypes,
|
|
94
|
-
maxSize,
|
|
95
|
-
allowedFileNameCharacters,
|
|
113
|
+
const hasMaxFiles = maxFiles && previous.length >= maxFiles;
|
|
114
|
+
if (hasMaxFiles) {
|
|
115
|
+
return {
|
|
116
|
+
code: 'too-many-files',
|
|
117
|
+
message: `Too many files. You may only upload ${maxFiles} file(s).`,
|
|
96
118
|
};
|
|
119
|
+
}
|
|
97
120
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// @ts-ignore
|
|
112
|
-
upload.errorMessage = file.dropRejectionMessage;
|
|
113
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
114
|
-
// @ts-ignore
|
|
115
|
-
} else if (maxSize && totalSize + newFilesTotalSize + upload.file.size > maxSize) {
|
|
116
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
117
|
-
// @ts-ignore
|
|
118
|
-
upload.errorMessage = 'Total documents size is too large';
|
|
119
|
-
} else {
|
|
120
|
-
upload.start();
|
|
121
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
122
|
-
// @ts-ignore
|
|
123
|
-
newFilesTotalSize += upload.file.size;
|
|
124
|
-
}
|
|
125
|
-
if (onFileDelivery) {
|
|
126
|
-
onFileDelivery(upload);
|
|
127
|
-
} else if (deliveryChannel && fileDeliveryMetadata) {
|
|
128
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
129
|
-
// @ts-ignore
|
|
130
|
-
// upload.onSuccess.push(() => {
|
|
131
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
132
|
-
// @ts-ignore
|
|
133
|
-
// if (upload?.references?.[0]) {
|
|
134
|
-
// allow form to revalidate when upload is complete
|
|
135
|
-
// setFieldTouched(name, true);
|
|
136
|
-
// deliver upon upload complete, not form submit
|
|
137
|
-
// if (!deliverFileOnSubmit) {
|
|
138
|
-
// callFileDelivery(upload);
|
|
139
|
-
// }
|
|
140
|
-
// }
|
|
141
|
-
// });
|
|
142
|
-
}
|
|
121
|
+
return null;
|
|
122
|
+
},
|
|
123
|
+
[maxFiles]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const onDrop = useCallback(
|
|
127
|
+
(acceptedFiles: File[], fileRejections: (FileRejection & { id: number })[]) => {
|
|
128
|
+
let newSize = 0;
|
|
129
|
+
for (const file of acceptedFiles) {
|
|
130
|
+
newSize += file.size;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setTotalSize((prev) => prev + newSize);
|
|
143
134
|
|
|
144
|
-
|
|
145
|
-
});
|
|
135
|
+
const previous = watch(name) ?? [];
|
|
146
136
|
|
|
147
|
-
// Set
|
|
148
|
-
|
|
137
|
+
// Set accepted files to form context
|
|
138
|
+
setValue(name, previous.concat(acceptedFiles));
|
|
149
139
|
|
|
150
|
-
if (
|
|
140
|
+
if (fileRejections.length > 0) {
|
|
141
|
+
for (const rejection of fileRejections) {
|
|
142
|
+
rejection.id = counter.increment();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (setFileRejections) setFileRejections(fileRejections);
|
|
151
147
|
},
|
|
152
|
-
[
|
|
148
|
+
[setFileRejections]
|
|
153
149
|
);
|
|
154
150
|
|
|
155
|
-
const { getRootProps, getInputProps } = useDropzone({ onDrop });
|
|
156
|
-
|
|
157
151
|
const accept = allowedFileTypes.join(',');
|
|
158
152
|
|
|
153
|
+
const { getRootProps, getInputProps } = useDropzone({
|
|
154
|
+
onDrop,
|
|
155
|
+
maxSize,
|
|
156
|
+
maxFiles,
|
|
157
|
+
disabled,
|
|
158
|
+
multiple,
|
|
159
|
+
accept,
|
|
160
|
+
validator,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const inputProps = getInputProps({
|
|
164
|
+
multiple,
|
|
165
|
+
accept,
|
|
166
|
+
onChange,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
170
|
+
if (inputProps.onChange) {
|
|
171
|
+
inputProps.onChange(event);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
159
175
|
return (
|
|
160
176
|
<Box sx={outerBoxStyles} {...getRootProps()}>
|
|
161
177
|
<Box sx={innerBoxStyles}>
|
|
@@ -171,11 +187,8 @@ export const Dropzone = ({
|
|
|
171
187
|
disabled={disabled}
|
|
172
188
|
maxSize={maxSize}
|
|
173
189
|
onClick={onClick}
|
|
174
|
-
inputProps={
|
|
175
|
-
|
|
176
|
-
accept,
|
|
177
|
-
onChange,
|
|
178
|
-
})}
|
|
190
|
+
inputProps={inputProps}
|
|
191
|
+
onChange={handleOnChange}
|
|
179
192
|
/>
|
|
180
193
|
</>
|
|
181
194
|
</Stack>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { ErrorAlert } from './ErrorAlert';
|
|
4
|
+
|
|
5
|
+
describe('ErrorAlert', () => {
|
|
6
|
+
test('should render error message', () => {
|
|
7
|
+
render(<ErrorAlert id={0} errors={[{ code: 'test', message: 'example' }]} fileName="file" onClose={jest.fn()} />);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByText('Error: file')).toBeDefined();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Alert, AlertTitle } from '@availity/mui-alert';
|
|
2
|
+
import type { FileRejection } from 'react-dropzone';
|
|
3
|
+
|
|
4
|
+
const codes: Record<string, string> = {
|
|
5
|
+
'file-too-large': 'File exceeds maximum size',
|
|
6
|
+
'file-invalid-type': 'File has an invalid type',
|
|
7
|
+
'file-too-small': 'File is smaller than minimum size',
|
|
8
|
+
'too-many-file': 'Too many files',
|
|
9
|
+
'duplicate-name': 'Duplicate file selected',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ErrorAlertProps = {
|
|
13
|
+
/**
|
|
14
|
+
* Array of file rejection errors
|
|
15
|
+
*/
|
|
16
|
+
errors: FileRejection['errors'];
|
|
17
|
+
/**
|
|
18
|
+
* Name of the file that encountered errors
|
|
19
|
+
*/
|
|
20
|
+
fileName: string;
|
|
21
|
+
/**
|
|
22
|
+
* Unique identifier for the error alert
|
|
23
|
+
*/
|
|
24
|
+
id: number;
|
|
25
|
+
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const ErrorAlert = ({ errors, fileName, id, onClose }: ErrorAlertProps) => {
|
|
30
|
+
if (errors.length === 0) return null;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{errors.map((error) => {
|
|
35
|
+
return (
|
|
36
|
+
<Alert severity="error" onClose={onClose} key={`${id}-${error.code}`}>
|
|
37
|
+
<AlertTitle>
|
|
38
|
+
{codes[error.code] || 'Error'}: {fileName}
|
|
39
|
+
</AlertTitle>
|
|
40
|
+
{error.message}
|
|
41
|
+
</Alert>
|
|
42
|
+
);
|
|
43
|
+
})}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -1,16 +1,50 @@
|
|
|
1
|
-
import { render } from '@testing-library/react';
|
|
1
|
+
import { screen, render, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
3
|
|
|
3
4
|
import { FileList } from './FileList';
|
|
4
5
|
|
|
5
6
|
describe('FileList', () => {
|
|
6
|
-
test('should render successfully', () => {
|
|
7
|
+
test('should render successfully', async () => {
|
|
8
|
+
const mockFile = new File(['file content'], 'mock.txt', { type: 'text/plain' });
|
|
9
|
+
|
|
7
10
|
render(
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
12
|
+
<FileList
|
|
13
|
+
files={[mockFile]}
|
|
14
|
+
options={{ bucketId: '123', customerId: '123', clientId: '123' }}
|
|
15
|
+
onRemoveFile={() => {
|
|
16
|
+
// noop
|
|
17
|
+
}}
|
|
18
|
+
/>
|
|
19
|
+
</QueryClientProvider>
|
|
14
20
|
);
|
|
21
|
+
|
|
22
|
+
await waitFor(() => {
|
|
23
|
+
expect(screen.getByText('mock.txt')).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should call onRemoveFile', async () => {
|
|
28
|
+
const mockRemove = jest.fn();
|
|
29
|
+
const mockFile = new File(['file content'], 'mock.txt', { type: 'text/plain' });
|
|
30
|
+
|
|
31
|
+
render(
|
|
32
|
+
<QueryClientProvider client={new QueryClient()}>
|
|
33
|
+
<FileList
|
|
34
|
+
files={[mockFile]}
|
|
35
|
+
options={{ bucketId: '123', customerId: '123', clientId: '123' }}
|
|
36
|
+
onRemoveFile={(id) => {
|
|
37
|
+
mockRemove(id);
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
</QueryClientProvider>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByText('mock.txt')).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
fireEvent.click(screen.getByRole('button'));
|
|
48
|
+
expect(mockRemove).toHaveBeenCalled();
|
|
15
49
|
});
|
|
16
50
|
});
|
package/src/lib/FileList.tsx
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
|
-
import type Upload from '@availity/upload-core';
|
|
1
|
+
import type { default as Upload, UploadOptions } from '@availity/upload-core';
|
|
2
2
|
import { List, ListItem, ListItemText, ListItemIcon, ListItemButton } from '@availity/mui-list';
|
|
3
3
|
import { DeleteIcon, FileIcon } from '@availity/mui-icon';
|
|
4
4
|
import { Grid } from '@availity/mui-layout';
|
|
5
5
|
|
|
6
6
|
import { UploadProgressBar } from './UploadProgressBar';
|
|
7
7
|
import { formatBytes, getFileExtIcon } from './util';
|
|
8
|
+
import { useUploadCore } from './useUploadCore';
|
|
8
9
|
|
|
9
10
|
type FileRowProps = {
|
|
10
|
-
/** The
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
11
|
+
/** The File object containing information about the uploaded file */
|
|
12
|
+
file: File;
|
|
13
|
+
/**
|
|
14
|
+
* Callback function called when a file is removed
|
|
15
|
+
* @param id - The unique identifier of the file being removed
|
|
16
|
+
* @param upload - The Upload instance associated with the file
|
|
17
|
+
*/
|
|
18
|
+
onRemoveFile: (id: string, upload: Upload) => void;
|
|
19
|
+
/** Configuration options for the upload process */
|
|
20
|
+
options: UploadOptions;
|
|
14
21
|
};
|
|
15
22
|
|
|
16
|
-
const FileRow = ({
|
|
17
|
-
const { ext, icon } = getFileExtIcon(
|
|
23
|
+
const FileRow = ({ file, options, onRemoveFile }: FileRowProps) => {
|
|
24
|
+
const { ext, icon } = getFileExtIcon(file.name);
|
|
18
25
|
console.log('ext, icon:', ext, icon);
|
|
19
26
|
|
|
27
|
+
const { data: upload } = useUploadCore(file, options);
|
|
28
|
+
|
|
29
|
+
if (!upload) return null;
|
|
30
|
+
|
|
20
31
|
return (
|
|
21
|
-
|
|
32
|
+
<>
|
|
22
33
|
<Grid container spacing={2} alignItems="center" justifyContent="space-between" width="100%">
|
|
23
34
|
<Grid xs={1}>
|
|
24
35
|
<ListItemIcon>
|
|
@@ -35,35 +46,45 @@ const FileRow = ({ upload, onRemoveFile }: FileRowProps) => {
|
|
|
35
46
|
<UploadProgressBar upload={upload} />
|
|
36
47
|
</Grid>
|
|
37
48
|
<Grid xs={1}>
|
|
38
|
-
<ListItemButton
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
>
|
|
49
|
+
<ListItemButton
|
|
50
|
+
onClick={() => {
|
|
51
|
+
onRemoveFile(upload.id, upload);
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<ListItemIcon>
|
|
44
55
|
<DeleteIcon />
|
|
45
56
|
</ListItemIcon>
|
|
46
57
|
</ListItemButton>
|
|
47
58
|
</Grid>
|
|
48
59
|
</Grid>
|
|
49
|
-
|
|
60
|
+
</>
|
|
50
61
|
);
|
|
51
62
|
};
|
|
52
63
|
|
|
53
64
|
export type FileListProps = {
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Array of File objects to be displayed in the list
|
|
67
|
+
*/
|
|
68
|
+
files: File[];
|
|
69
|
+
/**
|
|
70
|
+
* Callback function called when a file is removed from the list
|
|
71
|
+
* @param id - The unique identifier of the file being removed
|
|
72
|
+
* @param upload - The Upload instance associated with the file
|
|
73
|
+
*/
|
|
74
|
+
onRemoveFile: (id: string, upload: Upload) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Configuration options applied to all file uploads in the list
|
|
77
|
+
*/
|
|
78
|
+
options: UploadOptions;
|
|
58
79
|
};
|
|
59
80
|
|
|
60
|
-
export const FileList = ({
|
|
61
|
-
if (
|
|
81
|
+
export const FileList = ({ files, options, onRemoveFile }: FileListProps) => {
|
|
82
|
+
if (files.length === 0) return null;
|
|
62
83
|
|
|
63
84
|
return (
|
|
64
85
|
<List>
|
|
65
|
-
{
|
|
66
|
-
return <FileRow key={
|
|
86
|
+
{files.map((file) => {
|
|
87
|
+
return <FileRow key={file.name} file={file} options={options} onRemoveFile={onRemoveFile} />;
|
|
67
88
|
})}
|
|
68
89
|
</List>
|
|
69
90
|
);
|