tp-react-elements-dev 1.13.1 → 1.13.3
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/components/Form/FormRender.esm.js +8 -8
- package/dist/components/FormComponents/FileUpload/MultiFileWithPreview.esm.js +33 -98
- package/dist/components/FormComponents/FormTextField/FormTextField.esm.js +1 -1
- package/dist/components/FormComponents/Select/MultiSelectAutocomplete.esm.js +173 -13
- package/dist/components/FormComponents/index.esm.js +6 -20
- package/package.json +1 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import React__default, { lazy, Suspense } from 'react';
|
|
3
|
-
import FormCheckBox from '../FormComponents/FormCheckBox/FormCheckBox.esm.js';
|
|
4
|
-
import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
|
|
5
|
-
import FormNumberField from '../FormComponents/FormNumberField/FormNumberField.esm.js';
|
|
6
|
-
import FormNumberFieldDecimal from '../FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js';
|
|
7
|
-
import FormRadioGroup from '../FormComponents/FormRadioGroup/FormRadioGroup.esm.js';
|
|
8
|
-
import FormTextAreaField from '../FormComponents/FormTextAreaField/FormTextAreaField.esm.js';
|
|
9
|
-
import FormTextField from '../FormComponents/FormTextField/FormTextField.esm.js';
|
|
10
|
-
import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js';
|
|
11
3
|
import SingleSelect from '../FormComponents/Select/SingleSelect.esm.js';
|
|
12
4
|
import MultiSelectAutocomplete from '../FormComponents/Select/MultiSelectAutocomplete.esm.js';
|
|
13
5
|
import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
|
|
14
6
|
import FormActiveSwitch from './FormActiveSwitch.esm.js';
|
|
15
7
|
import FormTextFieldWithSelect from '../FormComponents/FormTextFieldWithSelect/FormTextFieldWithSelect.esm.js';
|
|
8
|
+
import FormTextAreaField from '../FormComponents/FormTextAreaField/FormTextAreaField.esm.js';
|
|
9
|
+
import FormCheckBox from '../FormComponents/FormCheckBox/FormCheckBox.esm.js';
|
|
10
|
+
import FormRadioGroup from '../FormComponents/FormRadioGroup/FormRadioGroup.esm.js';
|
|
11
|
+
import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
|
|
12
|
+
import FormNumberFieldDecimal from '../FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js';
|
|
13
|
+
import FormNumberField from '../FormComponents/FormNumberField/FormNumberField.esm.js';
|
|
14
|
+
import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js';
|
|
15
|
+
import FormTextField from '../FormComponents/FormTextField/FormTextField.esm.js';
|
|
16
16
|
|
|
17
17
|
// Lazy-loaded heavy components
|
|
18
18
|
const RichTextEditorWrapper = lazy(() => import('../RichTextEditor/index.esm.js').then((m) => ({ default: m.RichTextEditorWrapper })));
|
|
@@ -21,28 +21,21 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
21
21
|
const [open, setOpen] = useState(false);
|
|
22
22
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
23
23
|
const [currentFileIndex, setCurrentFileIndex] = useState(0);
|
|
24
|
+
// Clamp preview index when selection changes to avoid undefined access
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (selectedFiles.length === 0) {
|
|
27
|
+
setCurrentFileIndex(0);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (currentFileIndex > selectedFiles.length - 1) {
|
|
31
|
+
setCurrentFileIndex(selectedFiles.length - 1);
|
|
32
|
+
}
|
|
33
|
+
else if (currentFileIndex < 0) {
|
|
34
|
+
setCurrentFileIndex(0);
|
|
35
|
+
}
|
|
36
|
+
}, [selectedFiles, currentFileIndex]);
|
|
24
37
|
// Determine if this is single or multiple file mode
|
|
25
38
|
const isSingleFileMode = props.item.maxLength === 1;
|
|
26
|
-
// useEffect(() => {
|
|
27
|
-
// if (watched === null || watched === undefined) {
|
|
28
|
-
// if (inputRef.current) inputRef.current.value = '';
|
|
29
|
-
// }
|
|
30
|
-
// // Normalize backend-provided file(s) ONLY if they look like URLs
|
|
31
|
-
// const looksLikeUrl = (s: string) => /^(https?:|blob:|data:|\/)/i.test(s);
|
|
32
|
-
// if (typeof watched === 'string' && watched.trim() !== '') {
|
|
33
|
-
// const str = watched.replace(/\\/g, '/').trim();
|
|
34
|
-
// if (looksLikeUrl(str)) {
|
|
35
|
-
// setSelectedFiles([str]);
|
|
36
|
-
// }
|
|
37
|
-
// } else if (Array.isArray(watched) && typeof watched[0] === 'string') {
|
|
38
|
-
// const normalized = watched
|
|
39
|
-
// .map((f) => (f || '').toString().replace(/\\/g, '/').trim())
|
|
40
|
-
// .filter((f) => looksLikeUrl(f));
|
|
41
|
-
// if (normalized.length) {
|
|
42
|
-
// setSelectedFiles(normalized);
|
|
43
|
-
// }
|
|
44
|
-
// }
|
|
45
|
-
// }, [watched]);
|
|
46
39
|
useEffect(() => {
|
|
47
40
|
if (watched === null || watched === undefined) {
|
|
48
41
|
if (inputRef.current)
|
|
@@ -80,77 +73,6 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
80
73
|
}
|
|
81
74
|
}, [watched]);
|
|
82
75
|
const isError = !!props.errors?.[props.item.name];
|
|
83
|
-
// const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
84
|
-
// const files = event.target.files;
|
|
85
|
-
// if (!files) return;
|
|
86
|
-
// const fileArray = Array.from(files);
|
|
87
|
-
// const allowedExtensions: Record<string, string[]> = {
|
|
88
|
-
// excel: ['xls', 'xlsx'],
|
|
89
|
-
// pdf: ['pdf'],
|
|
90
|
-
// image: ['jpg', 'jpeg', 'png'],
|
|
91
|
-
// all: ['pdf', 'jpg', 'jpeg', 'png', 'xls', 'xlsx', 'doc', 'docx', 'zip'],
|
|
92
|
-
// zip: ['zip'],
|
|
93
|
-
// document: ['doc', 'docx', 'pdf'],
|
|
94
|
-
// };
|
|
95
|
-
// const validExtensions =
|
|
96
|
-
// props.item.fileType === 'excel'
|
|
97
|
-
// ? allowedExtensions.excel
|
|
98
|
-
// : props.item.fileType === 'pdf'
|
|
99
|
-
// ? allowedExtensions.pdf
|
|
100
|
-
// : props.item.fileType === 'image'
|
|
101
|
-
// ? allowedExtensions.image
|
|
102
|
-
// : allowedExtensions.all;
|
|
103
|
-
// const invalidFile = fileArray.find((file) => {
|
|
104
|
-
// const ext = file.name.split('.').pop()?.toLowerCase();
|
|
105
|
-
// return !ext || !validExtensions.includes(ext);
|
|
106
|
-
// });
|
|
107
|
-
// if (invalidFile) {
|
|
108
|
-
// props.item?.handleFileError?.(
|
|
109
|
-
// `Please upload ${validExtensions.join(', ')} files only`
|
|
110
|
-
// );
|
|
111
|
-
// event.target.value = '';
|
|
112
|
-
// props.setValue(props.item.name, null);
|
|
113
|
-
// props.setValue(props.item.name + 'File', []);
|
|
114
|
-
// return;
|
|
115
|
-
// }
|
|
116
|
-
// const largeFile = fileArray.find((file) => file.size > 20000000);
|
|
117
|
-
// if (largeFile) {
|
|
118
|
-
// props.item?.handleFileError?.('File size should be less than 20MB');
|
|
119
|
-
// event.target.value = '';
|
|
120
|
-
// props.setValue(props.item.name, null);
|
|
121
|
-
// props.setValue(props.item.name + 'File', []);
|
|
122
|
-
// return;
|
|
123
|
-
// }
|
|
124
|
-
// // Handle single vs multiple file mode
|
|
125
|
-
// let combinedFiles: (File | string)[];
|
|
126
|
-
// const shouldReplaceExisting =
|
|
127
|
-
// isSingleFileMode || (selectedFiles.length === 1 && typeof selectedFiles[0] === 'string');
|
|
128
|
-
// if (shouldReplaceExisting) {
|
|
129
|
-
// // Replace the existing (string URL or single file) with the new selection
|
|
130
|
-
// combinedFiles = fileArray;
|
|
131
|
-
// } else {
|
|
132
|
-
// // Append new files to existing list
|
|
133
|
-
// combinedFiles = [...selectedFiles, ...fileArray];
|
|
134
|
-
// // Check if maxLength is exceeded
|
|
135
|
-
// if (props.item.maxLength && combinedFiles.length > props.item.maxLength) {
|
|
136
|
-
// props.item?.handleFileError?.(
|
|
137
|
-
// `You can only upload up to ${props.item.maxLength} files`
|
|
138
|
-
// );
|
|
139
|
-
// event.target.value = '';
|
|
140
|
-
// return;
|
|
141
|
-
// }
|
|
142
|
-
// }
|
|
143
|
-
// const combinedFileNames = combinedFiles
|
|
144
|
-
// .map((f) => (f instanceof File ? f.name : f.split('/').pop()))
|
|
145
|
-
// .join(',');
|
|
146
|
-
// props.setValue(props.item.name, combinedFileNames);
|
|
147
|
-
// props.setValue(props.item.name + 'File', combinedFiles);
|
|
148
|
-
// setSelectedFiles(combinedFiles);
|
|
149
|
-
// // Move preview to newly selected file(s)
|
|
150
|
-
// const newIndex = shouldReplaceExisting ? 0 : Math.max(0, combinedFiles.length - fileArray.length);
|
|
151
|
-
// setCurrentFileIndex(newIndex);
|
|
152
|
-
// props.item.onChangeSetFileFn?.(combinedFiles as File[]);
|
|
153
|
-
// };
|
|
154
76
|
const handleFileChange = (event) => {
|
|
155
77
|
const files = event.target.files;
|
|
156
78
|
if (!files)
|
|
@@ -203,7 +125,8 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
203
125
|
}
|
|
204
126
|
}
|
|
205
127
|
const combinedFileNames = combinedFiles
|
|
206
|
-
.map((f) => (f instanceof File ? f.name : f.split('/').pop()))
|
|
128
|
+
.map((f) => (f instanceof File ? f.name : (f?.toString?.() || '').split('/').pop()))
|
|
129
|
+
.filter(Boolean)
|
|
207
130
|
.join(',');
|
|
208
131
|
props.setValue(props.item.name, combinedFileNames);
|
|
209
132
|
props.setValue(props.item.name + 'File', combinedFiles);
|
|
@@ -249,25 +172,34 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
249
172
|
}
|
|
250
173
|
};
|
|
251
174
|
const isImageFile = (file) => {
|
|
175
|
+
if (!file)
|
|
176
|
+
return false;
|
|
252
177
|
const ext = typeof file === 'string'
|
|
253
178
|
? file.split('.').pop()?.toLowerCase()
|
|
254
|
-
: file.name
|
|
179
|
+
: file.name?.split('.').pop()?.toLowerCase();
|
|
255
180
|
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext || '');
|
|
256
181
|
};
|
|
257
182
|
const isPdfFile = (file) => {
|
|
258
|
-
|
|
183
|
+
if (!file)
|
|
184
|
+
return false;
|
|
185
|
+
const name = typeof file === 'string' ? file : (file.name || '');
|
|
259
186
|
return name.toLowerCase().endsWith('.pdf');
|
|
260
187
|
};
|
|
261
188
|
const isExcelFile = (file) => {
|
|
262
|
-
|
|
189
|
+
if (!file)
|
|
190
|
+
return false;
|
|
191
|
+
const name = typeof file === 'string' ? file : (file.name || '');
|
|
263
192
|
return /\.xlsx?$/.test(name.toLowerCase());
|
|
264
193
|
};
|
|
265
194
|
const isWordFile = (file) => {
|
|
266
|
-
|
|
195
|
+
if (!file)
|
|
196
|
+
return false;
|
|
197
|
+
const name = typeof file === 'string' ? file : (file.name || '');
|
|
267
198
|
return /\.(doc|docx)$/.test(name.toLowerCase());
|
|
268
199
|
};
|
|
269
200
|
const getObjectUrl = (file) => {
|
|
270
|
-
|
|
201
|
+
if (!file)
|
|
202
|
+
return '';
|
|
271
203
|
return typeof file === 'string' ? file : URL.createObjectURL(file);
|
|
272
204
|
};
|
|
273
205
|
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
|
|
@@ -296,9 +228,12 @@ const MultiFileWithPreview = ({ props, variant, }) => {
|
|
|
296
228
|
p: 3,
|
|
297
229
|
}, 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: (() => {
|
|
298
230
|
const currentFile = selectedFiles[currentFileIndex];
|
|
231
|
+
if (!currentFile) {
|
|
232
|
+
return jsx(Typography, { variant: "body2", color: "text.secondary", children: "No file selected" });
|
|
233
|
+
}
|
|
299
234
|
const fileName = typeof currentFile === 'string'
|
|
300
235
|
? currentFile.split('/').pop() || currentFile
|
|
301
|
-
: currentFile.name;
|
|
236
|
+
: (currentFile.name || '');
|
|
302
237
|
// Images
|
|
303
238
|
if (isImageFile(currentFile)) {
|
|
304
239
|
return (jsx("img", { alt: "Preview", src: getObjectUrl(currentFile), style: {
|
|
@@ -64,7 +64,7 @@ const FormTextField = ({ props, variant }) => {
|
|
|
64
64
|
}
|
|
65
65
|
if (props?.item?.allowSpecialChars === false) {
|
|
66
66
|
const value = e.target.value;
|
|
67
|
-
e.target.value = value.replace(/[^a-zA-Z0-9]/g, '');
|
|
67
|
+
e.target.value = value.replace(/[^a-zA-Z0-9\s]/g, '');
|
|
68
68
|
}
|
|
69
69
|
if (props?.item?.allowNumbers === false) {
|
|
70
70
|
const value = e.target.value;
|
|
@@ -1,19 +1,153 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Autocomplete, TextField,
|
|
3
|
-
import { useMemo } from 'react';
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { Checkbox, Autocomplete, TextField, Box, Tooltip, Chip } from '@mui/material';
|
|
3
|
+
import { useState, useRef, useMemo, useCallback } from 'react';
|
|
4
4
|
import { useWatch, Controller } from 'react-hook-form';
|
|
5
5
|
import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
|
|
6
6
|
import 'dayjs';
|
|
7
7
|
import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
|
|
8
8
|
|
|
9
|
+
//region old code
|
|
10
|
+
// const MultiSelectAutocomplete = ({
|
|
11
|
+
// props,
|
|
12
|
+
// variant,
|
|
13
|
+
// }: {
|
|
14
|
+
// props: FormRenderProps;
|
|
15
|
+
// variant: VariantProps;
|
|
16
|
+
// }) => {
|
|
17
|
+
// const isError = !!props.errors?.[props.item.name];
|
|
18
|
+
// const watchedValue = useWatch({ control: props.control, name: props.item.name });
|
|
19
|
+
// const normalizeToStringArray = (value: unknown): string[] => {
|
|
20
|
+
// if (Array.isArray(value)) {
|
|
21
|
+
// return value.map((v) => String(v));
|
|
22
|
+
// }
|
|
23
|
+
// if (typeof value === 'string') {
|
|
24
|
+
// return value
|
|
25
|
+
// .split(',')
|
|
26
|
+
// .map((v) => v.trim())
|
|
27
|
+
// .filter((v) => v.length > 0);
|
|
28
|
+
// }
|
|
29
|
+
// if (value == null) return [];
|
|
30
|
+
// return [String(value)];
|
|
31
|
+
// };
|
|
32
|
+
// const normalizedSelectedValues = normalizeToStringArray(watchedValue);
|
|
33
|
+
// const isShowBorderError = isError && normalizedSelectedValues.length === 0;
|
|
34
|
+
// const options: OptionItem[] = (props.item.options || []) as OptionItem[];
|
|
35
|
+
// const selectedOptions: OptionItem[] = useMemo(() => {
|
|
36
|
+
// const valueSet = new Set(normalizedSelectedValues.map((v) => String(v)));
|
|
37
|
+
// return options.filter((opt) => valueSet.has(String(opt.value)));
|
|
38
|
+
// }, [normalizedSelectedValues, options]);
|
|
39
|
+
// return (
|
|
40
|
+
// <Controller
|
|
41
|
+
// control={props.control}
|
|
42
|
+
// name={props.item.name}
|
|
43
|
+
// key={props.item.name}
|
|
44
|
+
// render={({ field }) => (
|
|
45
|
+
// <>
|
|
46
|
+
// {renderLabel(variant, props)}
|
|
47
|
+
// <Autocomplete
|
|
48
|
+
// {...field}
|
|
49
|
+
// multiple
|
|
50
|
+
// disableCloseOnSelect
|
|
51
|
+
// id={`${props.item.name}-autocomplete-multi`}
|
|
52
|
+
// options={options}
|
|
53
|
+
// value={selectedOptions}
|
|
54
|
+
// getOptionLabel={(option) => String(option.label)}
|
|
55
|
+
// isOptionEqualToValue={(option, value) => String(option.value) === String(value.value)}
|
|
56
|
+
// onChange={(_, newValue) => {
|
|
57
|
+
// const joined = (newValue || []).map((o: OptionItem) => String(o.value)).join(',');
|
|
58
|
+
// props.setValue(props.item.name, joined);
|
|
59
|
+
// props?.item?.onChangeFn && props?.item?.onChangeFn(joined);
|
|
60
|
+
// props?.clearErrors && props?.clearErrors(props?.item?.name);
|
|
61
|
+
// }}
|
|
62
|
+
// onBlur={(e: any) => {
|
|
63
|
+
// props?.item?.onBlurFn && props?.item?.onBlurFn(e);
|
|
64
|
+
// }}
|
|
65
|
+
// size="small"
|
|
66
|
+
// renderOption={(renderProps, option, { selected }) => (
|
|
67
|
+
// <li {...renderProps} style={{ fontSize: '11px' }}>
|
|
68
|
+
// <Checkbox
|
|
69
|
+
// checked={selected}
|
|
70
|
+
// size="small"
|
|
71
|
+
// sx={{ mr: 1, p: '2px' }}
|
|
72
|
+
// />
|
|
73
|
+
// {option.label}
|
|
74
|
+
// </li>
|
|
75
|
+
// )}
|
|
76
|
+
// renderTags={(tagValue) => (
|
|
77
|
+
// tagValue.length ? <span>{tagValue.map((o: OptionItem) => String(o.label)).join(' , ')}</span> : null
|
|
78
|
+
// )}
|
|
79
|
+
// sx={{
|
|
80
|
+
// '& .MuiAutocomplete-input': {
|
|
81
|
+
// padding: '0px 0px 0px 5px !important',
|
|
82
|
+
// },
|
|
83
|
+
// '& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root,.css-8edmr2-MuiFormLabel-root-MuiInputLabel-root': {
|
|
84
|
+
// top: '-3px',
|
|
85
|
+
// },
|
|
86
|
+
// '& .MuiAutocomplete-option': {
|
|
87
|
+
// fontSize: '11px',
|
|
88
|
+
// zIndex: 2000,
|
|
89
|
+
// },
|
|
90
|
+
// ...props.item.sx,
|
|
91
|
+
// }}
|
|
92
|
+
// ListboxProps={{
|
|
93
|
+
// onScroll: (event: React.SyntheticEvent) => {
|
|
94
|
+
// const listboxNode = event.currentTarget as HTMLElement;
|
|
95
|
+
// if (listboxNode.scrollTop + listboxNode.clientHeight === listboxNode.scrollHeight) {
|
|
96
|
+
// // Infinite scroll hook can be wired here
|
|
97
|
+
// }
|
|
98
|
+
// },
|
|
99
|
+
// }}
|
|
100
|
+
// disabled={props.item.disable}
|
|
101
|
+
// slotProps={{
|
|
102
|
+
// popper: {
|
|
103
|
+
// sx: {
|
|
104
|
+
// zIndex: 1401,
|
|
105
|
+
// },
|
|
106
|
+
// },
|
|
107
|
+
// listbox: {
|
|
108
|
+
// sx: {
|
|
109
|
+
// fontSize: '11px',
|
|
110
|
+
// },
|
|
111
|
+
// },
|
|
112
|
+
// }}
|
|
113
|
+
// renderInput={(params) => (
|
|
114
|
+
// <TextField
|
|
115
|
+
// {...params}
|
|
116
|
+
// placeholder={props.item.placeholder}
|
|
117
|
+
// error={isShowBorderError}
|
|
118
|
+
// label={
|
|
119
|
+
// variant !== 'standard'
|
|
120
|
+
// ? `${props.item.label}${props.item.required ? ' *' : ''}`
|
|
121
|
+
// : ''
|
|
122
|
+
// }
|
|
123
|
+
// inputProps={{
|
|
124
|
+
// ...params.inputProps,
|
|
125
|
+
// 'aria-labelledby': `${props.item.name}-label`,
|
|
126
|
+
// 'aria-describedby': `${props.item.name}-helper ${props.item.name}-error`,
|
|
127
|
+
// 'aria-required': props.item.required ? true : undefined,
|
|
128
|
+
// }}
|
|
129
|
+
// />
|
|
130
|
+
// )}
|
|
131
|
+
// />
|
|
132
|
+
// <FormBottomField {...props} />
|
|
133
|
+
// </>
|
|
134
|
+
// )}
|
|
135
|
+
// />
|
|
136
|
+
// );
|
|
137
|
+
// };
|
|
138
|
+
//endregion
|
|
9
139
|
const MultiSelectAutocomplete = ({ props, variant, }) => {
|
|
10
140
|
const isError = !!props.errors?.[props.item.name];
|
|
11
141
|
const watchedValue = useWatch({ control: props.control, name: props.item.name });
|
|
142
|
+
const [open, setOpen] = useState(false);
|
|
143
|
+
const tagsContainerRef = useRef(null);
|
|
12
144
|
const normalizeToStringArray = (value) => {
|
|
13
145
|
if (Array.isArray(value)) {
|
|
146
|
+
// already array of primitives/strings
|
|
14
147
|
return value.map((v) => String(v));
|
|
15
148
|
}
|
|
16
149
|
if (typeof value === 'string') {
|
|
150
|
+
// if backend provided CSV string, convert to array
|
|
17
151
|
return value
|
|
18
152
|
.split(',')
|
|
19
153
|
.map((v) => v.trim())
|
|
@@ -21,26 +155,52 @@ const MultiSelectAutocomplete = ({ props, variant, }) => {
|
|
|
21
155
|
}
|
|
22
156
|
if (value == null)
|
|
23
157
|
return [];
|
|
158
|
+
// for any other types, coerce to single-element array
|
|
24
159
|
return [String(value)];
|
|
25
160
|
};
|
|
26
161
|
const normalizedSelectedValues = normalizeToStringArray(watchedValue);
|
|
27
162
|
const isShowBorderError = isError && normalizedSelectedValues.length === 0;
|
|
28
|
-
const options = (props.item.options || []);
|
|
163
|
+
const options = useMemo(() => (props.item.options || []), [props.item.options]);
|
|
29
164
|
const selectedOptions = useMemo(() => {
|
|
30
165
|
const valueSet = new Set(normalizedSelectedValues.map((v) => String(v)));
|
|
31
166
|
return options.filter((opt) => valueSet.has(String(opt.value)));
|
|
32
167
|
}, [normalizedSelectedValues, options]);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
168
|
+
// Derived overflow flag without extra renders
|
|
169
|
+
const isOverflowing = (selectedOptions || []).length > 2;
|
|
170
|
+
// Stable callbacks to avoid re-renders
|
|
171
|
+
const getOptionLabelCb = useCallback((option) => String(option.label), []);
|
|
172
|
+
const isOptionEqualCb = useCallback((option, value) => String(option.value) === String(value.value), []);
|
|
173
|
+
const renderOptionCb = useCallback((renderProps, option, state) => (jsxs("li", { ...renderProps, style: { fontSize: '11px' }, children: [jsx(Checkbox, { checked: state.selected, size: "small", sx: { mr: 1, p: '2px' } }), option.label] })), []);
|
|
174
|
+
const chipSx = useMemo(() => ({ mr: 0.5, height: 20, fontSize: '10px', borderRadius: '10px', '& .MuiChip-label': { px: 0.75, py: 0 } }), []);
|
|
175
|
+
const selectedTooltip = useMemo(() => selectedOptions.map(o => String(o.label)).join(', '), [selectedOptions]);
|
|
176
|
+
const handleChange = useCallback((_, newValue) => {
|
|
177
|
+
const values = Array.isArray(newValue)
|
|
178
|
+
? newValue.map((o) => String(o.value))
|
|
179
|
+
: [];
|
|
180
|
+
// Minimize work during selection for snappier UX
|
|
181
|
+
props.setValue(props.item.name, values, { shouldValidate: false, shouldDirty: true });
|
|
182
|
+
props?.item?.onChangeFn && props?.item?.onChangeFn(values.join(','));
|
|
183
|
+
props?.clearErrors && props?.clearErrors(props?.item?.name);
|
|
184
|
+
}, [props]);
|
|
185
|
+
const handleBlur = useCallback((e) => {
|
|
186
|
+
props?.item?.onBlurFn && props?.item?.onBlurFn(e);
|
|
187
|
+
}, [props]);
|
|
188
|
+
return (jsx(Controller, { control: props.control, name: props.item.name, render: ({ field: { ref, onBlur: fieldOnBlur } }) => (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(Autocomplete, { ref: ref, open: open, onOpen: () => setOpen(true), onClose: () => setOpen(false), multiple: true, disableCloseOnSelect: true, id: `${props.item.name}-autocomplete-multi`, options: options, value: selectedOptions, getOptionLabel: getOptionLabelCb, isOptionEqualToValue: isOptionEqualCb, onChange: handleChange, onBlur: (e) => {
|
|
189
|
+
fieldOnBlur();
|
|
190
|
+
handleBlur(e);
|
|
191
|
+
}, size: "small", disablePortal: false, autoHighlight: true, selectOnFocus: true, clearOnBlur: false, renderTags: (tagValue, getTagProps) => (jsx(Box, { ref: tagsContainerRef, sx: {
|
|
192
|
+
'& :hover': {
|
|
193
|
+
cursor: 'pointer',
|
|
194
|
+
},
|
|
195
|
+
display: 'flex', flexWrap: 'nowrap', overflow: 'hidden', alignItems: 'center'
|
|
196
|
+
}, children: isOverflowing ? (jsx(Tooltip, { title: selectedTooltip, placement: "top", children: jsx(Chip, { size: "small", label: `${tagValue.length} selected`, onClick: () => setOpen(true), clickable: true, sx: chipSx }) })) : (tagValue.map((option, index) => (jsx(Tooltip, { title: String(option?.label ?? option), placement: "top", children: jsx(Chip, { ...getTagProps({ index }), size: "small", label: String(option?.label ?? option), sx: chipSx }) }, String(option?.value ?? option?.label ?? index))))) })), renderOption: renderOptionCb, sx: {
|
|
41
197
|
'& .MuiAutocomplete-input': {
|
|
42
198
|
padding: '0px 0px 0px 5px !important',
|
|
43
199
|
},
|
|
200
|
+
'& .MuiAutocomplete-inputRoot': {
|
|
201
|
+
flexWrap: 'nowrap', // keep search beside chip
|
|
202
|
+
alignItems: 'center'
|
|
203
|
+
},
|
|
44
204
|
'& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root,.css-8edmr2-MuiFormLabel-root-MuiInputLabel-root': {
|
|
45
205
|
top: '-3px',
|
|
46
206
|
},
|
|
@@ -57,7 +217,7 @@ const MultiSelectAutocomplete = ({ props, variant, }) => {
|
|
|
57
217
|
}, disabled: props.item.disable, slotProps: {
|
|
58
218
|
popper: {
|
|
59
219
|
sx: {
|
|
60
|
-
zIndex:
|
|
220
|
+
zIndex: 20000,
|
|
61
221
|
},
|
|
62
222
|
},
|
|
63
223
|
listbox: {
|
|
@@ -1,38 +1,24 @@
|
|
|
1
|
+
export { default as SingleSelect } from './Select/SingleSelect.esm.js';
|
|
2
|
+
export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete.esm.js';
|
|
3
|
+
export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi.esm.js';
|
|
4
|
+
export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
|
|
1
5
|
export { default as FormNumberField } from './FormNumberField/FormNumberField.esm.js';
|
|
2
6
|
export { default as FormNumberFieldDecimal } from './FormNumberField/FormNumberFieldDecimal.esm.js';
|
|
3
7
|
export { default as FormTextAreaField } from './FormTextAreaField/FormTextAreaField.esm.js';
|
|
4
8
|
export { default as FormTextField } from './FormTextField/FormTextField.esm.js';
|
|
5
9
|
export { default as PasswordField } from './PasswordField/PasswordField.esm.js';
|
|
6
10
|
export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
|
|
7
|
-
export { default as SingleSelect } from './Select/SingleSelect.esm.js';
|
|
8
|
-
export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete.esm.js';
|
|
9
|
-
export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi.esm.js';
|
|
10
|
-
export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
|
|
11
11
|
export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
|
|
12
12
|
export { default as FormRadioGroup } from './FormRadioGroup/FormRadioGroup.esm.js';
|
|
13
13
|
export { default as DatepickerWrapperV2 } from './DatePicker/DatepickerWrapperV2.esm.js';
|
|
14
14
|
export { default as MonthPicker } from './DatePicker/MonthPicker.esm.js';
|
|
15
15
|
export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
|
|
16
|
-
export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
|
|
17
16
|
export { default as TimePicker } from './TimePicker/TimePicker.esm.js';
|
|
18
17
|
export { default as FormRenderFileUpload } from './FileUpload/FormRenderFileUpload.esm.js';
|
|
19
18
|
export { default as FormRenderMultiFileUpload } from './FileUpload/FormRenderMultiFileUpload.esm.js';
|
|
20
|
-
import 'react/jsx-runtime';
|
|
21
|
-
import '@mui/material';
|
|
22
|
-
import 'react';
|
|
23
|
-
import 'react-hook-form';
|
|
24
|
-
import 'dayjs';
|
|
25
|
-
export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
|
|
26
|
-
import '@mui/icons-material/Visibility';
|
|
27
|
-
import '@mui/icons-material/Close';
|
|
28
|
-
import '@mui/icons-material/NavigateBefore';
|
|
29
|
-
import '@mui/icons-material/NavigateNext';
|
|
30
|
-
import '@mui/icons-material/Delete';
|
|
31
|
-
import '@mui/icons-material/UploadFile';
|
|
32
|
-
import '@mui/icons-material/PictureAsPdf';
|
|
33
|
-
import '@mui/icons-material/TableChart';
|
|
34
|
-
import '@mui/icons-material/Description';
|
|
35
19
|
export { default as RichTextEditor } from './RichTextEditor/RichTextEditor.esm.js';
|
|
36
20
|
export { default as RichTextEditorWrapper } from './RichTextEditor/RichTextEditorWrapper.esm.js';
|
|
21
|
+
export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
|
|
37
22
|
export { default as FormErrorField } from './FormErrorField/FormErrorField.esm.js';
|
|
38
23
|
export { default as HelperText } from './HelperText/HelperText.esm.js';
|
|
24
|
+
export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
|