@groupeactual/ui-kit 1.7.0-beta.3 → 1.7.0-beta.5

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.
@@ -0,0 +1,690 @@
1
+ import React, {
2
+ ChangeEvent,
3
+ DragEvent,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+
11
+ import { faGoogleDrive } from '@fortawesome/free-brands-svg-icons';
12
+ import {
13
+ faEye,
14
+ faFolderOpen,
15
+ faTrash,
16
+ faUpload,
17
+ } from '@fortawesome/pro-regular-svg-icons';
18
+ import {
19
+ faCaretDown,
20
+ faFileAlt,
21
+ faInfoCircle,
22
+ } from '@fortawesome/pro-solid-svg-icons';
23
+ import { Box, IconButton } from '@mui/material';
24
+ import Menu from '@mui/material/Menu';
25
+
26
+ import Button from '@/components/Button';
27
+ import IconProvider from '@/components/IconProvider';
28
+ import MenuItem from '@/components/MenuItem';
29
+ import Text from '@/components/Text';
30
+ import Tooltip from '@/components/Tooltip';
31
+ import GooglePickerWrapper from '@/helpers/GooglePickerWrapper';
32
+
33
+ import {
34
+ AcceptTextType,
35
+ FileDataType,
36
+ GoogleDriveFile,
37
+ } from './fileuploader.interface';
38
+
39
+ interface Props {
40
+ title?: string;
41
+ subTitle?: string;
42
+ titleAddButton?: string;
43
+ helperText?: string;
44
+ titleTooltip?: string;
45
+ files?: FileDataType[];
46
+ isMulti?: boolean;
47
+ accept?: string[];
48
+ acceptText?: AcceptTextType;
49
+ orText?: string;
50
+ disabled?: boolean;
51
+ error?: string;
52
+ enableGoogleDrive?: boolean;
53
+ googleAuthClientId?: string;
54
+ googleApiKey?: string;
55
+ _isDroppingFile?: boolean; // * Only used for storybook
56
+ validateFile?: (
57
+ _size: number,
58
+ _type: string,
59
+ _accept: string[],
60
+ _setUploadFileError: React.Dispatch<React.SetStateAction<boolean | string>>,
61
+ ) => boolean;
62
+ onTouched?: () => void;
63
+ onFilesDataChange?: (_fileData: FileDataType[] | undefined) => void;
64
+ }
65
+
66
+ const FileUploader = ({
67
+ title = '',
68
+ subTitle = 'Déposer un fichier ici',
69
+ titleAddButton = 'Ajouter un fichier',
70
+ helperText = '',
71
+ titleTooltip,
72
+ files = [],
73
+ isMulti = false,
74
+ accept = [],
75
+ acceptText = { fileFormat: '', maxSize: '', subText: '' },
76
+ orText = 'ou',
77
+ disabled = false,
78
+ error,
79
+ enableGoogleDrive = false,
80
+ googleAuthClientId,
81
+ googleApiKey,
82
+ _isDroppingFile = false,
83
+ validateFile,
84
+ onTouched,
85
+ onFilesDataChange,
86
+ }: Props) => {
87
+ const DEFAULT_COLOR = '%23CBCBCB';
88
+ const ERROR_COLOR = '%23b80025';
89
+ const DROPPING_COLOR = '%23004F88';
90
+
91
+ // * States
92
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
93
+ // * currentFiles is used only to revoke the object URL when the component is unmounted for performance reasons
94
+ const [currentFiles, setCurrentFiles] = useState<File[]>([]);
95
+ // * filesData is used to display the files in the component
96
+ const [filesData, setFilesData] = useState<FileDataType[] | undefined>([]);
97
+ const [isDroppingFile, setIsDroppingFile] =
98
+ useState<boolean>(_isDroppingFile);
99
+ // * uploadFileError is used to display the error message when the file is not valid from the validateFile function
100
+ const [uploadFileError, setUploadFileError] = useState<string | boolean>(
101
+ false,
102
+ );
103
+
104
+ // * Refs
105
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
106
+
107
+ // * Colors
108
+ const dashedColor = useMemo(() => {
109
+ if ((!isMulti && filesData?.[0]?.name) || disabled) return DEFAULT_COLOR;
110
+ if (isDroppingFile) return DROPPING_COLOR;
111
+ if (error || uploadFileError !== false) return ERROR_COLOR;
112
+ return DEFAULT_COLOR;
113
+ }, [isDroppingFile, error, uploadFileError]);
114
+
115
+ const bgColor = useMemo(() => {
116
+ if ((!isMulti && filesData?.[0]?.name) || disabled) return 'greyXLight';
117
+ if (isDroppingFile) return 'blueHoverEquivalence';
118
+ return 'white';
119
+ }, [filesData?.length, disabled, isDroppingFile]);
120
+
121
+ const titleColor = useMemo(() => {
122
+ if (!isMulti && filesData?.[0]?.name) return 'greyDark';
123
+ if (error || uploadFileError !== false) return 'redError';
124
+ return 'greyXDark';
125
+ }, [filesData?.length, error, uploadFileError]);
126
+
127
+ // * Styles
128
+ const inputCss = useMemo(
129
+ () => ({
130
+ height: '87px',
131
+ backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='4' ry='4' stroke='${dashedColor}' stroke-width='2' stroke-dasharray='8%2c 8' stroke-dashoffset='0' stroke-linecap='round'/%3e%3c/svg%3e")`,
132
+ borderRadius: '4px',
133
+ py: '24px',
134
+ px: '22px',
135
+ display: 'flex',
136
+ justifyContent: 'space-between',
137
+ position: 'relative',
138
+ alignItems: 'center',
139
+ overflow: 'hidden',
140
+ backgroundColor: bgColor,
141
+ }),
142
+ [dashedColor, bgColor],
143
+ );
144
+
145
+ // * Events handlers
146
+ const handleClick = (event: React.MouseEvent<HTMLElement>) => {
147
+ setAnchorEl(event.currentTarget);
148
+ };
149
+ const handleClose = () => {
150
+ setAnchorEl(null);
151
+ };
152
+
153
+ const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
154
+ event.preventDefault();
155
+ event.stopPropagation();
156
+
157
+ if ((!isMulti && filesData?.[0]?.name) || disabled) return;
158
+
159
+ setIsDroppingFile(true);
160
+ };
161
+
162
+ const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
163
+ event.preventDefault();
164
+ event.stopPropagation();
165
+
166
+ if ((!isMulti && filesData?.[0]?.name) || disabled) return;
167
+
168
+ setIsDroppingFile(false);
169
+ };
170
+
171
+ // * Handle file change and file deletion
172
+ const handleFileChange = useCallback(
173
+ (
174
+ _event: ChangeEvent<HTMLInputElement> | null,
175
+ data?: {
176
+ docs: GoogleDriveFile[];
177
+ } | null,
178
+ token: string | null = null,
179
+ accept: string[] = [],
180
+ fakeClick?: boolean,
181
+ ) => {
182
+ if (fakeClick) {
183
+ fileInputRef?.current?.click();
184
+ return;
185
+ }
186
+
187
+ handleClose();
188
+ onTouched?.();
189
+
190
+ // * Local Files
191
+ if (
192
+ fileInputRef.current?.files &&
193
+ fileInputRef.current?.files?.length > 0
194
+ ) {
195
+ const validInputFiles: File[] = [];
196
+ const fileList = Array.from(fileInputRef.current?.files);
197
+
198
+ const newFileData: FileDataType[] = fileList
199
+ .map((file) => {
200
+ if (
201
+ validateFile &&
202
+ !validateFile(file.size, file.type, accept, setUploadFileError)
203
+ ) {
204
+ setIsDroppingFile(false);
205
+ return null;
206
+ }
207
+
208
+ validInputFiles.push(file);
209
+
210
+ return {
211
+ name: file?.name ?? '',
212
+ size: Math.round(file.size / 1024),
213
+ type: file.type,
214
+ url: URL.createObjectURL(file),
215
+ };
216
+ })
217
+ .filter((file): file is FileDataType => file !== null);
218
+
219
+ if (newFileData && newFileData?.length > 0) {
220
+ setCurrentFiles([...(currentFiles || []), ...validInputFiles]);
221
+ setFilesData([...(filesData || []), ...newFileData]);
222
+ }
223
+ } else if (token && data && data.docs && data.docs.length > 0) {
224
+ // * Google Drive
225
+ const validDocs = data.docs.filter((doc) =>
226
+ validateFile
227
+ ? validateFile(
228
+ doc.sizeBytes,
229
+ doc.mimeType,
230
+ accept,
231
+ setUploadFileError,
232
+ )
233
+ : true,
234
+ );
235
+
236
+ const googleFileData = validDocs.map((doc) => ({
237
+ name: doc.name,
238
+ size: Math.round(doc.sizeBytes / 1024), // * Convert size to KB
239
+ type: doc.mimeType,
240
+ url: `https://drive.google.com/file/d/${doc.id}/view?usp=drive_we`,
241
+ driveFileId: doc.id,
242
+ driveAccessToken: token,
243
+ }));
244
+
245
+ setFilesData([...(filesData || []), ...googleFileData]);
246
+ }
247
+
248
+ setIsDroppingFile(false);
249
+ },
250
+ [
251
+ fileInputRef,
252
+ currentFiles,
253
+ filesData,
254
+ setIsDroppingFile,
255
+ setCurrentFiles,
256
+ setFilesData,
257
+ validateFile,
258
+ onTouched,
259
+ handleClose,
260
+ ],
261
+ );
262
+
263
+ const handleDelete = useCallback(
264
+ (fileIndex: number) => {
265
+ //* Remove the file from the currentFiles array
266
+ if (currentFiles && currentFiles.length > 0) {
267
+ const fileToDelete = currentFiles[fileIndex];
268
+ if (fileToDelete) {
269
+ // * Revoke the object URL to free up memory, use try and catch to avoid errors on the other files not imported with the file input
270
+ try {
271
+ const fileURL = URL.createObjectURL(fileToDelete);
272
+ URL.revokeObjectURL(fileURL);
273
+ } catch {
274
+ // * Do nothing
275
+ }
276
+ }
277
+
278
+ if (currentFiles.length === 1) {
279
+ setCurrentFiles([]);
280
+ } else {
281
+ setCurrentFiles(
282
+ currentFiles.filter((_, index) => index !== fileIndex),
283
+ );
284
+ }
285
+ }
286
+
287
+ //* Remove the file from the filesData array
288
+ if (filesData && filesData.length === 1) {
289
+ setFilesData([]);
290
+ } else {
291
+ setFilesData(
292
+ Object.values(filesData || []).filter(
293
+ (_, index) => index !== fileIndex,
294
+ ),
295
+ );
296
+ }
297
+
298
+ setIsDroppingFile(false);
299
+ },
300
+ [currentFiles, filesData, setIsDroppingFile, setCurrentFiles, setFilesData],
301
+ );
302
+
303
+ // * Utils
304
+ const extractExtensions = (types: string[]): string[] => {
305
+ return types.map((type) => {
306
+ const extension = type.split('/')[1].toUpperCase();
307
+ return extension;
308
+ });
309
+ };
310
+
311
+ // * UseEffects
312
+
313
+ // * Initialize the component with the files passed as props
314
+ useEffect(() => {
315
+ const FilesDataDto: FileDataType[] = files.map((file) => ({
316
+ name: file.name,
317
+ size: Math.round(file.size / 1024), // Convert size to KB
318
+ type: file.type,
319
+ url: file.url,
320
+ }));
321
+
322
+ const initialFiles = FilesDataDto.map(
323
+ (fileData) =>
324
+ new File(
325
+ [new Blob([`Dummy content for ${fileData.name}`])],
326
+ fileData.name,
327
+ {
328
+ type: fileData.type,
329
+ },
330
+ ),
331
+ );
332
+
333
+ setCurrentFiles(initialFiles);
334
+ setFilesData(FilesDataDto);
335
+ }, []);
336
+
337
+ useEffect(() => {
338
+ // * Notify parent component
339
+ onFilesDataChange?.(filesData);
340
+ }, [filesData]);
341
+
342
+ useEffect(() => {
343
+ return () => {
344
+ if (!currentFiles?.length) return;
345
+
346
+ try {
347
+ // * Only for file imported with the file input, use try and catch to avoid errors on the other files
348
+ currentFiles.forEach((file) => {
349
+ const fileURL = URL.createObjectURL(file);
350
+ URL.revokeObjectURL(fileURL);
351
+ });
352
+ } catch {
353
+ // * Do nothing
354
+ }
355
+ };
356
+ }, [currentFiles]);
357
+
358
+ return (
359
+ <Box
360
+ data-testid="Uploader-document"
361
+ sx={{
362
+ pb: 3,
363
+ }}
364
+ >
365
+ <Box sx={{ display: 'inline-flex', alignItems: 'center' }}>
366
+ <Text variant="body2Medium" color={titleColor} pl={1}>
367
+ {title}
368
+ </Text>
369
+ {titleTooltip && (
370
+ <Box sx={{ marginLeft: '4px' }}>
371
+ <Tooltip
372
+ title={titleTooltip}
373
+ sx={{
374
+ display: 'flex',
375
+ }}
376
+ >
377
+ <IconProvider icon={faInfoCircle} size="sm" color="blueInfo" />
378
+ </Tooltip>
379
+ </Box>
380
+ )}
381
+ </Box>
382
+
383
+ <Box
384
+ mt={1}
385
+ p={2}
386
+ sx={inputCss}
387
+ onDragOver={handleDragOver}
388
+ onDragEnter={handleDragOver}
389
+ onDragLeave={handleDragLeave}
390
+ onDragEnd={handleDragLeave}
391
+ >
392
+ <Box
393
+ sx={{
394
+ opacity: 0,
395
+ position: 'absolute',
396
+ top: '0px',
397
+ right: '0px',
398
+ width: '100%',
399
+ height: '100%',
400
+ 'input[type="file"]': {
401
+ cursor:
402
+ disabled || (filesData?.[0]?.name && !isMulti)
403
+ ? 'not-allowed'
404
+ : 'pointer',
405
+ color: 'transparent',
406
+ width: '100%',
407
+ height: '100%',
408
+ },
409
+ }}
410
+ >
411
+ <input
412
+ title=""
413
+ data-testid="document-input"
414
+ disabled={!!(!isMulti && filesData?.[0]?.name) || disabled}
415
+ type="file"
416
+ ref={fileInputRef}
417
+ onChange={(event) => handleFileChange(event, null, null, accept)}
418
+ onBlur={onTouched}
419
+ multiple={isMulti}
420
+ accept={accept.join(',')}
421
+ />
422
+ </Box>
423
+ <Box sx={{ display: 'flex', flexDirection: 'column' }}>
424
+ <Text
425
+ variant="body2Medium"
426
+ color={
427
+ (!isMulti && filesData?.[0]?.name && 'greyDark') || 'greyXDark'
428
+ }
429
+ >
430
+ {subTitle}
431
+ </Text>
432
+ <Box sx={{ maxWidth: '240px' }}>
433
+ <Text
434
+ variant="caption"
435
+ sx={{
436
+ color: 'greyDark',
437
+ marginTop: '2px',
438
+ }}
439
+ >
440
+ Format{' '}
441
+ {(acceptText?.fileFormat && acceptText?.fileFormat?.trim() !== ''
442
+ ? acceptText.fileFormat
443
+ : extractExtensions(accept).join(', ')) + ' - '}
444
+ {(acceptText.maxSize && acceptText.maxSize) || 'Max. 10 Mo'}
445
+ {acceptText.subText && (
446
+ <>
447
+ <br />
448
+ {acceptText.subText}
449
+ </>
450
+ )}
451
+ </Text>
452
+ </Box>
453
+ </Box>
454
+ <Box>
455
+ <Text
456
+ variant="body2Medium"
457
+ color={
458
+ (!isMulti && filesData?.[0]?.name && 'greyDark') || 'greyXDark'
459
+ }
460
+ >
461
+ {orText}
462
+ </Text>
463
+ </Box>
464
+ <Box>
465
+ {(enableGoogleDrive && (
466
+ <>
467
+ <Button
468
+ variant="secondary"
469
+ sx={{
470
+ display: 'flex',
471
+ '&.MuiButton-secondaryPrimary.Mui-disabled': {
472
+ backgroundColor: '#FFFFFF !important',
473
+ },
474
+ }}
475
+ onClick={handleClick}
476
+ disabled={!!(!isMulti && filesData?.[0]?.name)}
477
+ endIcon={<IconProvider icon={faCaretDown} />}
478
+ >
479
+ <IconProvider icon={faUpload} size="md" mr={1} />
480
+ {titleAddButton}
481
+ </Button>
482
+ <Menu
483
+ data-testid="seizure-card-menu"
484
+ anchorEl={anchorEl}
485
+ open={Boolean(anchorEl)}
486
+ onClose={handleClose}
487
+ anchorOrigin={{
488
+ vertical: 'bottom',
489
+ horizontal: 'right',
490
+ }}
491
+ transformOrigin={{
492
+ vertical: 'top',
493
+ horizontal: 'right',
494
+ }}
495
+ slotProps={{
496
+ paper: {
497
+ style: {
498
+ marginTop: '8px',
499
+ marginLeft: '0',
500
+ },
501
+ },
502
+ }}
503
+ >
504
+ <MenuItem
505
+ testId="pc-add"
506
+ onClick={() => handleFileChange(null, null, null, [], true)}
507
+ >
508
+ <Box gap={1} display="flex">
509
+ <IconProvider size="sm" icon={faFolderOpen} />
510
+ <Text variant="body2">Depuis votre PC</Text>
511
+ </Box>
512
+ </MenuItem>
513
+ <GooglePickerWrapper
514
+ callback={(data, token) =>
515
+ handleFileChange(null, data, token, accept)
516
+ }
517
+ multiselect={true}
518
+ navHidden={false}
519
+ googleAuthClientId={googleAuthClientId ?? ''}
520
+ googleApiKey={googleApiKey ?? ''}
521
+ scopes="https://www.googleapis.com/auth/drive.file"
522
+ viewId="FOLDERS"
523
+ >
524
+ <MenuItem testId="drive-add" onClick={handleClose}>
525
+ <Box gap={1} display="flex">
526
+ <IconProvider size="sm" icon={faGoogleDrive} />
527
+ <Text variant="body2">Depuis Google Drive</Text>
528
+ </Box>
529
+ </MenuItem>
530
+ </GooglePickerWrapper>
531
+ </Menu>
532
+ </>
533
+ )) || (
534
+ <Button
535
+ variant="secondary"
536
+ sx={{
537
+ display: 'flex',
538
+ '&.MuiButton-secondaryPrimary.Mui-disabled': {
539
+ backgroundColor: '#FFFFFF !important',
540
+ },
541
+ }}
542
+ onClick={() => handleFileChange(null, null, null, [], true)}
543
+ disabled={!!(!isMulti && filesData?.[0]?.name)}
544
+ >
545
+ <IconProvider icon={faUpload} size="md" mr={1} />
546
+ {titleAddButton}
547
+ </Button>
548
+ )}
549
+ </Box>
550
+ </Box>
551
+ {(error || helperText || uploadFileError !== false) && (
552
+ <Box pl={1} pt={1}>
553
+ <Text
554
+ variant="caption"
555
+ color={
556
+ ((error || uploadFileError !== false) && 'redError') || 'greyDark'
557
+ }
558
+ data-testid="helperText"
559
+ >
560
+ {uploadFileError !== false
561
+ ? uploadFileError
562
+ : (error ?? helperText)}
563
+ </Text>
564
+ </Box>
565
+ )}
566
+ <Box sx={{ mt: '16px' }}>
567
+ {(isMulti
568
+ ? Object.values(filesData ?? [])
569
+ : Object.values(filesData ?? [])?.slice(0, 1)
570
+ )?.map((fileData, index) => (
571
+ <Box
572
+ key={index}
573
+ sx={{
574
+ display: 'flex',
575
+ alignItems: 'center',
576
+ border: '0.5px solid',
577
+ borderColor: error ? 'redError' : 'greyLightDefaultBorder',
578
+ borderRadius: '0',
579
+ justifyContent: 'space-between',
580
+ maxHeight: '50px',
581
+ p: '16px',
582
+ mb: '8px',
583
+ }}
584
+ >
585
+ <Box
586
+ sx={{
587
+ display: 'flex',
588
+ alignItems: 'center',
589
+ flexShrink: 1,
590
+ minWidth: 0,
591
+ }}
592
+ >
593
+ <IconProvider
594
+ icon={faFileAlt}
595
+ color="greyMediumInactive"
596
+ size="sm"
597
+ mr={1}
598
+ />
599
+ <Text
600
+ variant="body2Medium"
601
+ color="greyXDark"
602
+ sx={{
603
+ overflow: 'hidden',
604
+ whiteSpace: 'nowrap',
605
+ textAlign: 'left',
606
+ textOverflow: 'ellipsis',
607
+ flexShrink: 1,
608
+ }}
609
+ >
610
+ {fileData?.name}
611
+ </Text>
612
+ {fileData?.size && fileData.size !== 0 ? (
613
+ <Text
614
+ component="span"
615
+ variant="body2Regular"
616
+ color="greyDark"
617
+ sx={{ minWidth: '41px', marginLeft: '8px' }}
618
+ >
619
+ ({fileData?.size} ko)
620
+ </Text>
621
+ ) : null}
622
+ </Box>
623
+ <Box
624
+ sx={{
625
+ display: 'flex',
626
+ alignItems: 'center',
627
+ flexShrink: 0,
628
+ }}
629
+ >
630
+ <IconButton
631
+ size="medium"
632
+ color="primary"
633
+ sx={{
634
+ height: '42px',
635
+ width: '42px',
636
+ mx: 1 / 2,
637
+ outline: 'none !important',
638
+ borderRadius: '4px',
639
+ '&:hover': {
640
+ backgroundColor: 'blueHoverOpacity12',
641
+ },
642
+ }}
643
+ data-testid={`view-btn-${index}`}
644
+ onClick={() => {
645
+ if (fileData?.url) {
646
+ window.open(fileData.url, '_blank');
647
+ }
648
+ }}
649
+ >
650
+ <IconProvider
651
+ icon={faEye}
652
+ color="grey"
653
+ size="md"
654
+ height="16px"
655
+ width="16px"
656
+ />
657
+ </IconButton>
658
+ <IconButton
659
+ size="medium"
660
+ color="primary"
661
+ sx={{
662
+ mx: 1 / 2,
663
+ height: '42px',
664
+ width: '42px',
665
+ outline: 'none !important',
666
+ borderRadius: '4px',
667
+ '&:hover': {
668
+ backgroundColor: 'blueHoverOpacity12',
669
+ },
670
+ }}
671
+ data-testid={`delete-btn-${index}`}
672
+ onClick={() => handleDelete(index)}
673
+ >
674
+ <IconProvider
675
+ icon={faTrash}
676
+ color="grey"
677
+ size="md"
678
+ height="16px"
679
+ width="16px"
680
+ />
681
+ </IconButton>
682
+ </Box>
683
+ </Box>
684
+ ))}
685
+ </Box>
686
+ </Box>
687
+ );
688
+ };
689
+
690
+ export default FileUploader;
@@ -0,0 +1,21 @@
1
+ export interface AcceptTextType {
2
+ fileFormat?: string;
3
+ maxSize?: string;
4
+ subText?: string;
5
+ }
6
+
7
+ export interface FileDataType {
8
+ name: string;
9
+ size: number;
10
+ type: string;
11
+ url: string;
12
+ driveFileId?: string;
13
+ driveAccessToken?: string;
14
+ }
15
+
16
+ export interface GoogleDriveFile {
17
+ name: string;
18
+ sizeBytes: number;
19
+ mimeType: string;
20
+ id: string;
21
+ }
@@ -1 +1 @@
1
- export { default } from './FileUploaderSingle';
1
+ export { default } from './FileUploader';