tp-react-elements 2.0.3 → 2.0.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.
- package/README.md +6 -2
- package/dist/_virtual/index.esm11.js +2 -2
- package/dist/_virtual/index.esm12.js +2 -2
- package/dist/_virtual/index.esm13.js +2 -2
- package/dist/_virtual/index.esm14.js +2 -2
- package/dist/_virtual/index.esm4.js +2 -2
- package/dist/_virtual/index.esm5.js +2 -2
- package/dist/_virtual/index.esm6.js +2 -2
- package/dist/components/DateRangePicker/DateRangePicker.esm.js +49 -72
- package/dist/components/Form/FormRender.esm.js +1 -1
- package/dist/components/FormComponents/DatePicker/DatepickerWrapperV2.esm.js +10 -15
- package/dist/components/FormComponents/DatePicker/DatetimepickerWrapper.esm.js +53 -61
- package/dist/components/FormComponents/FileUpload/FormRenderFileUpload.esm.js +54 -68
- package/dist/components/FormComponents/FileUpload/FormRenderMultiFileUpload.esm.js +61 -76
- package/dist/components/FormComponents/FileUpload/MultiFileWithPreview.esm.js +103 -132
- package/dist/components/FormComponents/FormNumberField/FormNumberField.d.ts +0 -38
- package/dist/components/FormComponents/FormNumberField/FormNumberField.esm.js +32 -97
- package/dist/components/FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js +44 -38
- package/dist/components/FormComponents/FormTextField/FormTextField.esm.js +3 -3
- package/dist/components/FormComponents/FormTextFieldWithSelect/FormTextFieldWithSelect.esm.js +3 -2
- package/dist/components/FormComponents/PasswordField/PasswordField.esm.js +4 -4
- package/dist/components/FormComponents/RichTextEditor/RichTextEditor.esm.js +23 -22
- package/dist/components/FormComponents/Select/SearchableSelect.esm.js +9 -9
- package/dist/components/FormComponents/TimePicker/TimePicker.esm.js +19 -19
- package/dist/components/FormComponents/index.esm.js +2 -2
- package/dist/components/ModalField/ConfirmationDialog.esm.js +3 -3
- package/dist/components/ModalField/ModalField.esm.js +5 -12
- package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +17 -2
- package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +161 -61
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.d.ts +21 -3
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +133 -92
- package/dist/components/SelectField/MultiSelectFieldComponent.esm.js +3 -3
- package/dist/components/SessionTimeOut/SessionTimeOut.esm.js +33 -34
- package/dist/node_modules/react-date-range/dist/components/DateInput/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateRange/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateRangePicker/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DayCell/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DefinedRange/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/InputRangeField/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/Month/index.esm.js +1 -1
- package/package.json +1 -1
|
@@ -1,104 +1,89 @@
|
|
|
1
1
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { Box, TextField } from '@mui/material';
|
|
3
|
-
import { useRef, useEffect } from 'react';
|
|
3
|
+
import { useRef, useEffect, useMemo, useCallback } from 'react';
|
|
4
4
|
import { useWatch } from 'react-hook-form';
|
|
5
5
|
import 'dayjs';
|
|
6
6
|
import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
|
|
7
7
|
import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
|
|
8
8
|
|
|
9
|
+
// Static lookup — defined once at module level, never recreated
|
|
10
|
+
const EXTENSION_MAP = {
|
|
11
|
+
excel: ['xls', 'xlsx'],
|
|
12
|
+
pdf: ['pdf'],
|
|
13
|
+
zip: ['zip'],
|
|
14
|
+
image: ['jpg', 'jpeg', 'png'],
|
|
15
|
+
document: ['doc', 'docx', 'pdf'],
|
|
16
|
+
all: ['pdf', 'jpg', 'jpeg', 'png', 'xls', 'xlsx', 'doc', 'docx', 'zip'],
|
|
17
|
+
};
|
|
9
18
|
const FormRenderMultiFileUpload = ({ props, variant, }) => {
|
|
10
|
-
// const getDocument: Document = document;
|
|
11
19
|
const inputRef = useRef(null);
|
|
12
20
|
const watched = useWatch({ control: props.control, name: props.item.name });
|
|
13
21
|
useEffect(() => {
|
|
14
|
-
if (watched === null || watched === undefined) {
|
|
15
|
-
|
|
16
|
-
inputRef.current.value = '';
|
|
22
|
+
if ((watched === null || watched === undefined) && inputRef.current) {
|
|
23
|
+
inputRef.current.value = '';
|
|
17
24
|
}
|
|
18
25
|
}, [watched]);
|
|
19
26
|
const isError = !!props.errors?.[props.item.name];
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
excel: ['xls', 'xlsx'],
|
|
23
|
-
pdf: ['pdf'],
|
|
24
|
-
zip: ['zip'],
|
|
25
|
-
image: ['jpg', 'jpeg', 'png'],
|
|
26
|
-
document: ['doc', 'docx', 'pdf'],
|
|
27
|
-
all: ['pdf', 'jpg', 'jpeg', 'png', 'xls', 'xlsx', 'doc', 'docx', 'zip'],
|
|
28
|
-
};
|
|
29
|
-
const getValidExtensions = () => {
|
|
27
|
+
// Derived once per fileType change — not on every render
|
|
28
|
+
const validExtensions = useMemo(() => {
|
|
30
29
|
const fileType = props.item.fileType;
|
|
31
|
-
if (!fileType)
|
|
32
|
-
return
|
|
33
|
-
}
|
|
30
|
+
if (!fileType)
|
|
31
|
+
return EXTENSION_MAP.all;
|
|
34
32
|
if (Array.isArray(fileType)) {
|
|
35
|
-
|
|
36
|
-
const hasPredefinedKeys = fileType.some((item) => extensionMap[item]);
|
|
33
|
+
const hasPredefinedKeys = fileType.some((t) => EXTENSION_MAP[t]);
|
|
37
34
|
if (hasPredefinedKeys) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else {
|
|
45
|
-
// Treat as direct extension
|
|
46
|
-
extensions.add(item);
|
|
47
|
-
}
|
|
35
|
+
const exts = new Set();
|
|
36
|
+
fileType.forEach((t) => {
|
|
37
|
+
if (EXTENSION_MAP[t])
|
|
38
|
+
EXTENSION_MAP[t].forEach((e) => exts.add(e));
|
|
39
|
+
else
|
|
40
|
+
exts.add(t);
|
|
48
41
|
});
|
|
49
|
-
return Array.from(
|
|
42
|
+
return Array.from(exts);
|
|
50
43
|
}
|
|
51
|
-
// All items are direct extensions
|
|
52
44
|
return fileType;
|
|
53
45
|
}
|
|
54
|
-
return
|
|
55
|
-
};
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
return EXTENSION_MAP[fileType] ?? EXTENSION_MAP.all;
|
|
47
|
+
}, [props.item.fileType]);
|
|
48
|
+
const accept = useMemo(() => validExtensions.map((e) => `.${e}`).join(','), [validExtensions]);
|
|
49
|
+
const handleChange = useCallback((event) => {
|
|
50
|
+
const files = event.target.files;
|
|
51
|
+
const fileName = files && files.length > 0
|
|
52
|
+
? Array.from(files)
|
|
53
|
+
.map((item) => item.name)
|
|
54
|
+
.join(',')
|
|
55
|
+
: null;
|
|
56
|
+
let fileExtension = null;
|
|
57
|
+
if (fileName) {
|
|
58
|
+
const ext = fileName.split('.').pop();
|
|
59
|
+
if (ext)
|
|
60
|
+
fileExtension = ext.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
if (props.item?.fileType && fileExtension && !validExtensions.includes(fileExtension)) {
|
|
63
|
+
props.item?.handleFileError?.(`Please upload ${validExtensions.join(', ')} files only`);
|
|
64
|
+
event.target.value = '';
|
|
65
|
+
props.setValue(props.item.name, null);
|
|
66
|
+
props.setValue(props.item.name + 'Name', '');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (files && files[files.length - 1].size > 20000000) {
|
|
70
|
+
props.item?.handleFileError?.('File size should be less than 20MB');
|
|
71
|
+
props.setValue(props.item.name, null);
|
|
72
|
+
props.setValue(props.item.name + 'File', []);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const fileArray = Array.from(files);
|
|
76
|
+
props.setValue(props.item.name, fileName);
|
|
77
|
+
props.setValue(props.item.name + 'File', fileArray);
|
|
78
|
+
props.clearErrors(props.item.name);
|
|
79
|
+
}, [validExtensions, props.item, props.setValue, props.clearErrors]);
|
|
80
|
+
return (jsxs(Fragment, { children: [jsxs(Box, { paddingLeft: '4px', sx: { ...props.item.sx }, children: [props.item?.label && (jsx(Box, { sx: { fontSize: '10px;' }, children: renderLabel(variant, props, true) })), jsx(TextField, { type: "file", id: props.item.name, error: isError, inputRef: inputRef, inputProps: {
|
|
61
81
|
multiple: true,
|
|
62
|
-
accept
|
|
82
|
+
accept,
|
|
63
83
|
'aria-labelledby': `${props.item.name}-label`,
|
|
64
84
|
'aria-describedby': `${props.item.name}-helper ${props.item.name}-error`,
|
|
65
85
|
'aria-required': props.item.required ? true : undefined,
|
|
66
|
-
}, onChange:
|
|
67
|
-
const file = event.target.files;
|
|
68
|
-
const fileName = file && file.length > 0
|
|
69
|
-
? Array.from(file)
|
|
70
|
-
.map((item) => item.name)
|
|
71
|
-
.join(',')
|
|
72
|
-
: null;
|
|
73
|
-
let fileExtension = null;
|
|
74
|
-
if (fileName) {
|
|
75
|
-
const ext = fileName.split('.').pop();
|
|
76
|
-
if (ext) {
|
|
77
|
-
fileExtension = ext.toLowerCase();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
const validExtensions = getValidExtensions();
|
|
81
|
-
if (props.item?.fileType && fileExtension && !validExtensions.includes(fileExtension)) {
|
|
82
|
-
props.item?.handleFileError &&
|
|
83
|
-
props.item?.handleFileError(`Please upload ${validExtensions.join(', ')} files only`);
|
|
84
|
-
event.target.value = ''; // Clear the file input\
|
|
85
|
-
props.setValue(props.item?.name, null);
|
|
86
|
-
props.setValue(props.item?.name + 'Name', '');
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
else if (event.target.files[event.target.files.length - 1].size > 20000000) {
|
|
90
|
-
props.item?.handleFileError &&
|
|
91
|
-
props.item?.handleFileError(`File size should be less than 20MB`);
|
|
92
|
-
event.target.files = null; // Clear the file input
|
|
93
|
-
props.setValue(props.item?.name, null);
|
|
94
|
-
props.setValue(props.item?.name + 'File', []);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const fileArray = Array.from(file);
|
|
98
|
-
props.setValue(props.item?.name, fileName);
|
|
99
|
-
props.setValue(props.item?.name + 'File', fileArray);
|
|
100
|
-
props.clearErrors(props.item?.name);
|
|
101
|
-
}, sx: { width: '100%' } })] }), jsx(FormBottomField, { ...props })] }));
|
|
86
|
+
}, onChange: handleChange, sx: { width: '100%' } })] }), jsx(FormBottomField, { ...props })] }));
|
|
102
87
|
};
|
|
103
88
|
|
|
104
89
|
export { FormRenderMultiFileUpload as default };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { Box, Button, IconButton, Modal, Typography } from '@mui/material';
|
|
3
|
-
import { useRef, useState, useEffect } from 'react';
|
|
3
|
+
import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
4
|
import { useWatch } from 'react-hook-form';
|
|
5
5
|
import 'dayjs';
|
|
6
6
|
import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
|
|
@@ -15,6 +15,48 @@ import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
|
|
15
15
|
import TableChartIcon from '@mui/icons-material/TableChart';
|
|
16
16
|
import DescriptionIcon from '@mui/icons-material/Description';
|
|
17
17
|
|
|
18
|
+
// ─── Module-level pure utilities ──────────────────────────────────────────────
|
|
19
|
+
// Defined outside the component so they are never recreated on render.
|
|
20
|
+
const EXTENSION_MAP = {
|
|
21
|
+
excel: ['xls', 'xlsx'],
|
|
22
|
+
pdf: ['pdf'],
|
|
23
|
+
image: ['jpg', 'jpeg', 'png'],
|
|
24
|
+
all: ['pdf', 'jpg', 'jpeg', 'png', 'xls', 'xlsx', 'doc', 'docx', 'zip'],
|
|
25
|
+
zip: ['zip'],
|
|
26
|
+
document: ['doc', 'docx', 'pdf'],
|
|
27
|
+
};
|
|
28
|
+
const isImageFile = (file) => {
|
|
29
|
+
if (!file)
|
|
30
|
+
return false;
|
|
31
|
+
const ext = typeof file === 'string'
|
|
32
|
+
? file.split('.').pop()?.toLowerCase()
|
|
33
|
+
: file.name?.split('.').pop()?.toLowerCase();
|
|
34
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext || '');
|
|
35
|
+
};
|
|
36
|
+
const isPdfFile = (file) => {
|
|
37
|
+
if (!file)
|
|
38
|
+
return false;
|
|
39
|
+
const name = typeof file === 'string' ? file : (file.name || '');
|
|
40
|
+
return name.toLowerCase().endsWith('.pdf');
|
|
41
|
+
};
|
|
42
|
+
const isExcelFile = (file) => {
|
|
43
|
+
if (!file)
|
|
44
|
+
return false;
|
|
45
|
+
const name = typeof file === 'string' ? file : (file.name || '');
|
|
46
|
+
return /\.xlsx?$/.test(name.toLowerCase());
|
|
47
|
+
};
|
|
48
|
+
const isWordFile = (file) => {
|
|
49
|
+
if (!file)
|
|
50
|
+
return false;
|
|
51
|
+
const name = typeof file === 'string' ? file : (file.name || '');
|
|
52
|
+
return /\.(doc|docx)$/.test(name.toLowerCase());
|
|
53
|
+
};
|
|
54
|
+
const getObjectUrl = (file) => {
|
|
55
|
+
if (!file)
|
|
56
|
+
return '';
|
|
57
|
+
return typeof file === 'string' ? file : URL.createObjectURL(file);
|
|
58
|
+
};
|
|
59
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
18
60
|
const MultiFileWithPreview = ({ props, variant, }) => {
|
|
19
61
|
const inputRef = useRef(null);
|
|
20
62
|
const watched = useWatch({ control: props.control, name: props.item.name });
|
|
@@ -22,7 +64,7 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
22
64
|
const [open, setOpen] = useState(false);
|
|
23
65
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
24
66
|
const [currentFileIndex, setCurrentFileIndex] = useState(0);
|
|
25
|
-
// Clamp preview index when selection changes
|
|
67
|
+
// Clamp preview index when selection changes
|
|
26
68
|
useEffect(() => {
|
|
27
69
|
if (selectedFiles.length === 0) {
|
|
28
70
|
setCurrentFileIndex(0);
|
|
@@ -35,11 +77,12 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
35
77
|
setCurrentFileIndex(0);
|
|
36
78
|
}
|
|
37
79
|
}, [selectedFiles, currentFileIndex]);
|
|
38
|
-
// Determine if this is single or multiple file mode
|
|
39
80
|
const isSingleFileMode = props.item.maxLength === 1;
|
|
40
81
|
useEffect(() => {
|
|
41
82
|
const isWatchedEmpty = watched === null || watched === undefined || watched === '';
|
|
42
|
-
const isWatchedFileEmpty = watchedFile === null ||
|
|
83
|
+
const isWatchedFileEmpty = watchedFile === null ||
|
|
84
|
+
watchedFile === undefined ||
|
|
85
|
+
(Array.isArray(watchedFile) && watchedFile.length === 0);
|
|
43
86
|
if (isWatchedEmpty && isWatchedFileEmpty) {
|
|
44
87
|
if (inputRef.current)
|
|
45
88
|
inputRef.current.value = '';
|
|
@@ -48,38 +91,36 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
48
91
|
}
|
|
49
92
|
const looksLikeUrl = (s) => typeof s === 'string' && /^(https?:|blob:|data:|\/)/i.test(s);
|
|
50
93
|
let files = [];
|
|
51
|
-
// Check watchedFile first
|
|
52
94
|
if (Array.isArray(watchedFile) && watchedFile.length > 0) {
|
|
53
|
-
files = watchedFile
|
|
95
|
+
files = watchedFile
|
|
96
|
+
.map((f) => {
|
|
54
97
|
if (f instanceof File)
|
|
55
98
|
return f;
|
|
56
99
|
if (typeof f === 'object' && f !== null) {
|
|
57
|
-
const
|
|
58
|
-
const key = possibleKeys.find((k) => f[k]);
|
|
100
|
+
const key = ['image', 'url', 'path', 'filePath'].find((k) => f[k]);
|
|
59
101
|
return key ? f[key]?.replace(/\\/g, '/').trim() : undefined;
|
|
60
102
|
}
|
|
61
|
-
if (typeof f === 'string')
|
|
62
|
-
return f.replace(/\\/g, '/').trim();
|
|
63
|
-
}
|
|
103
|
+
if (typeof f === 'string')
|
|
104
|
+
return f.replace(/\\/g, '/').trim();
|
|
64
105
|
return undefined;
|
|
65
|
-
})
|
|
106
|
+
})
|
|
107
|
+
.filter(Boolean);
|
|
66
108
|
}
|
|
67
109
|
else if (typeof watchedFile === 'string' && watchedFile.trim() !== '') {
|
|
68
|
-
files = watchedFile.split(',').map(s => s.replace(/\\/g, '/').trim()).filter(Boolean);
|
|
110
|
+
files = watchedFile.split(',').map((s) => s.replace(/\\/g, '/').trim()).filter(Boolean);
|
|
69
111
|
}
|
|
70
|
-
// Fallback to watched if files array is empty
|
|
71
112
|
if (files.length === 0) {
|
|
72
113
|
if (typeof watched === 'string' && watched.trim() !== '') {
|
|
73
|
-
const parts = watched.split(',').map(s => s.replace(/\\/g, '/').trim());
|
|
74
|
-
files = parts.filter(
|
|
114
|
+
const parts = watched.split(',').map((s) => s.replace(/\\/g, '/').trim());
|
|
115
|
+
files = parts.filter(looksLikeUrl);
|
|
75
116
|
}
|
|
76
117
|
else if (Array.isArray(watched)) {
|
|
77
|
-
files = watched
|
|
118
|
+
files = watched
|
|
119
|
+
.map((item) => {
|
|
78
120
|
if (item instanceof File)
|
|
79
121
|
return item;
|
|
80
122
|
if (typeof item === 'object' && item !== null) {
|
|
81
|
-
const
|
|
82
|
-
const key = possibleKeys.find((k) => item[k]);
|
|
123
|
+
const key = ['image', 'url', 'path', 'filePath'].find((k) => item[k]);
|
|
83
124
|
return key ? item[key]?.replace(/\\/g, '/').trim() : undefined;
|
|
84
125
|
}
|
|
85
126
|
if (typeof item === 'string') {
|
|
@@ -87,59 +128,41 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
87
128
|
return looksLikeUrl(str) ? str : undefined;
|
|
88
129
|
}
|
|
89
130
|
return undefined;
|
|
90
|
-
})
|
|
131
|
+
})
|
|
132
|
+
.filter(Boolean);
|
|
91
133
|
}
|
|
92
134
|
}
|
|
93
|
-
if (files.length > 0)
|
|
135
|
+
if (files.length > 0)
|
|
94
136
|
setSelectedFiles(files);
|
|
95
|
-
}
|
|
96
137
|
}, [watched, watchedFile]);
|
|
97
138
|
const isError = !!props.errors?.[props.item.name];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
pdf: ['pdf'],
|
|
101
|
-
image: ['jpg', 'jpeg', 'png'],
|
|
102
|
-
all: ['pdf', 'jpg', 'jpeg', 'png', 'xls', 'xlsx', 'doc', 'docx', 'zip'],
|
|
103
|
-
zip: ['zip'],
|
|
104
|
-
document: ['doc', 'docx', 'pdf'],
|
|
105
|
-
};
|
|
106
|
-
const getValidExtensions = () => {
|
|
139
|
+
// Derived once per fileType change
|
|
140
|
+
const validExtensions = useMemo(() => {
|
|
107
141
|
const fileType = props.item.fileType;
|
|
108
|
-
if (!fileType)
|
|
109
|
-
return
|
|
110
|
-
}
|
|
142
|
+
if (!fileType)
|
|
143
|
+
return EXTENSION_MAP.all;
|
|
111
144
|
if (Array.isArray(fileType)) {
|
|
112
|
-
|
|
113
|
-
const hasPredefinedKeys = fileType.some((item) => extensionMap[item]);
|
|
145
|
+
const hasPredefinedKeys = fileType.some((t) => EXTENSION_MAP[t]);
|
|
114
146
|
if (hasPredefinedKeys) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else {
|
|
122
|
-
// Treat as direct extension
|
|
123
|
-
extensions.add(item);
|
|
124
|
-
}
|
|
147
|
+
const exts = new Set();
|
|
148
|
+
fileType.forEach((t) => {
|
|
149
|
+
if (EXTENSION_MAP[t])
|
|
150
|
+
EXTENSION_MAP[t].forEach((e) => exts.add(e));
|
|
151
|
+
else
|
|
152
|
+
exts.add(t);
|
|
125
153
|
});
|
|
126
|
-
return Array.from(
|
|
154
|
+
return Array.from(exts);
|
|
127
155
|
}
|
|
128
|
-
// All items are direct extensions
|
|
129
156
|
return fileType;
|
|
130
157
|
}
|
|
131
|
-
return
|
|
132
|
-
};
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
return extensions.map((ext) => `.${ext}`).join(',');
|
|
136
|
-
};
|
|
137
|
-
const handleFileChange = (event) => {
|
|
158
|
+
return EXTENSION_MAP[fileType] ?? EXTENSION_MAP.all;
|
|
159
|
+
}, [props.item.fileType]);
|
|
160
|
+
const accept = useMemo(() => validExtensions.map((e) => `.${e}`).join(','), [validExtensions]);
|
|
161
|
+
const handleFileChange = useCallback((event) => {
|
|
138
162
|
const files = event.target.files;
|
|
139
163
|
if (!files)
|
|
140
164
|
return;
|
|
141
165
|
const fileArray = Array.from(files);
|
|
142
|
-
const validExtensions = getValidExtensions();
|
|
143
166
|
const invalidFile = fileArray.find((file) => {
|
|
144
167
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
145
168
|
return !ext || !validExtensions.includes(ext);
|
|
@@ -159,15 +182,12 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
159
182
|
props.setValue(props.item.name + 'File', []);
|
|
160
183
|
return;
|
|
161
184
|
}
|
|
162
|
-
let combinedFiles
|
|
185
|
+
let combinedFiles;
|
|
163
186
|
if (isSingleFileMode) {
|
|
164
|
-
// only 1 file allowed → replace
|
|
165
187
|
combinedFiles = fileArray;
|
|
166
188
|
}
|
|
167
189
|
else {
|
|
168
|
-
// append to existing
|
|
169
190
|
combinedFiles = [...selectedFiles, ...fileArray];
|
|
170
|
-
// limit by maxLength
|
|
171
191
|
if (props.item.maxLength && combinedFiles.length > props.item.maxLength) {
|
|
172
192
|
props.item?.handleFileError?.(`You can only upload up to ${props.item.maxLength} files`);
|
|
173
193
|
event.target.value = '';
|
|
@@ -182,26 +202,25 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
182
202
|
props.setValue(props.item.name + 'File', combinedFiles);
|
|
183
203
|
props.clearErrors(props.item.name);
|
|
184
204
|
setSelectedFiles(combinedFiles);
|
|
185
|
-
setCurrentFileIndex(combinedFiles.length - fileArray.length);
|
|
205
|
+
setCurrentFileIndex(combinedFiles.length - fileArray.length);
|
|
186
206
|
props.item.onChangeSetFileFn?.(combinedFiles);
|
|
187
|
-
};
|
|
188
|
-
const handlePreviewClick = () => {
|
|
207
|
+
}, [validExtensions, isSingleFileMode, selectedFiles, props.item, props.setValue, props.clearErrors]);
|
|
208
|
+
const handlePreviewClick = useCallback(() => {
|
|
189
209
|
setCurrentFileIndex(0);
|
|
190
210
|
setOpen(true);
|
|
191
|
-
};
|
|
192
|
-
const handleClose = () => setOpen(false);
|
|
193
|
-
const handlePrevious = () => {
|
|
194
|
-
setCurrentFileIndex((prev) => prev > 0 ? prev - 1 : selectedFiles.length - 1);
|
|
195
|
-
};
|
|
196
|
-
const handleNext = () => {
|
|
197
|
-
setCurrentFileIndex((prev) => prev < selectedFiles.length - 1 ? prev + 1 : 0);
|
|
198
|
-
};
|
|
199
|
-
const handleDeleteFile = (indexToDelete) => {
|
|
200
|
-
const updatedFiles = selectedFiles.filter((_,
|
|
211
|
+
}, []);
|
|
212
|
+
const handleClose = useCallback(() => setOpen(false), []);
|
|
213
|
+
const handlePrevious = useCallback(() => {
|
|
214
|
+
setCurrentFileIndex((prev) => (prev > 0 ? prev - 1 : selectedFiles.length - 1));
|
|
215
|
+
}, [selectedFiles.length]);
|
|
216
|
+
const handleNext = useCallback(() => {
|
|
217
|
+
setCurrentFileIndex((prev) => (prev < selectedFiles.length - 1 ? prev + 1 : 0));
|
|
218
|
+
}, [selectedFiles.length]);
|
|
219
|
+
const handleDeleteFile = useCallback((indexToDelete) => {
|
|
220
|
+
const updatedFiles = selectedFiles.filter((_, i) => i !== indexToDelete);
|
|
201
221
|
setSelectedFiles(updatedFiles);
|
|
202
|
-
if (inputRef.current)
|
|
222
|
+
if (inputRef.current)
|
|
203
223
|
inputRef.current.value = '';
|
|
204
|
-
}
|
|
205
224
|
if (updatedFiles.length === 0) {
|
|
206
225
|
props.setValue(props.item.name, null);
|
|
207
226
|
props.setValue(props.item.name + 'File', []);
|
|
@@ -216,59 +235,21 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
216
235
|
if (currentFileIndex >= updatedFiles.length) {
|
|
217
236
|
setCurrentFileIndex(Math.max(0, updatedFiles.length - 1));
|
|
218
237
|
}
|
|
219
|
-
props.item.onChangeSetFileFn
|
|
220
|
-
|
|
221
|
-
if (updatedFiles.length === 0) {
|
|
238
|
+
props.item.onChangeSetFileFn?.(updatedFiles);
|
|
239
|
+
if (updatedFiles.length === 0)
|
|
222
240
|
setOpen(false);
|
|
223
|
-
|
|
224
|
-
};
|
|
225
|
-
const isImageFile = (file) => {
|
|
226
|
-
if (!file)
|
|
227
|
-
return false;
|
|
228
|
-
const ext = typeof file === 'string'
|
|
229
|
-
? file.split('.').pop()?.toLowerCase()
|
|
230
|
-
: file.name?.split('.').pop()?.toLowerCase();
|
|
231
|
-
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext || '');
|
|
232
|
-
};
|
|
233
|
-
const isPdfFile = (file) => {
|
|
234
|
-
if (!file)
|
|
235
|
-
return false;
|
|
236
|
-
const name = typeof file === 'string' ? file : (file.name || '');
|
|
237
|
-
return name.toLowerCase().endsWith('.pdf');
|
|
238
|
-
};
|
|
239
|
-
const isExcelFile = (file) => {
|
|
240
|
-
if (!file)
|
|
241
|
-
return false;
|
|
242
|
-
const name = typeof file === 'string' ? file : (file.name || '');
|
|
243
|
-
return /\.xlsx?$/.test(name.toLowerCase());
|
|
244
|
-
};
|
|
245
|
-
const isWordFile = (file) => {
|
|
246
|
-
if (!file)
|
|
247
|
-
return false;
|
|
248
|
-
const name = typeof file === 'string' ? file : (file.name || '');
|
|
249
|
-
return /\.(doc|docx)$/.test(name.toLowerCase());
|
|
250
|
-
};
|
|
251
|
-
const getObjectUrl = (file) => {
|
|
252
|
-
if (!file)
|
|
253
|
-
return '';
|
|
254
|
-
return typeof file === 'string' ? file : URL.createObjectURL(file);
|
|
255
|
-
};
|
|
241
|
+
}, [selectedFiles, currentFileIndex, props.item, props.setValue]);
|
|
256
242
|
return (jsxs(Fragment, { children: [jsxs(Box, { paddingLeft: "4px", sx: { ...props.item.sx, position: 'relative' }, children: [props.item?.label && (jsx(Box, { sx: { fontSize: '10px' }, children: renderLabel(variant, props, true) })), jsxs(Box, { display: "flex", alignItems: "center", gap: 1, children: [jsxs(Button, { variant: "outlined", component: "label", startIcon: jsx(UploadFileIcon, {}), sx: { flex: 1, textTransform: 'none' }, color: isError ? 'error' : 'primary', children: [selectedFiles.length > 0
|
|
257
243
|
? isSingleFileMode
|
|
258
244
|
? '1 file selected'
|
|
259
245
|
: `${selectedFiles.length} file(s) selected`
|
|
260
246
|
: isSingleFileMode
|
|
261
247
|
? 'Upload File'
|
|
262
|
-
: 'Upload Files', jsx("input", { type: "file", hidden: true, multiple: !isSingleFileMode, accept:
|
|
263
|
-
position: 'absolute',
|
|
264
|
-
top: '50%',
|
|
265
|
-
left: '50%',
|
|
248
|
+
: 'Upload Files', jsx("input", { type: "file", hidden: true, multiple: !isSingleFileMode, accept: accept, ref: inputRef, onChange: handleFileChange })] }), jsx(Box, { sx: { width: 36, display: 'flex', justifyContent: 'center' }, children: props.item.showFileCarousel && selectedFiles.length > 0 ? (jsx(IconButton, { onClick: handlePreviewClick, color: "primary", size: "small", "aria-label": "preview selected files", children: jsx(VisibilityIcon, {}) })) : null })] })] }), jsx(FormBottomField, { ...props }), jsx(Modal, { open: open, onClose: handleClose, children: jsxs(Box, { sx: {
|
|
249
|
+
position: 'absolute', top: '50%', left: '50%',
|
|
266
250
|
transform: 'translate(-50%, -50%)',
|
|
267
|
-
width: 600,
|
|
268
|
-
|
|
269
|
-
borderRadius: 2,
|
|
270
|
-
boxShadow: 24,
|
|
271
|
-
p: 3,
|
|
251
|
+
width: 600, bgcolor: 'background.paper',
|
|
252
|
+
borderRadius: 2, boxShadow: 24, p: 3,
|
|
272
253
|
}, children: [jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", children: [jsx(Typography, { variant: "h6", children: "File Preview" }), jsxs(Box, { display: "flex", gap: 1, children: [jsx(IconButton, { onClick: () => handleDeleteFile(currentFileIndex), color: "error", title: "Delete current file", children: jsx(DeleteIcon, {}) }), jsx(IconButton, { onClick: handleClose, children: jsx(CloseIcon, {}) })] })] }), jsx(Box, { mt: 2, children: selectedFiles.length > 0 && (jsxs(Fragment, { children: [jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "center", gap: 2, children: [selectedFiles.length > 1 && !isSingleFileMode ? (jsx(IconButton, { onClick: handlePrevious, children: jsx(NavigateBeforeIcon, {}) })) : null, jsx(Box, { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", sx: { p: 2, minHeight: '400px', width: '100%' }, children: (() => {
|
|
273
254
|
const currentFile = selectedFiles[currentFileIndex];
|
|
274
255
|
if (!currentFile) {
|
|
@@ -277,31 +258,21 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
277
258
|
const fileName = typeof currentFile === 'string'
|
|
278
259
|
? currentFile.split('/').pop() || currentFile
|
|
279
260
|
: (currentFile.name || '');
|
|
280
|
-
// Images
|
|
281
261
|
if (isImageFile(currentFile)) {
|
|
282
|
-
return (jsx("img", { alt: "Preview", src: getObjectUrl(currentFile), style: {
|
|
283
|
-
maxHeight: '400px',
|
|
284
|
-
maxWidth: '100%',
|
|
285
|
-
objectFit: 'contain',
|
|
286
|
-
borderRadius: 8,
|
|
287
|
-
} }));
|
|
262
|
+
return (jsx("img", { alt: "Preview", src: getObjectUrl(currentFile), style: { maxHeight: '400px', maxWidth: '100%', objectFit: 'contain', borderRadius: 8 } }));
|
|
288
263
|
}
|
|
289
|
-
// PDF inline viewer
|
|
290
264
|
if (isPdfFile(currentFile)) {
|
|
291
265
|
const url = getObjectUrl(currentFile);
|
|
292
|
-
return (jsx(Box, { width: "100%", height: 420, children: jsx("object", { data: url, type: "application/pdf", width: "100%", height: "100%", children: jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 1, children: [jsx(PictureAsPdfIcon, { color: "error" }), jsx(Typography, { variant: "body2", children: "Preview not available.
|
|
266
|
+
return (jsx(Box, { width: "100%", height: 420, children: jsx("object", { data: url, type: "application/pdf", width: "100%", height: "100%", children: jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 1, children: [jsx(PictureAsPdfIcon, { color: "error" }), jsx(Typography, { variant: "body2", children: "Preview not available." }), jsx(Button, { size: "small", variant: "outlined", href: url, target: "_blank", rel: "noreferrer", children: "Open PDF" })] }) }) }));
|
|
293
267
|
}
|
|
294
|
-
// Excel preview placeholder with actions
|
|
295
268
|
if (isExcelFile(currentFile)) {
|
|
296
269
|
const url = getObjectUrl(currentFile);
|
|
297
270
|
return (jsxs(Box, { textAlign: "center", children: [jsx(TableChartIcon, { color: "primary", sx: { fontSize: 64, mb: 1 } }), jsx(Typography, { variant: "body1", gutterBottom: true, children: fileName }), jsx(Button, { size: "small", variant: "outlined", href: url, target: "_blank", rel: "noreferrer", children: "Open" })] }));
|
|
298
271
|
}
|
|
299
|
-
// Word preview placeholder with actions
|
|
300
272
|
if (isWordFile(currentFile)) {
|
|
301
273
|
const url = getObjectUrl(currentFile);
|
|
302
274
|
return (jsxs(Box, { textAlign: "center", children: [jsx(DescriptionIcon, { color: "primary", sx: { fontSize: 64, mb: 1 } }), jsx(Typography, { variant: "body1", gutterBottom: true, children: fileName }), jsx(Button, { size: "small", variant: "outlined", href: url, target: "_blank", rel: "noreferrer", children: "Open" })] }));
|
|
303
275
|
}
|
|
304
|
-
// Fallback: show name
|
|
305
276
|
return jsx(Typography, { variant: "body1", children: fileName });
|
|
306
277
|
})() }), selectedFiles.length > 1 && !isSingleFileMode ? (jsx(IconButton, { onClick: handleNext, children: jsx(NavigateNextIcon, {}) })) : null] }), selectedFiles.length > 1 && !isSingleFileMode && (jsx(Box, { display: "flex", justifyContent: "center", mt: 2, children: jsxs(Typography, { variant: "body2", color: "text.secondary", children: [currentFileIndex + 1, " of ", selectedFiles.length] }) }))] })) })] }) })] }));
|
|
307
278
|
};
|
|
@@ -1,44 +1,6 @@
|
|
|
1
1
|
import { FormRenderProps, VariantProps } from '../../../utils';
|
|
2
|
-
/**
|
|
3
|
-
* FormNumberField Component
|
|
4
|
-
*
|
|
5
|
-
* A numeric input field component that integrates with react-hook-form.
|
|
6
|
-
* This component is used for integer inputs, phone numbers, and pincodes.
|
|
7
|
-
* It restricts input to numeric characters only.
|
|
8
|
-
*
|
|
9
|
-
* @component
|
|
10
|
-
* @param {Object} props - The component props
|
|
11
|
-
* @param {FormRenderProps} props.props - Form rendering properties from parent form
|
|
12
|
-
* @param {VariantProps} props.variant - UI variant style ('standard', 'outlined', etc.)
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* // Basic usage in FormRender
|
|
16
|
-
* <FormNumberField props={formRenderProps} variant="outlined" />
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* // Usage with specific configuration
|
|
20
|
-
* const formItem = {
|
|
21
|
-
* name: "phoneNumber",
|
|
22
|
-
* label: "Phone Number",
|
|
23
|
-
* inputType: "number",
|
|
24
|
-
* required: true,
|
|
25
|
-
* maxLength: 10
|
|
26
|
-
* };
|
|
27
|
-
* <FormNumberField
|
|
28
|
-
* props={{
|
|
29
|
-
* item: formItem,
|
|
30
|
-
* control: formMethods.control,
|
|
31
|
-
* errors: formMethods.formState.errors
|
|
32
|
-
* }}
|
|
33
|
-
* variant="outlined"
|
|
34
|
-
* />
|
|
35
|
-
*/
|
|
36
2
|
declare const FormNumberField: ({ props, variant }: {
|
|
37
3
|
props: FormRenderProps;
|
|
38
4
|
variant: VariantProps;
|
|
39
5
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
40
|
-
/**
|
|
41
|
-
* Export the FormNumberField component for use in the form renderer
|
|
42
|
-
* This component is used for number, pincode, and phoneNumber input types
|
|
43
|
-
*/
|
|
44
6
|
export default FormNumberField;
|