tp-react-elements 1.3.18 → 1.3.19
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/_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.esm7.js +2 -2
- package/dist/components/FormComponents/Select/MultiSelectAutocomplete.d.ts +21 -1
- package/dist/components/FormComponents/Select/MultiSelectAutocomplete.esm.js +117 -197
- package/dist/components/FormComponents/Select/SingleSelectSearchApi.d.ts +46 -1
- package/dist/components/FormComponents/Select/SingleSelectSearchApi.esm.js +180 -64
- 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,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var InputRangeField = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { InputRangeField as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DayCell = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DayCell as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Month = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Month as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateInput = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateInput as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DefinedRange = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DefinedRange as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateRange = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRange as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateRangePicker = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRangePicker as __exports };
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { FormRenderProps, VariantProps } from '../../../utils';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Multi-select autocomplete backed by `react-hook-form`.
|
|
4
|
+
*
|
|
5
|
+
* Key behaviours:
|
|
6
|
+
* - Values stored as **`string[]`** in the form field.
|
|
7
|
+
* - Accepts CSV strings and normalises them automatically (backward-compat
|
|
8
|
+
* with older API responses).
|
|
9
|
+
* - Shows individual chips for up to `maxChips` selections, then collapses
|
|
10
|
+
* into a single **"N selected"** chip with a tooltip listing all labels.
|
|
11
|
+
* - The "N selected" chip has a **delete button** that clears all selections.
|
|
12
|
+
* - **`onChangeFn` is only called on genuine user interactions** (select /
|
|
13
|
+
* remove / clear). Pre-populating the form with `defaultValues` or calling
|
|
14
|
+
* `reset()` does **not** trigger `onChangeFn`, preventing unwanted API calls.
|
|
15
|
+
* - `onChangeFn` receives a `string[]` (not a CSV string).
|
|
16
|
+
*/
|
|
17
|
+
declare const MultiSelectAutocomplete: ({ props, variant, maxChips, }: {
|
|
3
18
|
props: FormRenderProps;
|
|
4
19
|
variant: VariantProps;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum number of chips shown individually before collapsing into
|
|
22
|
+
* "N selected". Default: `2`.
|
|
23
|
+
*/
|
|
24
|
+
maxChips?: number;
|
|
5
25
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
6
26
|
export default MultiSelectAutocomplete;
|
|
@@ -6,233 +6,153 @@ import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
|
|
|
6
6
|
import 'dayjs';
|
|
7
7
|
import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
139
|
-
const MultiSelectAutocomplete = ({ props, variant, }) => {
|
|
140
|
-
const isError = !!props.errors?.[props.item.name];
|
|
141
|
-
const watchedValue = useWatch({ control: props.control, name: props.item.name });
|
|
9
|
+
// ─── Module-level helpers ─────────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Normalises any value shape coming from react-hook-form to a plain string[].
|
|
12
|
+
*
|
|
13
|
+
* | Input shape | Output |
|
|
14
|
+
* |----------------------|-------------------|
|
|
15
|
+
* | `string[]` | as-is (coerced) |
|
|
16
|
+
* | `'1,2,3'` (CSV) | `['1','2','3']` |
|
|
17
|
+
* | `null` / `undefined` | `[]` |
|
|
18
|
+
* | any other scalar | `[String(value)]` |
|
|
19
|
+
*/
|
|
20
|
+
const normalizeToStringArray = (value) => {
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.map((v) => String(v));
|
|
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)
|
|
30
|
+
return [];
|
|
31
|
+
return [String(value)];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* MUI Autocomplete change reasons that represent a genuine user interaction.
|
|
35
|
+
* `'blur'` is deliberately excluded because it fires programmatically when
|
|
36
|
+
* focus moves away (e.g. on form reset / programmatic setValue), which would
|
|
37
|
+
* trigger `onChangeFn` → API calls for pre-selected values.
|
|
38
|
+
*/
|
|
39
|
+
const USER_CHANGE_REASONS = new Set(['selectOption', 'removeOption', 'clear']);
|
|
40
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Multi-select autocomplete backed by `react-hook-form`.
|
|
43
|
+
*
|
|
44
|
+
* Key behaviours:
|
|
45
|
+
* - Values stored as **`string[]`** in the form field.
|
|
46
|
+
* - Accepts CSV strings and normalises them automatically (backward-compat
|
|
47
|
+
* with older API responses).
|
|
48
|
+
* - Shows individual chips for up to `maxChips` selections, then collapses
|
|
49
|
+
* into a single **"N selected"** chip with a tooltip listing all labels.
|
|
50
|
+
* - The "N selected" chip has a **delete button** that clears all selections.
|
|
51
|
+
* - **`onChangeFn` is only called on genuine user interactions** (select /
|
|
52
|
+
* remove / clear). Pre-populating the form with `defaultValues` or calling
|
|
53
|
+
* `reset()` does **not** trigger `onChangeFn`, preventing unwanted API calls.
|
|
54
|
+
* - `onChangeFn` receives a `string[]` (not a CSV string).
|
|
55
|
+
*/
|
|
56
|
+
const MultiSelectAutocomplete = ({ props, variant, maxChips = 2, }) => {
|
|
57
|
+
// ── Destructure stable references from props ───────────────────────────────
|
|
58
|
+
const { control, errors, item, setValue, clearErrors } = props;
|
|
59
|
+
const { name, options: rawOptions, onChangeFn, onBlurFn, disable, placeholder, required, label, sx: itemSx, } = item;
|
|
60
|
+
const isError = !!errors?.[name];
|
|
61
|
+
const watchedValue = useWatch({ control, name });
|
|
142
62
|
const [open, setOpen] = useState(false);
|
|
143
63
|
const tagsContainerRef = useRef(null);
|
|
144
|
-
const normalizeToStringArray = (value) => {
|
|
145
|
-
if (Array.isArray(value)) {
|
|
146
|
-
// already array of primitives/strings
|
|
147
|
-
return value.map((v) => String(v));
|
|
148
|
-
}
|
|
149
|
-
if (typeof value === 'string') {
|
|
150
|
-
// if backend provided CSV string, convert to array
|
|
151
|
-
return value
|
|
152
|
-
.split(',')
|
|
153
|
-
.map((v) => v.trim())
|
|
154
|
-
.filter((v) => v.length > 0);
|
|
155
|
-
}
|
|
156
|
-
if (value == null)
|
|
157
|
-
return [];
|
|
158
|
-
// for any other types, coerce to single-element array
|
|
159
|
-
return [String(value)];
|
|
160
|
-
};
|
|
161
64
|
const normalizedSelectedValues = normalizeToStringArray(watchedValue);
|
|
162
65
|
const isShowBorderError = isError && normalizedSelectedValues.length === 0;
|
|
163
|
-
const options = useMemo(() => (
|
|
66
|
+
const options = useMemo(() => (rawOptions || []), [rawOptions]);
|
|
164
67
|
const selectedOptions = useMemo(() => {
|
|
165
68
|
const valueSet = new Set(normalizedSelectedValues.map((v) => String(v)));
|
|
166
69
|
return options.filter((opt) => valueSet.has(String(opt.value)));
|
|
167
70
|
}, [normalizedSelectedValues, options]);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Stable callbacks to avoid re-renders
|
|
71
|
+
const isOverflowing = selectedOptions.length > maxChips;
|
|
72
|
+
// ── Stable callbacks ───────────────────────────────────────────────────────
|
|
171
73
|
const getOptionLabelCb = useCallback((option) => String(option.label), []);
|
|
172
74
|
const isOptionEqualCb = useCallback((option, value) => String(option.value) === String(value.value), []);
|
|
173
75
|
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(() => ({
|
|
175
|
-
|
|
176
|
-
|
|
76
|
+
const chipSx = useMemo(() => ({
|
|
77
|
+
mr: 0.5,
|
|
78
|
+
height: 20,
|
|
79
|
+
fontSize: '10px',
|
|
80
|
+
borderRadius: '10px',
|
|
81
|
+
'& .MuiChip-label': { px: 0.75, py: 0 },
|
|
82
|
+
}), []);
|
|
83
|
+
const selectedTooltip = useMemo(() => selectedOptions.map((o) => String(o.label)).join(', '), [selectedOptions]);
|
|
84
|
+
/**
|
|
85
|
+
* Change handler — only fires `onChangeFn` for genuine user interactions.
|
|
86
|
+
*
|
|
87
|
+
* MUI passes a `reason` string as the third argument:
|
|
88
|
+
* 'selectOption' | 'removeOption' | 'clear' → user action → fire onChangeFn
|
|
89
|
+
* 'blur' → programmatic → suppress onChangeFn
|
|
90
|
+
*
|
|
91
|
+
* This prevents `onChangeFn` → API calls when the field is pre-populated
|
|
92
|
+
* via `defaultValues`, `reset()`, or an external `setValue()`.
|
|
93
|
+
*/
|
|
94
|
+
const handleChange = useCallback((_, newValue, reason) => {
|
|
177
95
|
const values = Array.isArray(newValue)
|
|
178
96
|
? newValue.map((o) => String(o.value))
|
|
179
97
|
: [];
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
98
|
+
setValue(name, values, { shouldValidate: false, shouldDirty: true });
|
|
99
|
+
if (USER_CHANGE_REASONS.has(reason) && onChangeFn) {
|
|
100
|
+
onChangeFn(values); // passes string[] — not CSV
|
|
101
|
+
}
|
|
102
|
+
clearErrors?.(name);
|
|
103
|
+
}, [setValue, clearErrors, name, onChangeFn]);
|
|
104
|
+
const handleBlur = useCallback((e) => { onBlurFn?.(e); }, [onBlurFn]);
|
|
105
|
+
/** Clears ALL selections — wired to the delete button on the overflow chip. */
|
|
106
|
+
const handleClearAll = useCallback(() => {
|
|
107
|
+
setValue(name, [], { shouldValidate: false, shouldDirty: true });
|
|
108
|
+
onChangeFn?.([]); // user explicitly cleared all → fire callback
|
|
109
|
+
clearErrors?.(name);
|
|
110
|
+
}, [setValue, clearErrors, name, onChangeFn]);
|
|
111
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
112
|
+
return (jsx(Controller, { control: control, name: 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: `${name}-autocomplete-multi`, options: options, value: selectedOptions, getOptionLabel: getOptionLabelCb, isOptionEqualToValue: isOptionEqualCb, getOptionDisabled: (option) => option.disable, onChange: handleChange, onBlur: (e) => {
|
|
189
113
|
fieldOnBlur();
|
|
190
114
|
handleBlur(e);
|
|
191
115
|
}, size: "small", disablePortal: false, autoHighlight: true, selectOnFocus: true, clearOnBlur: false, renderTags: (tagValue, getTagProps) => (jsx(Box, { ref: tagsContainerRef, sx: {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
116
|
+
display: 'flex',
|
|
117
|
+
flexWrap: 'nowrap',
|
|
118
|
+
overflow: 'hidden',
|
|
119
|
+
alignItems: 'center',
|
|
120
|
+
'& :hover': { cursor: 'pointer' },
|
|
121
|
+
}, children: isOverflowing ? (
|
|
122
|
+
// ── Collapsed chip: shows "N selected" with tooltip ──────
|
|
123
|
+
// onDelete wires a delete (×) button that clears all selections
|
|
124
|
+
jsx(Tooltip, { title: selectedTooltip, placement: "top", children: jsx(Chip, { size: "small", label: `${tagValue.length} selected`, onClick: () => setOpen(true), onDelete: handleClearAll, clickable: true, sx: chipSx }) })) : (
|
|
125
|
+
// ── Individual chips: up to maxChips items ───────────────
|
|
126
|
+
tagValue.map((option, index) => {
|
|
127
|
+
const { key: tagKey, ...tagProps } = getTagProps({ index });
|
|
128
|
+
return (jsx(Tooltip, { title: String(option?.label ?? option), placement: "top", children: jsx(Chip, { ...tagProps, size: "small", label: String(option?.label ?? option), sx: chipSx }) }, tagKey ?? String(option?.value ?? option?.label ?? index)));
|
|
129
|
+
})) })), renderOption: renderOptionCb, sx: {
|
|
197
130
|
'& .MuiAutocomplete-input': {
|
|
198
131
|
padding: '0px 0px 0px 5px !important',
|
|
199
132
|
},
|
|
200
133
|
'& .MuiAutocomplete-inputRoot': {
|
|
201
|
-
flexWrap: 'nowrap',
|
|
202
|
-
alignItems: 'center'
|
|
134
|
+
flexWrap: 'nowrap',
|
|
135
|
+
alignItems: 'center',
|
|
203
136
|
},
|
|
204
|
-
'& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root
|
|
137
|
+
'& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root, .css-8edmr2-MuiFormLabel-root-MuiInputLabel-root': {
|
|
205
138
|
top: '-3px',
|
|
206
139
|
},
|
|
207
140
|
'& .MuiAutocomplete-option': {
|
|
208
141
|
fontSize: '11px',
|
|
209
142
|
zIndex: 2000,
|
|
210
143
|
},
|
|
211
|
-
...
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}, disabled: props.item.disable, slotProps: {
|
|
218
|
-
popper: {
|
|
219
|
-
sx: {
|
|
220
|
-
zIndex: 20000,
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
listbox: {
|
|
224
|
-
sx: {
|
|
225
|
-
fontSize: '11px',
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
}, renderInput: (params) => (jsx(TextField, { ...params, placeholder: props.item.placeholder, error: isShowBorderError, label: variant !== 'standard'
|
|
229
|
-
? `${props.item.label}${props.item.required ? ' *' : ''}`
|
|
144
|
+
...itemSx,
|
|
145
|
+
}, disabled: disable, slotProps: {
|
|
146
|
+
popper: { sx: { zIndex: 20000 } },
|
|
147
|
+
listbox: { sx: { fontSize: '11px' } },
|
|
148
|
+
}, renderInput: (params) => (jsx(TextField, { ...params, placeholder: placeholder, error: isShowBorderError, label: variant !== 'standard'
|
|
149
|
+
? `${label}${required ? ' *' : ''}`
|
|
230
150
|
: '', inputProps: {
|
|
231
151
|
...params.inputProps,
|
|
232
|
-
'aria-labelledby': `${
|
|
233
|
-
'aria-describedby': `${
|
|
234
|
-
'aria-required':
|
|
235
|
-
} })) }), jsx(FormBottomField, { ...props })] })) }
|
|
152
|
+
'aria-labelledby': `${name}-label`,
|
|
153
|
+
'aria-describedby': `${name}-helper ${name}-error`,
|
|
154
|
+
'aria-required': required ? true : undefined,
|
|
155
|
+
} })) }), jsx(FormBottomField, { ...props })] })) }));
|
|
236
156
|
};
|
|
237
157
|
|
|
238
158
|
export { MultiSelectAutocomplete as default };
|
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
import { FormRenderProps, VariantProps } from '../../../utils';
|
|
2
|
-
|
|
2
|
+
export interface SearchOption {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string | number;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Single-select autocomplete that fetches options from a remote API.
|
|
9
|
+
*
|
|
10
|
+
* ### Authentication
|
|
11
|
+
* Prefer `fetchOptions` — a consumer-supplied async function that handles auth
|
|
12
|
+
* internally and returns `SearchOption[]`. Falls back to `item.api` +
|
|
13
|
+
* `item.jwtHeader` via axios when `fetchOptions` is not provided.
|
|
14
|
+
*
|
|
15
|
+
* ### Pre-selected values (edit mode)
|
|
16
|
+
* Pass the pre-selected option in `item.options` so the label displays
|
|
17
|
+
* immediately without a round-trip search. If `item.options` is empty and
|
|
18
|
+
* `autoFill` is true, the component seeds the search input with the form value
|
|
19
|
+
* so options load on mount.
|
|
20
|
+
*
|
|
21
|
+
* ### API call guard
|
|
22
|
+
* Searches fire only after `minSearchLength` chars AND after the `debounceMs`
|
|
23
|
+
* debounce delay — preventing a request per keystroke.
|
|
24
|
+
*/
|
|
25
|
+
declare const SingleSelectSearchApi: ({ props, variant, minSearchLength, debounceMs, fetchOptions, }: {
|
|
3
26
|
props: FormRenderProps;
|
|
4
27
|
variant: VariantProps;
|
|
28
|
+
/**
|
|
29
|
+
* Minimum characters before the search fires.
|
|
30
|
+
* Default: `3`.
|
|
31
|
+
*/
|
|
32
|
+
minSearchLength?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Debounce delay in milliseconds.
|
|
35
|
+
* Default: `300`.
|
|
36
|
+
*/
|
|
37
|
+
debounceMs?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Preferred way to load options. Takes precedence over `item.api`.
|
|
40
|
+
* Receives the current search text and an AbortSignal for cancellation.
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* fetchOptions={async (text, signal) => {
|
|
44
|
+
* const res = await myAuthenticatedApi.search(text, { signal });
|
|
45
|
+
* return res.items.map(r => ({ label: r.name, value: r.id }));
|
|
46
|
+
* }}
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
fetchOptions?: (searchText: string, signal: AbortSignal) => Promise<SearchOption[]>;
|
|
5
50
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
6
51
|
export default SingleSelectSearchApi;
|
|
@@ -1,85 +1,201 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { Autocomplete,
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { Controller } from 'react-hook-form';
|
|
2
|
+
import { Autocomplete, Tooltip, TextField, Paper, Typography } from '@mui/material';
|
|
5
3
|
import '../../../node_modules/axios/index.esm.js';
|
|
4
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
5
|
+
import { useWatch, Controller } from 'react-hook-form';
|
|
6
6
|
import 'dayjs';
|
|
7
7
|
import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
|
|
8
8
|
import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
|
|
9
9
|
import axios from '../../../node_modules/axios/lib/axios.esm.js';
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// ─── Module-level helpers ─────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Appends a query parameter correctly regardless of whether the base URL
|
|
14
|
+
* already contains a query string (`?` or `&`).
|
|
15
|
+
*
|
|
16
|
+
* Old code used `&` unconditionally which broke clean base URLs.
|
|
17
|
+
*/
|
|
18
|
+
const appendQueryParam = (url, key, value) => {
|
|
19
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
20
|
+
return `${url}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
21
|
+
};
|
|
22
|
+
/** True when the error is a request cancellation (AbortController / axios). */
|
|
23
|
+
const isCancellation = (err) => axios.isCancel?.(err) ||
|
|
24
|
+
(err instanceof Error && (err.name === 'AbortError' || err.name === 'CanceledError'));
|
|
25
|
+
// ─── Debounce hook ────────────────────────────────────────────────────────────
|
|
26
|
+
function useDebounce(value, delayMs) {
|
|
27
|
+
const [debounced, setDebounced] = useState(value);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const id = setTimeout(() => setDebounced(value), delayMs);
|
|
30
|
+
return () => clearTimeout(id);
|
|
31
|
+
}, [value, delayMs]);
|
|
32
|
+
return debounced;
|
|
33
|
+
}
|
|
34
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* Single-select autocomplete that fetches options from a remote API.
|
|
37
|
+
*
|
|
38
|
+
* ### Authentication
|
|
39
|
+
* Prefer `fetchOptions` — a consumer-supplied async function that handles auth
|
|
40
|
+
* internally and returns `SearchOption[]`. Falls back to `item.api` +
|
|
41
|
+
* `item.jwtHeader` via axios when `fetchOptions` is not provided.
|
|
42
|
+
*
|
|
43
|
+
* ### Pre-selected values (edit mode)
|
|
44
|
+
* Pass the pre-selected option in `item.options` so the label displays
|
|
45
|
+
* immediately without a round-trip search. If `item.options` is empty and
|
|
46
|
+
* `autoFill` is true, the component seeds the search input with the form value
|
|
47
|
+
* so options load on mount.
|
|
48
|
+
*
|
|
49
|
+
* ### API call guard
|
|
50
|
+
* Searches fire only after `minSearchLength` chars AND after the `debounceMs`
|
|
51
|
+
* debounce delay — preventing a request per keystroke.
|
|
52
|
+
*/
|
|
53
|
+
const SingleSelectSearchApi = ({ props, variant, minSearchLength = 3, debounceMs = 300, fetchOptions, }) => {
|
|
54
|
+
// ── Stable references from props ──────────────────────────────────────────
|
|
55
|
+
const { control, errors, item, setValue, clearErrors } = props;
|
|
56
|
+
const { name, label, required, disable, placeholder, helperText, autoFIll: autoFill, // match the (typo'd) field name in FormSectionPropsItem
|
|
57
|
+
setLoadList, api, jwtHeader, searchValue: searchParamKey, sx: itemSx, } = item;
|
|
58
|
+
// ── Form value (reactive, no function call in deps) ───────────────────────
|
|
59
|
+
const formValue = useWatch({ control, name });
|
|
60
|
+
const isError = !!errors?.[name];
|
|
61
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
62
|
+
/** Text shown in the input (display). */
|
|
63
|
+
const [displayText, setDisplayText] = useState('');
|
|
64
|
+
/** Text that drives API calls — only updated when user types. */
|
|
65
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
66
|
+
const [options, setOptions] = useState([]);
|
|
16
67
|
const [isLoading, setIsLoading] = useState(false);
|
|
68
|
+
const [fetchError, setFetchError] = useState(null);
|
|
69
|
+
/** Stores the full selected option object so the label stays visible. */
|
|
70
|
+
const [selectedOption, setSelectedOption] = useState(null);
|
|
71
|
+
const debouncedSearch = useDebounce(searchQuery, debounceMs);
|
|
72
|
+
// ── Seed selectedOption from item.options (edit-mode preload) ─────────────
|
|
73
|
+
// If the parent supplies the pre-selected option in item.options, display it
|
|
74
|
+
// immediately on mount without a search round-trip.
|
|
17
75
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
76
|
+
if (!formValue || selectedOption?.value === formValue)
|
|
77
|
+
return;
|
|
78
|
+
const staticMatch = (item.options || []).find((o) => String(o.value) === String(formValue));
|
|
79
|
+
if (staticMatch) {
|
|
80
|
+
const opt = { label: String(staticMatch.label), value: staticMatch.value };
|
|
81
|
+
setSelectedOption(opt);
|
|
82
|
+
setDisplayText(String(staticMatch.label));
|
|
83
|
+
}
|
|
84
|
+
}, [formValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
85
|
+
// ── autoFill: auto-search for pre-selected value on mount ─────────────────
|
|
86
|
+
// When autoFill is true and no static option covers the pre-selected value,
|
|
87
|
+
// seed the search input so options load automatically on mount.
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (autoFill && formValue && !selectedOption) {
|
|
90
|
+
const text = String(formValue);
|
|
91
|
+
setDisplayText(text);
|
|
92
|
+
setSearchQuery(text);
|
|
93
|
+
}
|
|
94
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
95
|
+
// ── API fetch ─────────────────────────────────────────────────────────────
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!debouncedSearch || debouncedSearch.length < minSearchLength) {
|
|
98
|
+
setOptions([]);
|
|
99
|
+
return;
|
|
24
100
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
setIsLoading(true);
|
|
103
|
+
setFetchError(null);
|
|
104
|
+
const load = async () => {
|
|
105
|
+
try {
|
|
106
|
+
let data;
|
|
107
|
+
if (fetchOptions) {
|
|
108
|
+
// ── Preferred path: consumer-supplied function (auth-aware) ──
|
|
109
|
+
data = await fetchOptions(debouncedSearch, controller.signal);
|
|
110
|
+
}
|
|
111
|
+
else if (api) {
|
|
112
|
+
// ── Fallback: item.api + item.jwtHeader via axios ─────────────
|
|
113
|
+
const url = appendQueryParam(api, searchParamKey || 'searchvalue', debouncedSearch);
|
|
114
|
+
const res = await axios.get(url, { signal: controller.signal, headers: jwtHeader });
|
|
115
|
+
data = res.data;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
data = [];
|
|
119
|
+
}
|
|
120
|
+
setOptions(data);
|
|
121
|
+
setLoadList?.(data);
|
|
35
122
|
setIsLoading(false);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (isCancellation(err))
|
|
126
|
+
return; // silently ignore aborted requests
|
|
127
|
+
setFetchError('Failed to load options. Please try again.');
|
|
40
128
|
setIsLoading(false);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// }
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
load();
|
|
45
132
|
return () => controller.abort();
|
|
46
|
-
}, [
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
133
|
+
}, [debouncedSearch, minSearchLength, api, fetchOptions]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
134
|
+
// ── Stable callbacks ──────────────────────────────────────────────────────
|
|
135
|
+
const getOptionLabel = useCallback((option) => option?.label ?? '', []);
|
|
136
|
+
const isOptionEqualToValue = useCallback((option, value) => String(option?.value) === String(value?.value), []);
|
|
137
|
+
const handleChange = useCallback((_, newValue) => {
|
|
138
|
+
setSelectedOption(newValue);
|
|
139
|
+
setValue(name, newValue?.value ?? '');
|
|
140
|
+
clearErrors?.(name);
|
|
141
|
+
if (newValue) {
|
|
142
|
+
item.onChangeFn?.(newValue.value, newValue);
|
|
143
|
+
}
|
|
144
|
+
}, [setValue, clearErrors, name, item.onChangeFn]);
|
|
145
|
+
const handleInputChange = useCallback((_, newInputValue, reason) => {
|
|
146
|
+
setDisplayText(newInputValue);
|
|
147
|
+
if (reason === 'input') {
|
|
148
|
+
// User is typing → trigger debounced API search
|
|
149
|
+
setSearchQuery(newInputValue);
|
|
150
|
+
}
|
|
151
|
+
if (reason === 'clear') {
|
|
152
|
+
// User clicked the clear × → reset everything
|
|
153
|
+
setSearchQuery('');
|
|
154
|
+
setOptions([]);
|
|
155
|
+
setSelectedOption(null);
|
|
156
|
+
}
|
|
157
|
+
// 'reset' fires when MUI sets the input text after an option is selected
|
|
158
|
+
// — no search should trigger here
|
|
159
|
+
}, []);
|
|
160
|
+
// ── Resolved Autocomplete value ───────────────────────────────────────────
|
|
161
|
+
// Priority: selectedOption (if matches form value) → loaded options → null
|
|
162
|
+
const resolvedValue = useMemo(() => {
|
|
163
|
+
if (!formValue)
|
|
164
|
+
return null;
|
|
165
|
+
const fv = String(formValue);
|
|
166
|
+
if (selectedOption && String(selectedOption.value) === fv)
|
|
167
|
+
return selectedOption;
|
|
168
|
+
return options.find((o) => String(o.value) === fv) ?? null;
|
|
169
|
+
}, [formValue, selectedOption, options]);
|
|
170
|
+
// ── noOptionsText ─────────────────────────────────────────────────────────
|
|
171
|
+
const noOptionsText = useMemo(() => {
|
|
172
|
+
if (searchQuery.length < minSearchLength) {
|
|
173
|
+
return `Type at least ${minSearchLength} character${minSearchLength === 1 ? '' : 's'} to search`;
|
|
174
|
+
}
|
|
175
|
+
return fetchError ?? 'No results found';
|
|
176
|
+
}, [searchQuery, minSearchLength, fetchError]);
|
|
177
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
178
|
+
return (jsx(Controller, { control: control, name: name, render: ({ field: { ref, onBlur } }) => (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(Autocomplete, { ref: ref, value: resolvedValue, inputValue: displayText, onInputChange: handleInputChange, onChange: handleChange, onBlur: onBlur, options: options, getOptionLabel: getOptionLabel, isOptionEqualToValue: isOptionEqualToValue,
|
|
179
|
+
// Options come pre-filtered from the server — disable client-side filtering
|
|
180
|
+
filterOptions: (x) => x, loading: isLoading, disabled: disable, size: "small", noOptionsText: noOptionsText, sx: {
|
|
61
181
|
'& .MuiAutocomplete-input': {
|
|
62
182
|
padding: '0px 0px 0px 5px !important',
|
|
63
183
|
},
|
|
64
|
-
'& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root
|
|
184
|
+
'& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root': {
|
|
65
185
|
top: '-5px',
|
|
66
186
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
fontFamily: 'Roboto-Reg',
|
|
80
|
-
fontSize: '11px',
|
|
81
|
-
color: '#3651d3',
|
|
82
|
-
}, children: ["(", props?.item?.helperText, ")"] })), jsx(FormBottomField, { ...props })] })) }));
|
|
187
|
+
...itemSx,
|
|
188
|
+
}, slotProps: {
|
|
189
|
+
popper: { sx: { zIndex: 20000 } },
|
|
190
|
+
listbox: { sx: { fontSize: '11px' } },
|
|
191
|
+
}, PaperComponent: ({ children, ...paperProps }) => (jsx(Paper, { elevation: 4, ...paperProps, children: children })), renderInput: (params) => (jsx(Tooltip, { title: resolvedValue?.label ?? '', children: jsx(TextField, { ...params, error: isError && !formValue, label: variant !== 'standard'
|
|
192
|
+
? `${label}${required ? ' *' : ''}`
|
|
193
|
+
: '', placeholder: placeholder, inputProps: {
|
|
194
|
+
...params.inputProps,
|
|
195
|
+
'aria-labelledby': `${name}-label`,
|
|
196
|
+
'aria-describedby': `${name}-helper ${name}-error`,
|
|
197
|
+
'aria-required': required ? true : undefined,
|
|
198
|
+
} }) })) }), helperText && (jsx(Typography, { variant: "caption", color: "primary", sx: { mt: 0.25, display: 'block' }, children: helperText })), fetchError && (jsx(Typography, { variant: "caption", color: "error", sx: { mt: 0.25, display: 'block' }, children: fetchError })), jsx(FormBottomField, { ...props })] })) }));
|
|
83
199
|
};
|
|
84
200
|
|
|
85
201
|
export { SingleSelectSearchApi as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DateInput } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DateInput } from '../../../../../_virtual/index.esm14.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireClassnames } from '../../../../classnames/index.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DateRange } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DateRange } from '../../../../../_virtual/index.esm5.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireCalendar } from '../Calendar/index.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DateRangePicker } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm7.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireDateRange } from '../DateRange/index.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DayCell } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DayCell } from '../../../../../_virtual/index.esm12.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireClassnames } from '../../../../classnames/index.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DefinedRange } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DefinedRange } from '../../../../../_virtual/index.esm4.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireStyles } from '../../styles.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as InputRangeField } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as InputRangeField } from '../../../../../_virtual/index.esm11.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as Month } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as Month } from '../../../../../_virtual/index.esm13.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireDayCell } from '../DayCell/index.esm.js';
|