@popsure/dirty-swan 0.27.28 → 0.27.30
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/dist/index.css +12 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +199 -131
- package/dist/index.js.map +1 -1
- package/dist/lib/components/multiDropzone/UploadFileCell/index.d.ts +6 -5
- package/dist/lib/components/multiDropzone/icons/index.d.ts +6 -20
- package/dist/lib/components/multiDropzone/index.d.ts +8 -18
- package/dist/lib/components/multiDropzone/index.test.d.ts +1 -0
- package/dist/lib/components/multiDropzone/types.d.ts +31 -0
- package/dist/lib/components/multiDropzone/utils/index.d.ts +11 -0
- package/dist/lib/scss/utils/_index.scss +12 -0
- package/dist/lib/util/formatBytes/index.d.ts +1 -0
- package/package.json +3 -2
- package/src/lib/components/input/currency/index.stories.mdx +2 -0
- package/src/lib/components/input/index.tsx +1 -1
- package/src/lib/components/input/style.module.scss +0 -6
- package/src/lib/components/multiDropzone/UploadFileCell/index.tsx +44 -64
- package/src/lib/components/multiDropzone/UploadFileCell/style.module.scss +3 -9
- package/src/lib/components/multiDropzone/icons/eye.svg +10 -3
- package/src/lib/components/multiDropzone/icons/file-error.svg +4 -0
- package/src/lib/components/multiDropzone/icons/file-upload.svg +4 -0
- package/src/lib/components/multiDropzone/icons/file.svg +4 -0
- package/src/lib/components/multiDropzone/icons/index.ts +12 -44
- package/src/lib/components/multiDropzone/icons/trash-error.svg +6 -0
- package/src/lib/components/multiDropzone/icons/trash.svg +5 -5
- package/src/lib/components/multiDropzone/icons/upload-small.svg +12 -0
- package/src/lib/components/multiDropzone/index.stories.mdx +60 -2
- package/src/lib/components/multiDropzone/index.test.tsx +235 -0
- package/src/lib/components/multiDropzone/index.tsx +98 -69
- package/src/lib/components/multiDropzone/types.ts +36 -0
- package/src/lib/components/multiDropzone/utils/index.test.ts +112 -0
- package/src/lib/components/multiDropzone/utils/index.ts +69 -0
- package/src/lib/scss/utils/_index.scss +12 -0
- package/src/lib/util/formatBytes/index.test.ts +19 -0
- package/src/lib/util/formatBytes/index.ts +13 -0
- package/src/setupTests.js +2 -0
- package/src/lib/components/multiDropzone/icons/bmp-complete.svg +0 -10
- package/src/lib/components/multiDropzone/icons/bmp.svg +0 -10
- package/src/lib/components/multiDropzone/icons/doc-complete.svg +0 -11
- package/src/lib/components/multiDropzone/icons/doc.svg +0 -11
- package/src/lib/components/multiDropzone/icons/docx-complete.svg +0 -12
- package/src/lib/components/multiDropzone/icons/docx.svg +0 -12
- package/src/lib/components/multiDropzone/icons/generic-complete.svg +0 -4
- package/src/lib/components/multiDropzone/icons/generic-error.svg +0 -7
- package/src/lib/components/multiDropzone/icons/generic.svg +0 -4
- package/src/lib/components/multiDropzone/icons/heic-complete.svg +0 -11
- package/src/lib/components/multiDropzone/icons/heic.svg +0 -11
- package/src/lib/components/multiDropzone/icons/jpeg-complete.svg +0 -11
- package/src/lib/components/multiDropzone/icons/jpeg.svg +0 -11
- package/src/lib/components/multiDropzone/icons/jpg-complete.svg +0 -10
- package/src/lib/components/multiDropzone/icons/jpg.svg +0 -10
- package/src/lib/components/multiDropzone/icons/pdf-complete.svg +0 -8
- package/src/lib/components/multiDropzone/icons/pdf.svg +0 -8
- package/src/lib/components/multiDropzone/icons/png-complete.svg +0 -10
- package/src/lib/components/multiDropzone/icons/png.svg +0 -10
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
2
|
+
import { act } from 'react-dom/test-utils';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
|
|
5
|
+
import MultiDropzone, { MultiDropzoneProps } from '.';
|
|
6
|
+
|
|
7
|
+
const mockOnFileSelect = jest.fn();
|
|
8
|
+
const mockOnRemoveFile = jest.fn();
|
|
9
|
+
const file = new File(['DummyFile'], 'dummy.png', { type: 'image/png' });
|
|
10
|
+
|
|
11
|
+
const inputTestId = "ds-drop-input";
|
|
12
|
+
const spinnerTestId = "ds-filecell-spinner";
|
|
13
|
+
const progressbarTestId = "ds-filecell-progressbar";
|
|
14
|
+
const uploadedFilesMock = {
|
|
15
|
+
id: "123",
|
|
16
|
+
name: "File name",
|
|
17
|
+
progress: 100,
|
|
18
|
+
type: "jpg",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const setup = ({
|
|
22
|
+
uploadedFiles = [],
|
|
23
|
+
uploading = false,
|
|
24
|
+
...rest
|
|
25
|
+
}: Partial<MultiDropzoneProps>) => {
|
|
26
|
+
return render(
|
|
27
|
+
<MultiDropzone
|
|
28
|
+
{...rest}
|
|
29
|
+
uploadedFiles={uploadedFiles}
|
|
30
|
+
uploading={uploading}
|
|
31
|
+
onFileSelect={mockOnFileSelect}
|
|
32
|
+
onRemoveFile={mockOnRemoveFile}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('MultiDropzone component', () => {
|
|
38
|
+
it("should call onFileSelect on files change", async () => {
|
|
39
|
+
const screen = setup({});
|
|
40
|
+
const input = screen.getByTestId(inputTestId);
|
|
41
|
+
const files = [file, file];
|
|
42
|
+
|
|
43
|
+
await act(async () => {
|
|
44
|
+
fireEvent.change(input, { target: { files } });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(mockOnFileSelect).toHaveBeenCalledWith(files);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Error states', () => {
|
|
51
|
+
it("should show max files error message", () => {
|
|
52
|
+
const screen = setup({
|
|
53
|
+
maxFiles: 1,
|
|
54
|
+
uploadedFiles: [uploadedFilesMock, {
|
|
55
|
+
...uploadedFilesMock,
|
|
56
|
+
id: "222"
|
|
57
|
+
}],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText("Too many files.")).toBeVisible();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should show max file size error message", async () => {
|
|
64
|
+
const screen = setup({maxSize: 10 });
|
|
65
|
+
const input = screen.getByTestId(inputTestId);
|
|
66
|
+
const bigFile = file;
|
|
67
|
+
Object.defineProperty(bigFile, 'size', { value: 1024 });
|
|
68
|
+
|
|
69
|
+
await act(async () => {
|
|
70
|
+
fireEvent.change(input, { target: { files: [bigFile] } });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByText("File is too large. It must be less than 10 Bytes.")
|
|
75
|
+
).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should show wrong filetype error message", async () => {
|
|
79
|
+
const screen = setup({ accept: "document" });
|
|
80
|
+
const input = screen.getByTestId(inputTestId);
|
|
81
|
+
|
|
82
|
+
await act(async () => {
|
|
83
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
screen.getByText("File type must be one of DOC, DOCX, PDF")
|
|
88
|
+
).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should remove wrong filetype error message", async () => {
|
|
92
|
+
const screen = setup({ accept: "document" });
|
|
93
|
+
const input = screen.getByTestId(inputTestId);
|
|
94
|
+
|
|
95
|
+
await act(async () => {
|
|
96
|
+
fireEvent.change(input, { target: { files: [file] } });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
screen.getByAltText("remove").click();
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
screen.queryByText("File type must be one of DOC, DOCX, PDF")
|
|
103
|
+
).not.toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Copy text', () => {
|
|
108
|
+
it("should show uploader text", () => {
|
|
109
|
+
const screen = setup({});
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText("Choose file or drag & drop")).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should show uploader text translated", () => {
|
|
115
|
+
const instructionsText = "Drag drop file";
|
|
116
|
+
const screen = setup({
|
|
117
|
+
textOverrides: { instructionsText }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(screen.getByText(instructionsText)).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should show image accept file type label", () => {
|
|
124
|
+
const screen = setup({ accept: "image" });
|
|
125
|
+
|
|
126
|
+
expect(
|
|
127
|
+
screen.getByText("Supports HEIC, BMP, JPEG, JPG, PNG")
|
|
128
|
+
).toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should show document accept file type label", () => {
|
|
132
|
+
const screen = setup({ accept: "document" });
|
|
133
|
+
|
|
134
|
+
expect(
|
|
135
|
+
screen.getByText("Supports DOC, DOCX, PDF")
|
|
136
|
+
).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should custom document accept file type label", () => {
|
|
140
|
+
const screen = setup({ accept: {
|
|
141
|
+
"application/pdf": [".pdf"],
|
|
142
|
+
"image/jpg": [".jpg"],
|
|
143
|
+
} });
|
|
144
|
+
|
|
145
|
+
expect(
|
|
146
|
+
screen.getByText("Supports PDF, JPG")
|
|
147
|
+
).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should show disabled text if is uploading", () => {
|
|
151
|
+
const screen = setup({ uploading: true });
|
|
152
|
+
|
|
153
|
+
expect(
|
|
154
|
+
screen.getByText("Please wait while uploading file...")
|
|
155
|
+
).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Uploaded files', () => {
|
|
160
|
+
it("should show uploaded files", () => {
|
|
161
|
+
const screen = setup({
|
|
162
|
+
uploadedFiles: [uploadedFilesMock],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(
|
|
166
|
+
screen.getByText(uploadedFilesMock.name)
|
|
167
|
+
).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should call onRemoveFile with uploaded file id", () => {
|
|
171
|
+
const screen = setup({
|
|
172
|
+
uploadedFiles: [uploadedFilesMock],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
screen.getByAltText("remove").click();
|
|
176
|
+
|
|
177
|
+
expect(mockOnRemoveFile).toBeCalledWith(uploadedFilesMock.id);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should show uploaded file with uploading label", () => {
|
|
181
|
+
const screen = setup({
|
|
182
|
+
uploadedFiles: [{ ...uploadedFilesMock, progress: 50 }],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(screen.getByText("Uploading...")).toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should show uploaded file with progress bar", () => {
|
|
189
|
+
const screen = setup({
|
|
190
|
+
uploadedFiles: [{
|
|
191
|
+
...uploadedFilesMock,
|
|
192
|
+
progress: 50,
|
|
193
|
+
}],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(screen.getByTestId(progressbarTestId)).toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should show uploaded file with no progress bar", () => {
|
|
200
|
+
const screen = setup({
|
|
201
|
+
uploadedFiles: [{
|
|
202
|
+
...uploadedFilesMock,
|
|
203
|
+
progress: 50,
|
|
204
|
+
showProgressBar: false
|
|
205
|
+
}],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(screen.queryByTestId(progressbarTestId)).not.toBeInTheDocument();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should show uploaded file with loading spinner", () => {
|
|
212
|
+
const screen = setup({
|
|
213
|
+
uploadedFiles: [{
|
|
214
|
+
...uploadedFilesMock,
|
|
215
|
+
progress: 50,
|
|
216
|
+
showLoadingSpinner: true
|
|
217
|
+
}],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(screen.getByTestId(spinnerTestId)).toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should show uploaded file with no loading spinner", () => {
|
|
224
|
+
const screen = setup({
|
|
225
|
+
uploadedFiles: [{
|
|
226
|
+
...uploadedFilesMock,
|
|
227
|
+
progress: 50,
|
|
228
|
+
showLoadingSpinner: false
|
|
229
|
+
}],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(screen.queryByTestId(spinnerTestId)).not.toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -1,84 +1,90 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
|
-
import { useDropzone, FileRejection } from 'react-dropzone';
|
|
3
2
|
import classnames from 'classnames';
|
|
3
|
+
import { useDropzone, FileRejection } from 'react-dropzone';
|
|
4
4
|
import AnimateHeight from 'react-animate-height';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
6
|
import styles from './style.module.scss';
|
|
6
7
|
import icons from './icons/index'; // TODO: inline all of the svgs
|
|
7
8
|
import UploadFileCell from './UploadFileCell';
|
|
9
|
+
import {
|
|
10
|
+
formatAcceptFileList,
|
|
11
|
+
getErrorMessage,
|
|
12
|
+
getFormattedAcceptObject,
|
|
13
|
+
getUploadStatus
|
|
14
|
+
} from './utils';
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
| 'docx'
|
|
19
|
-
| 'pdf';
|
|
20
|
-
|
|
21
|
-
const getUploadStatus = (progress: number, error?: string): UploadStatus => {
|
|
22
|
-
if (error) {
|
|
23
|
-
return 'ERROR';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (progress < 100) {
|
|
27
|
-
return 'UPLOADING';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return 'COMPLETE';
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export interface UploadedFile {
|
|
34
|
-
id: string;
|
|
35
|
-
name: string;
|
|
36
|
-
type: FileType | string;
|
|
37
|
-
previewUrl?: string;
|
|
38
|
-
progress: number;
|
|
39
|
-
error?: string;
|
|
40
|
-
}
|
|
16
|
+
import {
|
|
17
|
+
AcceptType,
|
|
18
|
+
ErrorMessage,
|
|
19
|
+
FileType,
|
|
20
|
+
TextOverrides,
|
|
21
|
+
UploadedFile,
|
|
22
|
+
UploadStatus
|
|
23
|
+
} from './types';
|
|
24
|
+
import { formatBytes } from '../../util/formatBytes';
|
|
41
25
|
|
|
42
|
-
interface
|
|
26
|
+
interface MultiDropzoneProps {
|
|
27
|
+
accept?: AcceptType;
|
|
43
28
|
onFileSelect: (files: File[]) => void;
|
|
44
29
|
uploadedFiles: UploadedFile[];
|
|
45
30
|
uploading: boolean;
|
|
46
31
|
onRemoveFile: (id: string) => void;
|
|
47
32
|
isCondensed?: boolean;
|
|
48
33
|
maxFiles?: number;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
currentlyUploadingText?: string;
|
|
52
|
-
supportsText?: string;
|
|
53
|
-
};
|
|
34
|
+
maxSize?: number;
|
|
35
|
+
textOverrides?: TextOverrides;
|
|
54
36
|
}
|
|
55
37
|
|
|
56
|
-
|
|
38
|
+
const MultiDropZone = ({
|
|
39
|
+
accept,
|
|
57
40
|
uploadedFiles,
|
|
58
41
|
onFileSelect,
|
|
59
42
|
uploading,
|
|
60
43
|
onRemoveFile,
|
|
61
44
|
isCondensed = false,
|
|
62
45
|
maxFiles = 0,
|
|
46
|
+
maxSize,
|
|
63
47
|
textOverrides,
|
|
64
|
-
}:
|
|
65
|
-
const [
|
|
48
|
+
}: MultiDropzoneProps) => {
|
|
49
|
+
const [errors, setErrors] = useState<ErrorMessage[]>([]);
|
|
50
|
+
const formattedAccept = getFormattedAcceptObject(accept);
|
|
51
|
+
const fileList = formatAcceptFileList(formattedAccept);
|
|
52
|
+
const maxSizePlaceholder = maxSize && maxSize > 0
|
|
53
|
+
? `${textOverrides?.sizeUpToText || "up to"} ${formatBytes(maxSize)}`
|
|
54
|
+
: "";
|
|
55
|
+
const placeholder = `${textOverrides?.supportsTextShort || "Supports"} ${fileList || "JPEG, PNG, PDF"} ${maxSizePlaceholder}`;
|
|
56
|
+
const isOverMaxFiles = maxFiles > 0 && uploadedFiles.length > maxFiles;
|
|
57
|
+
|
|
58
|
+
const removeError = (removeId: string) => (
|
|
59
|
+
setErrors(errors.filter(({ id }) => id !== removeId))
|
|
60
|
+
);
|
|
66
61
|
|
|
67
62
|
const onDrop = useCallback(
|
|
68
63
|
(acceptedFiles: File[], filesRejected: FileRejection[]) => {
|
|
69
|
-
setError('');
|
|
70
|
-
|
|
71
|
-
if (filesRejected.length > 0) {
|
|
72
|
-
setError(filesRejected[0].errors[0].message);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
64
|
onFileSelect(acceptedFiles);
|
|
65
|
+
|
|
66
|
+
setErrors((previousErrors) => ([
|
|
67
|
+
...previousErrors,
|
|
68
|
+
...filesRejected.map(({ errors }) => ({
|
|
69
|
+
id: uuidv4(),
|
|
70
|
+
message: getErrorMessage(
|
|
71
|
+
errors[0],
|
|
72
|
+
{ fileList, maxSize },
|
|
73
|
+
textOverrides
|
|
74
|
+
),
|
|
75
|
+
}))
|
|
76
|
+
]));
|
|
77
77
|
},
|
|
78
|
-
[onFileSelect]
|
|
78
|
+
[fileList, maxSize, onFileSelect, textOverrides]
|
|
79
79
|
);
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
|
|
82
|
+
const { getRootProps, getInputProps } = useDropzone({
|
|
83
|
+
accept: formattedAccept,
|
|
84
|
+
disabled: uploading,
|
|
85
|
+
maxSize,
|
|
86
|
+
onDrop,
|
|
87
|
+
});
|
|
82
88
|
|
|
83
89
|
return (
|
|
84
90
|
<div className={styles.container}>
|
|
@@ -91,10 +97,13 @@ export default ({
|
|
|
91
97
|
)}
|
|
92
98
|
{...getRootProps()}
|
|
93
99
|
>
|
|
94
|
-
<input
|
|
100
|
+
<input
|
|
101
|
+
data-testid="ds-drop-input"
|
|
102
|
+
{...getInputProps()}
|
|
103
|
+
/>
|
|
95
104
|
<img
|
|
96
105
|
className={isCondensed ? styles.img : ''}
|
|
97
|
-
src={icons.uploadIcon}
|
|
106
|
+
src={isCondensed ? icons.uploadSmallIcon : icons.uploadIcon}
|
|
98
107
|
alt="purple cloud with an arrow"
|
|
99
108
|
/>
|
|
100
109
|
<div className={`p-h4 mt8 ${isCondensed ? styles.textInline : ''}`}>
|
|
@@ -104,28 +113,48 @@ export default ({
|
|
|
104
113
|
: textOverrides?.instructionsText || 'Choose file or drag & drop'}
|
|
105
114
|
</div>
|
|
106
115
|
<div className="p-p--small tc-grey-500">
|
|
107
|
-
{textOverrides?.supportsText ||
|
|
116
|
+
{textOverrides?.supportsText || placeholder}
|
|
108
117
|
</div>
|
|
109
118
|
</div>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
|
|
120
|
+
{errors.map(({ id, message }) => message && (
|
|
121
|
+
<UploadFileCell
|
|
122
|
+
uploadStatus="ERROR"
|
|
123
|
+
file={{
|
|
124
|
+
error: message,
|
|
125
|
+
id,
|
|
126
|
+
name: message,
|
|
127
|
+
progress: 0,
|
|
128
|
+
type: "",
|
|
129
|
+
}}
|
|
130
|
+
key={id}
|
|
131
|
+
onRemoveFile={() => removeError(id)}
|
|
132
|
+
uploading={false}
|
|
133
|
+
/>
|
|
134
|
+
))}
|
|
135
|
+
|
|
113
136
|
{uploadedFiles.length > 0 && (
|
|
114
137
|
<div className="w100 mt16">
|
|
115
|
-
{uploadedFiles.map((file) =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
/>
|
|
125
|
-
);
|
|
126
|
-
})}
|
|
138
|
+
{uploadedFiles.map((file) => (
|
|
139
|
+
<UploadFileCell
|
|
140
|
+
uploadStatus={getUploadStatus(file.progress, file.error)}
|
|
141
|
+
file={file}
|
|
142
|
+
key={file.id}
|
|
143
|
+
onRemoveFile={onRemoveFile}
|
|
144
|
+
uploading={uploading}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
127
147
|
</div>
|
|
128
148
|
)}
|
|
149
|
+
|
|
150
|
+
<AnimateHeight duration={300} height={isOverMaxFiles ? 'auto' : 0}>
|
|
151
|
+
<p className="tc-red-500 p-p--small">
|
|
152
|
+
{textOverrides?.tooManyFilesError || "Too many files."}
|
|
153
|
+
</p>
|
|
154
|
+
</AnimateHeight>
|
|
129
155
|
</div>
|
|
130
156
|
);
|
|
131
157
|
};
|
|
158
|
+
|
|
159
|
+
export type { FileType, MultiDropzoneProps, UploadedFile, UploadStatus };
|
|
160
|
+
export default MultiDropZone;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Accept } from "react-dropzone";
|
|
2
|
+
|
|
3
|
+
export type UploadStatus = 'UPLOADING' | 'COMPLETE' | 'ERROR';
|
|
4
|
+
|
|
5
|
+
export const DOCUMENT_FILES = ['doc', 'docx', 'pdf'];
|
|
6
|
+
export const IMAGE_FILES = ['heic', 'bmp', 'jpeg', 'jpg', 'png'];
|
|
7
|
+
|
|
8
|
+
export const FILE_TYPES = [...DOCUMENT_FILES, ...IMAGE_FILES];
|
|
9
|
+
export type FileType = typeof FILE_TYPES[number];
|
|
10
|
+
|
|
11
|
+
export interface UploadedFile {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
type: FileType | string;
|
|
15
|
+
previewUrl?: string;
|
|
16
|
+
progress: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
showProgressBar?: boolean;
|
|
19
|
+
showLoadingSpinner?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type AcceptType = "document" | "image" | Accept;
|
|
23
|
+
export interface TextOverrides {
|
|
24
|
+
currentlyUploadingText?: string;
|
|
25
|
+
fileTypeError?: string;
|
|
26
|
+
fileTooLargeError?: string;
|
|
27
|
+
instructionsText?: string;
|
|
28
|
+
sizeUpToText?: string;
|
|
29
|
+
supportsText?: string;
|
|
30
|
+
supportsTextShort?: string;
|
|
31
|
+
tooManyFilesError?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ErrorMessage {
|
|
34
|
+
id: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ErrorCode } from 'react-dropzone';
|
|
2
|
+
import {
|
|
3
|
+
formatAcceptFileList,
|
|
4
|
+
getErrorMessage,
|
|
5
|
+
getFormattedAcceptObject,
|
|
6
|
+
getUploadStatus
|
|
7
|
+
} from '.';
|
|
8
|
+
|
|
9
|
+
const documentsAccept = {
|
|
10
|
+
'application/doc': ['.doc'],
|
|
11
|
+
'application/docx': ['.docx'],
|
|
12
|
+
'application/pdf': ['.pdf']
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const imagesAccept = {
|
|
16
|
+
'image/heic': [".heic"],
|
|
17
|
+
'image/bmp': [".bmp"],
|
|
18
|
+
'image/jpeg': [".jpeg"],
|
|
19
|
+
'image/jpg': [".jpg"],
|
|
20
|
+
'image/png': [".png"]
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('getUploadStatus', () => {
|
|
24
|
+
it('Should return error status if error is passed', () => {
|
|
25
|
+
expect(getUploadStatus(0, "Error message")).toEqual("ERROR");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("Should return uploading status if progress hasn't finished", () => {
|
|
29
|
+
expect(getUploadStatus(50)).toEqual("UPLOADING");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("Should return complete status if progress has finished", () => {
|
|
33
|
+
expect(getUploadStatus(100)).toEqual("COMPLETE");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getFormattedAcceptObject', () => {
|
|
38
|
+
it('Should return image accept object if is of type image', () => {
|
|
39
|
+
expect(getFormattedAcceptObject("image")).toEqual(imagesAccept);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('Should return documents accept object if is of type document', () => {
|
|
43
|
+
expect(getFormattedAcceptObject("document")).toEqual(documentsAccept);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('Should return accept object if it is manually defined', () => {
|
|
47
|
+
const accept = { "application/pdf": [".pdf"] };
|
|
48
|
+
|
|
49
|
+
expect(getFormattedAcceptObject(accept)).toEqual(accept);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('formatAcceptFileList', () => {
|
|
54
|
+
it('Should return empty object if accept is empty', () => {
|
|
55
|
+
expect(formatAcceptFileList({})).toEqual("");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('Should return documents list if documents accept is passed', () => {
|
|
59
|
+
expect(formatAcceptFileList(documentsAccept)).toEqual("DOC, DOCX, PDF");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('Should return images list if images accept is passed', () => {
|
|
63
|
+
expect(formatAcceptFileList(imagesAccept)).toEqual("HEIC, BMP, JPEG, JPG, PNG");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('Should return extension based on accept passed', () => {
|
|
67
|
+
const accept = {
|
|
68
|
+
"application/pdf": [".pdf"],
|
|
69
|
+
"image/jpg": [".jpg"]
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
expect(formatAcceptFileList(accept)).toEqual("PDF, JPG");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('getErrorMessage', () => {
|
|
77
|
+
it('Should return default error message', () => {
|
|
78
|
+
const defaultMessage = "Default Error Message.";
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
getErrorMessage({
|
|
82
|
+
code: "UNKNOWN",
|
|
83
|
+
message: defaultMessage
|
|
84
|
+
}, {
|
|
85
|
+
fileList: ""
|
|
86
|
+
})
|
|
87
|
+
).toEqual(defaultMessage);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('Should return default FileInvalidType default message', () => {
|
|
91
|
+
const fileList = "JPG, PDF";
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
getErrorMessage({
|
|
95
|
+
code: ErrorCode.FileInvalidType,
|
|
96
|
+
message: ""
|
|
97
|
+
}, { fileList })
|
|
98
|
+
).toEqual(`File type must be one of ${fileList}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('Should return FileInvalidType with textOverride message', () => {
|
|
102
|
+
const fileTypeError = "File Invalid Error";
|
|
103
|
+
const fileList = "JPG, PDF";
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
getErrorMessage({
|
|
107
|
+
code: ErrorCode.FileInvalidType,
|
|
108
|
+
message: ""
|
|
109
|
+
}, { fileList }, { fileTypeError })
|
|
110
|
+
).toEqual(`${fileTypeError} ${fileList}`);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Accept, ErrorCode, FileError } from "react-dropzone";
|
|
2
|
+
import { formatBytes } from "../../../util/formatBytes";
|
|
3
|
+
import {
|
|
4
|
+
AcceptType,
|
|
5
|
+
DOCUMENT_FILES,
|
|
6
|
+
FileType,
|
|
7
|
+
IMAGE_FILES,
|
|
8
|
+
TextOverrides,
|
|
9
|
+
UploadStatus } from "../types";
|
|
10
|
+
|
|
11
|
+
export const getUploadStatus = (progress: number, error?: string): UploadStatus => {
|
|
12
|
+
if (error) {
|
|
13
|
+
return 'ERROR';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (progress < 100) {
|
|
17
|
+
return 'UPLOADING';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return 'COMPLETE';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const formatMimeType = (type: string, values: FileType[]): Accept => {
|
|
24
|
+
const formatedValues = {} as Accept;
|
|
25
|
+
|
|
26
|
+
values.forEach((value) => {
|
|
27
|
+
formatedValues[`${type}/${value}`] = [`.${value}`];
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return formatedValues;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const DOCUMENT_FILES_ACCEPT = formatMimeType("application", DOCUMENT_FILES);
|
|
34
|
+
export const IMAGE_FILES_ACCEPT = formatMimeType("image", IMAGE_FILES);
|
|
35
|
+
|
|
36
|
+
export const getFormattedAcceptObject = (accept: AcceptType = {}): Accept => {
|
|
37
|
+
if (accept === "document") {
|
|
38
|
+
return DOCUMENT_FILES_ACCEPT;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (accept === "image") {
|
|
42
|
+
return IMAGE_FILES_ACCEPT;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return accept;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const formatAcceptFileList = (accept: Accept): string => (
|
|
49
|
+
Object.values(accept)
|
|
50
|
+
.reduce((acc, value) => [...acc, ...value], [])
|
|
51
|
+
.join(", ")
|
|
52
|
+
.replace(/\./g, '')
|
|
53
|
+
.toUpperCase()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export const getErrorMessage = (
|
|
57
|
+
{ code, message }: FileError,
|
|
58
|
+
{ fileList = "", maxSize }: { fileList?: string, maxSize?: number },
|
|
59
|
+
textOverrides?: TextOverrides,
|
|
60
|
+
): string => {
|
|
61
|
+
switch (code) {
|
|
62
|
+
case ErrorCode.FileInvalidType:
|
|
63
|
+
return `${textOverrides?.fileTypeError || "File type must be one of"} ${fileList}`;
|
|
64
|
+
case ErrorCode.FileTooLarge:
|
|
65
|
+
return `${textOverrides?.fileTooLargeError || "File is too large. It must be less than"} ${formatBytes(maxSize || 0)}.`;
|
|
66
|
+
default:
|
|
67
|
+
return message;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
@function url-encoded-color($color) {
|
|
2
2
|
@return '%23' + str-slice('#{$color}', 2, -1);
|
|
3
3
|
}
|
|
4
|
+
|
|
5
|
+
.sr-only {
|
|
6
|
+
border-width: 0;
|
|
7
|
+
clip: rect(0, 0, 0, 0);
|
|
8
|
+
height: 1px;
|
|
9
|
+
margin: -1px;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
padding: 0;
|
|
12
|
+
position: absolute;
|
|
13
|
+
white-space: nowrap;
|
|
14
|
+
width: 1px;
|
|
15
|
+
}
|