tp-react-elements 3.0.16 → 3.0.18
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.esm12.js +2 -2
- package/dist/_virtual/index.esm13.js +2 -2
- package/dist/_virtual/index.esm14.js +2 -2
- package/dist/_virtual/index.esm6.js +2 -2
- package/dist/_virtual/index.esm7.js +2 -2
- package/dist/components/DateRangePicker/DateRangePicker.esm.js +1 -1
- package/dist/components/Form/FormRender.esm.js +3 -0
- package/dist/components/FormComponents/Select/MultiSelectSearchApi.d.ts +88 -0
- package/dist/components/FormComponents/Select/MultiSelectSearchApi.esm.js +375 -0
- package/dist/components/FormComponents/Select/SingleSelectSearchApi.esm.js +2 -2
- package/dist/components/FormComponents/index.d.ts +1 -0
- package/dist/components/FormComponents/index.esm.js +1 -0
- package/dist/components/Select/index.d.ts +1 -0
- package/dist/components/Select/index.esm.js +1 -0
- package/dist/node_modules/react-date-range/dist/components/Calendar/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateInput/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/InputRangeField/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/Month/index.esm.js +1 -1
- package/dist/utils/Interface/FormInterface.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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 InputRangeField = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { InputRangeField 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 Calendar = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Calendar as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateRangePicker = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRangePicker as __exports };
|
|
@@ -83,7 +83,7 @@ const DateRangePickerComponent = ({ item, control, watch, setValue, getValues, c
|
|
|
83
83
|
const displayValue = state[0].startDate && state[0].endDate
|
|
84
84
|
? `${format(state[0].startDate, 'dd/MM/yyyy')} - ${format(state[0].endDate, 'dd/MM/yyyy')}`
|
|
85
85
|
: '';
|
|
86
|
-
const label = variant !== 'standard' ? `${item.label}${item.required ? ' *' : ''}` : '';
|
|
86
|
+
const label = variant !== 'standard' && item.labelVariant !== 'static' ? `${item.label}${item.required ? ' *' : ''}` : '';
|
|
87
87
|
return (jsxs(Fragment, { children: [jsx(Box, { onClick: handleClick, sx: { width: '100%' }, children: jsx(Controller, { name: fromDateName, control: control, render: ({ fieldState }) => (jsxs(Fragment, { children: [renderLabel(variant, {
|
|
88
88
|
item,
|
|
89
89
|
...rest,
|
|
@@ -17,6 +17,7 @@ import FormTextFieldWithSelect from '../FormComponents/FormTextFieldWithSelect/F
|
|
|
17
17
|
import DateRangePickerComponent from '../DateRangePicker/DateRangePicker.esm.js';
|
|
18
18
|
import RichTextEditorWrapper from '../FormComponents/RichTextEditor/RichTextEditorWrapper.esm.js';
|
|
19
19
|
import SingleSelectSearchApi from '../FormComponents/Select/SingleSelectSearchApi.esm.js';
|
|
20
|
+
import MultiSelectSearchApi from '../FormComponents/Select/MultiSelectSearchApi.esm.js';
|
|
20
21
|
import DatetimepickerWrapper from '../FormComponents/DatePicker/DatetimepickerWrapper.esm.js';
|
|
21
22
|
|
|
22
23
|
// Lazy-loaded heavy components
|
|
@@ -90,6 +91,8 @@ const RenderForm = (props) => {
|
|
|
90
91
|
return jsx(MultiSelectAutocomplete, { props: props, variant: variant });
|
|
91
92
|
case 'single-select-search':
|
|
92
93
|
return jsx(SingleSelectSearchApi, { props: props, variant: variant });
|
|
94
|
+
case 'multi-select-search':
|
|
95
|
+
return jsx(MultiSelectSearchApi, { props: props, variant: variant });
|
|
93
96
|
// Dropdown select (non auto-complete)
|
|
94
97
|
case 'select-v2':
|
|
95
98
|
case 'basic-select': // new alias
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { FormRenderProps, VariantProps } from '../../../utils';
|
|
2
|
+
import type { SearchOption } from './SingleSelectSearchApi';
|
|
3
|
+
export type { SearchOption };
|
|
4
|
+
/**
|
|
5
|
+
* Multi-select autocomplete that fetches options from a remote API — the
|
|
6
|
+
* multi-value counterpart of {@link SingleSelectSearchApi}, combining its
|
|
7
|
+
* debounced-search/API mechanics with `MultiSelectAutocomplete`'s chip +
|
|
8
|
+
* checkbox multi-select UI.
|
|
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
|
+
* ### Values
|
|
16
|
+
* Stored as **`string[]`** in the form field (same convention as
|
|
17
|
+
* `MultiSelectAutocomplete`). CSV strings are accepted and normalised
|
|
18
|
+
* automatically.
|
|
19
|
+
*
|
|
20
|
+
* ### Pre-selected values (edit mode)
|
|
21
|
+
* Pass every pre-selected option in `item.options` so their labels display
|
|
22
|
+
* immediately without a search round-trip — same pattern as
|
|
23
|
+
* `SingleSelectSearchApi`. Unlike the single-select version there is no
|
|
24
|
+
* `autoFIll` equivalent here: with N pre-selected ids there is no single
|
|
25
|
+
* search term to seed, so `item.options` is the only way to pre-populate
|
|
26
|
+
* labels. Any pre-selected id never seen via `item.options` or a live search
|
|
27
|
+
* still renders as a chip (falling back to the raw id as its label) rather
|
|
28
|
+
* than silently disappearing from the field.
|
|
29
|
+
*
|
|
30
|
+
* ### API call guard
|
|
31
|
+
* Searches fire only after `minSearchLength` chars AND after the
|
|
32
|
+
* `debounceMs` debounce delay — preventing a request per keystroke.
|
|
33
|
+
*
|
|
34
|
+
* ### onChangeFn
|
|
35
|
+
* Only fires on genuine user interactions (select / remove / clear-all) —
|
|
36
|
+
* not when the form is populated programmatically (`defaultValues`,
|
|
37
|
+
* `reset()`, external `setValue()`). Receives `(values: string[], options:
|
|
38
|
+
* SearchOption[])`.
|
|
39
|
+
*
|
|
40
|
+
* ### Reviewing/managing selections — the info icon
|
|
41
|
+
* Once at least one option is selected, an info icon appears at the end of
|
|
42
|
+
* the input (next to MUI's own clear/dropdown icons). Clicking it opens a
|
|
43
|
+
* popover listing every selected label with its own remove button — a
|
|
44
|
+
* click-based, keyboard/touch-friendly alternative to the "N selected" chip's
|
|
45
|
+
* hover-only tooltip, and the only way to review selections once they've
|
|
46
|
+
* collapsed past `maxChips` (the dropdown itself only shows the *current
|
|
47
|
+
* search's* results, not the full selection).
|
|
48
|
+
*
|
|
49
|
+
* ### maxSelections — capping how many can be picked
|
|
50
|
+
* When set, further options are disabled (greyed out, not just hidden) once
|
|
51
|
+
* the cap is reached — already-selected ones stay clickable so the user can
|
|
52
|
+
* still deselect. A "`N/max` selected" caption appears below the field, and
|
|
53
|
+
* the dropdown's empty-state message explains the cap once it's hit. Omitted
|
|
54
|
+
* by default — unlimited selections, unchanged from before this prop existed.
|
|
55
|
+
*/
|
|
56
|
+
declare const MultiSelectSearchApi: ({ props, variant, minSearchLength, debounceMs, fetchOptions, maxChips, maxSelections, }: {
|
|
57
|
+
props: FormRenderProps;
|
|
58
|
+
variant: VariantProps;
|
|
59
|
+
/** Minimum characters before the search fires. Default: `3`. */
|
|
60
|
+
minSearchLength?: number;
|
|
61
|
+
/** Debounce delay in milliseconds. Default: `300`. */
|
|
62
|
+
debounceMs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Preferred way to load options. Takes precedence over `item.api`.
|
|
65
|
+
* Receives the current search text and an AbortSignal for cancellation.
|
|
66
|
+
*
|
|
67
|
+
* ```tsx
|
|
68
|
+
* fetchOptions={async (text, signal) => {
|
|
69
|
+
* const res = await myAuthenticatedApi.search(text, { signal });
|
|
70
|
+
* return res.items.map(r => ({ label: r.name, value: r.id }));
|
|
71
|
+
* }}
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
fetchOptions?: (searchText: string, signal: AbortSignal) => Promise<SearchOption[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Maximum number of chips shown individually before collapsing into
|
|
77
|
+
* "N selected". Default: `2`.
|
|
78
|
+
*/
|
|
79
|
+
maxChips?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Maximum number of options that can be selected at once. When the cap is
|
|
82
|
+
* reached, unselected options are disabled (still visible, not removed
|
|
83
|
+
* from the list) until the user frees up a slot by deselecting one.
|
|
84
|
+
* Omitted by default — no limit.
|
|
85
|
+
*/
|
|
86
|
+
maxSelections?: number;
|
|
87
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
88
|
+
export default MultiSelectSearchApi;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { Autocomplete, TextField, InputAdornment, IconButton, Box, Tooltip, Chip, Checkbox, Paper, Popover, List, ListItem, ListItemText, Typography } from '@mui/material';
|
|
3
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
4
|
+
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
|
5
|
+
import '../../../node_modules/axios/index.esm.js';
|
|
6
|
+
import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
|
|
7
|
+
import { useWatch, Controller } from 'react-hook-form';
|
|
8
|
+
import 'dayjs';
|
|
9
|
+
import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
|
|
10
|
+
import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
|
|
11
|
+
import axios from '../../../node_modules/axios/lib/axios.esm.js';
|
|
12
|
+
|
|
13
|
+
// ─── Module-level helpers ─────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Appends a query parameter correctly regardless of whether the base URL
|
|
16
|
+
* already contains a query string (`?` or `&`).
|
|
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
|
+
/**
|
|
26
|
+
* Normalises any value shape coming from react-hook-form to a plain string[].
|
|
27
|
+
*
|
|
28
|
+
* | Input shape | Output |
|
|
29
|
+
* |-----------------------|-------------------|
|
|
30
|
+
* | `string[]` | as-is (coerced) |
|
|
31
|
+
* | `'1,2,3'` (CSV) | `['1','2','3']` |
|
|
32
|
+
* | `null` / `undefined` | `[]` |
|
|
33
|
+
* | any other scalar | `[String(value)]` |
|
|
34
|
+
*/
|
|
35
|
+
const normalizeToStringArray = (value) => {
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return value.map((v) => String(v));
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
return value
|
|
40
|
+
.split(',')
|
|
41
|
+
.map((v) => v.trim())
|
|
42
|
+
.filter((v) => v.length > 0);
|
|
43
|
+
}
|
|
44
|
+
if (value == null)
|
|
45
|
+
return [];
|
|
46
|
+
return [String(value)];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* MUI Autocomplete change reasons that represent a genuine user interaction.
|
|
50
|
+
* `'blur'` is deliberately excluded because it fires programmatically when
|
|
51
|
+
* focus moves away (e.g. on form reset / programmatic setValue), which would
|
|
52
|
+
* trigger `onChangeFn` → API calls for pre-selected values.
|
|
53
|
+
*/
|
|
54
|
+
const USER_CHANGE_REASONS = new Set(['selectOption', 'removeOption', 'clear']);
|
|
55
|
+
// ─── Debounce hook ────────────────────────────────────────────────────────────
|
|
56
|
+
function useDebounce(value, delayMs) {
|
|
57
|
+
const [debounced, setDebounced] = useState(value);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const id = setTimeout(() => setDebounced(value), delayMs);
|
|
60
|
+
return () => clearTimeout(id);
|
|
61
|
+
}, [value, delayMs]);
|
|
62
|
+
return debounced;
|
|
63
|
+
}
|
|
64
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Multi-select autocomplete that fetches options from a remote API — the
|
|
67
|
+
* multi-value counterpart of {@link SingleSelectSearchApi}, combining its
|
|
68
|
+
* debounced-search/API mechanics with `MultiSelectAutocomplete`'s chip +
|
|
69
|
+
* checkbox multi-select UI.
|
|
70
|
+
*
|
|
71
|
+
* ### Authentication
|
|
72
|
+
* Prefer `fetchOptions` — a consumer-supplied async function that handles auth
|
|
73
|
+
* internally and returns `SearchOption[]`. Falls back to `item.api` +
|
|
74
|
+
* `item.jwtHeader` via axios when `fetchOptions` is not provided.
|
|
75
|
+
*
|
|
76
|
+
* ### Values
|
|
77
|
+
* Stored as **`string[]`** in the form field (same convention as
|
|
78
|
+
* `MultiSelectAutocomplete`). CSV strings are accepted and normalised
|
|
79
|
+
* automatically.
|
|
80
|
+
*
|
|
81
|
+
* ### Pre-selected values (edit mode)
|
|
82
|
+
* Pass every pre-selected option in `item.options` so their labels display
|
|
83
|
+
* immediately without a search round-trip — same pattern as
|
|
84
|
+
* `SingleSelectSearchApi`. Unlike the single-select version there is no
|
|
85
|
+
* `autoFIll` equivalent here: with N pre-selected ids there is no single
|
|
86
|
+
* search term to seed, so `item.options` is the only way to pre-populate
|
|
87
|
+
* labels. Any pre-selected id never seen via `item.options` or a live search
|
|
88
|
+
* still renders as a chip (falling back to the raw id as its label) rather
|
|
89
|
+
* than silently disappearing from the field.
|
|
90
|
+
*
|
|
91
|
+
* ### API call guard
|
|
92
|
+
* Searches fire only after `minSearchLength` chars AND after the
|
|
93
|
+
* `debounceMs` debounce delay — preventing a request per keystroke.
|
|
94
|
+
*
|
|
95
|
+
* ### onChangeFn
|
|
96
|
+
* Only fires on genuine user interactions (select / remove / clear-all) —
|
|
97
|
+
* not when the form is populated programmatically (`defaultValues`,
|
|
98
|
+
* `reset()`, external `setValue()`). Receives `(values: string[], options:
|
|
99
|
+
* SearchOption[])`.
|
|
100
|
+
*
|
|
101
|
+
* ### Reviewing/managing selections — the info icon
|
|
102
|
+
* Once at least one option is selected, an info icon appears at the end of
|
|
103
|
+
* the input (next to MUI's own clear/dropdown icons). Clicking it opens a
|
|
104
|
+
* popover listing every selected label with its own remove button — a
|
|
105
|
+
* click-based, keyboard/touch-friendly alternative to the "N selected" chip's
|
|
106
|
+
* hover-only tooltip, and the only way to review selections once they've
|
|
107
|
+
* collapsed past `maxChips` (the dropdown itself only shows the *current
|
|
108
|
+
* search's* results, not the full selection).
|
|
109
|
+
*
|
|
110
|
+
* ### maxSelections — capping how many can be picked
|
|
111
|
+
* When set, further options are disabled (greyed out, not just hidden) once
|
|
112
|
+
* the cap is reached — already-selected ones stay clickable so the user can
|
|
113
|
+
* still deselect. A "`N/max` selected" caption appears below the field, and
|
|
114
|
+
* the dropdown's empty-state message explains the cap once it's hit. Omitted
|
|
115
|
+
* by default — unlimited selections, unchanged from before this prop existed.
|
|
116
|
+
*/
|
|
117
|
+
const MultiSelectSearchApi = ({ props, variant, minSearchLength = 3, debounceMs = 300, fetchOptions, maxChips = 2, maxSelections, }) => {
|
|
118
|
+
// ── Stable references from props ──────────────────────────────────────────
|
|
119
|
+
const { control, errors, item, setValue, clearErrors } = props;
|
|
120
|
+
const { name, label, required, disable, placeholder, helperText, setLoadList, api, jwtHeader, searchValue: searchParamKey, options: rawOptions, sx: itemSx, labelVariant, } = item;
|
|
121
|
+
// ── Form value (reactive, no function call in deps) ───────────────────────
|
|
122
|
+
const formValue = useWatch({ control, name });
|
|
123
|
+
const selectedIds = useMemo(() => normalizeToStringArray(formValue), [formValue]);
|
|
124
|
+
const isError = !!errors?.[name];
|
|
125
|
+
const isShowBorderError = isError && selectedIds.length === 0;
|
|
126
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
127
|
+
const [open, setOpen] = useState(false);
|
|
128
|
+
/** Anchor for the "view/manage selected items" popover — null when closed. */
|
|
129
|
+
const [infoAnchorEl, setInfoAnchorEl] = useState(null);
|
|
130
|
+
/** Text shown in the input (display). */
|
|
131
|
+
const [displayText, setDisplayText] = useState('');
|
|
132
|
+
/** Text that drives API calls — only updated when user types. */
|
|
133
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
134
|
+
/** Live search results for the current query. */
|
|
135
|
+
const [options, setOptions] = useState([]);
|
|
136
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
137
|
+
const [fetchError, setFetchError] = useState(null);
|
|
138
|
+
const tagsContainerRef = useRef(null);
|
|
139
|
+
/**
|
|
140
|
+
* Persisted label cache, keyed by `String(value)`, for every option ever
|
|
141
|
+
* seen — seeded from `item.options`, then grown as live search results and
|
|
142
|
+
* user selections come in. This is what lets a chip keep its label after a
|
|
143
|
+
* later search replaces `options` with unrelated results (`options` only
|
|
144
|
+
* ever reflects the *current* query's matches).
|
|
145
|
+
*/
|
|
146
|
+
const [knownOptions, setKnownOptions] = useState(() => {
|
|
147
|
+
const map = new Map();
|
|
148
|
+
(rawOptions || []).forEach((o) => map.set(String(o.value), { label: String(o.label), value: o.value }));
|
|
149
|
+
return map;
|
|
150
|
+
});
|
|
151
|
+
const debouncedSearch = useDebounce(searchQuery, debounceMs);
|
|
152
|
+
// ── Grow the label cache as live search results come in ───────────────────
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (options.length === 0)
|
|
155
|
+
return;
|
|
156
|
+
setKnownOptions((prev) => {
|
|
157
|
+
let changed = false;
|
|
158
|
+
const next = new Map(prev);
|
|
159
|
+
for (const o of options) {
|
|
160
|
+
const key = String(o.value);
|
|
161
|
+
if (!next.has(key)) {
|
|
162
|
+
next.set(key, o);
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return changed ? next : prev;
|
|
167
|
+
});
|
|
168
|
+
}, [options]);
|
|
169
|
+
// ── API fetch ─────────────────────────────────────────────────────────────
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!debouncedSearch || debouncedSearch.length < minSearchLength) {
|
|
172
|
+
setOptions([]);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
setIsLoading(true);
|
|
177
|
+
setFetchError(null);
|
|
178
|
+
const load = async () => {
|
|
179
|
+
try {
|
|
180
|
+
let data;
|
|
181
|
+
if (fetchOptions) {
|
|
182
|
+
// ── Preferred path: consumer-supplied function (auth-aware) ──
|
|
183
|
+
data = await fetchOptions(debouncedSearch, controller.signal);
|
|
184
|
+
}
|
|
185
|
+
else if (api) {
|
|
186
|
+
// ── Fallback: item.api + item.jwtHeader via axios ─────────────
|
|
187
|
+
const url = appendQueryParam(api, searchParamKey || 'searchvalue', debouncedSearch);
|
|
188
|
+
const res = await axios.get(url, { signal: controller.signal, headers: jwtHeader });
|
|
189
|
+
data = res.data;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
data = [];
|
|
193
|
+
}
|
|
194
|
+
setOptions(data);
|
|
195
|
+
setLoadList?.(data);
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
if (isCancellation(err))
|
|
200
|
+
return; // silently ignore aborted requests
|
|
201
|
+
setFetchError('Failed to load options. Please try again.');
|
|
202
|
+
setIsLoading(false);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
load();
|
|
206
|
+
return () => controller.abort();
|
|
207
|
+
}, [debouncedSearch, minSearchLength, api, fetchOptions]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
208
|
+
// ── Stable callbacks ──────────────────────────────────────────────────────
|
|
209
|
+
const getOptionLabel = useCallback((option) => option?.label ?? '', []);
|
|
210
|
+
const isOptionEqualToValue = useCallback((option, value) => String(option?.value) === String(value?.value), []);
|
|
211
|
+
const handleChange = useCallback((_, newValue, reason) => {
|
|
212
|
+
// Defensive guard — getOptionDisabled already blocks picking past the
|
|
213
|
+
// cap at the UI level, this just protects against any other path in.
|
|
214
|
+
if (typeof maxSelections === 'number' && (newValue?.length ?? 0) > maxSelections)
|
|
215
|
+
return;
|
|
216
|
+
const values = (newValue || []).map((o) => String(o.value));
|
|
217
|
+
setValue(name, values, { shouldValidate: false, shouldDirty: true });
|
|
218
|
+
// Cache labels immediately so chips never flash blank before the
|
|
219
|
+
// options-grow effect above runs on the next render.
|
|
220
|
+
setKnownOptions((prev) => {
|
|
221
|
+
const next = new Map(prev);
|
|
222
|
+
(newValue || []).forEach((o) => next.set(String(o.value), o));
|
|
223
|
+
return next;
|
|
224
|
+
});
|
|
225
|
+
if (USER_CHANGE_REASONS.has(reason)) {
|
|
226
|
+
item.onChangeFn?.(values, newValue);
|
|
227
|
+
}
|
|
228
|
+
clearErrors?.(name);
|
|
229
|
+
}, [setValue, clearErrors, name, item.onChangeFn, maxSelections]);
|
|
230
|
+
const handleInputChange = useCallback((_, newInputValue, reason) => {
|
|
231
|
+
setDisplayText(newInputValue);
|
|
232
|
+
if (reason === 'input') {
|
|
233
|
+
// User is typing → trigger debounced API search
|
|
234
|
+
setSearchQuery(newInputValue);
|
|
235
|
+
}
|
|
236
|
+
if (reason === 'clear') {
|
|
237
|
+
// Clear (×) icon — resets the search text/results; selected chips are
|
|
238
|
+
// cleared separately via handleChange's 'clear' reason.
|
|
239
|
+
setSearchQuery('');
|
|
240
|
+
setOptions([]);
|
|
241
|
+
setDisplayText('');
|
|
242
|
+
}
|
|
243
|
+
// 'reset' fires when MUI resets the input text after a selection — no
|
|
244
|
+
// search should trigger here.
|
|
245
|
+
}, []);
|
|
246
|
+
/** Clears ALL selections — wired to the delete button on the overflow chip. */
|
|
247
|
+
const handleClearAll = useCallback(() => {
|
|
248
|
+
setValue(name, [], { shouldValidate: false, shouldDirty: true });
|
|
249
|
+
item.onChangeFn?.([], []); // user explicitly cleared all → fire callback
|
|
250
|
+
clearErrors?.(name);
|
|
251
|
+
}, [setValue, clearErrors, name, item.onChangeFn]);
|
|
252
|
+
// ── Resolved Autocomplete value ───────────────────────────────────────────
|
|
253
|
+
// Every selected id is resolved to its full option via: label cache →
|
|
254
|
+
// current search results → raw id as a last-resort label (never silently
|
|
255
|
+
// dropped, unlike a lookup miss on the single-select counterpart).
|
|
256
|
+
const resolvedValues = useMemo(() => {
|
|
257
|
+
return selectedIds.map((id) => {
|
|
258
|
+
const known = knownOptions.get(id);
|
|
259
|
+
if (known)
|
|
260
|
+
return known;
|
|
261
|
+
const live = options.find((o) => String(o.value) === id);
|
|
262
|
+
if (live)
|
|
263
|
+
return live;
|
|
264
|
+
return { label: id, value: id };
|
|
265
|
+
});
|
|
266
|
+
}, [selectedIds, knownOptions, options]);
|
|
267
|
+
/** Removes a single selection — wired to each row in the info popover. */
|
|
268
|
+
const handleRemoveOne = useCallback((valueToRemove) => {
|
|
269
|
+
const remaining = resolvedValues.filter((o) => String(o.value) !== String(valueToRemove));
|
|
270
|
+
const values = remaining.map((o) => String(o.value));
|
|
271
|
+
setValue(name, values, { shouldValidate: false, shouldDirty: true });
|
|
272
|
+
item.onChangeFn?.(values, remaining);
|
|
273
|
+
clearErrors?.(name);
|
|
274
|
+
}, [resolvedValues, setValue, clearErrors, name, item.onChangeFn]);
|
|
275
|
+
/**
|
|
276
|
+
* Backspace with an empty search box removes the last-selected chip —
|
|
277
|
+
* standard multi-select affordance (Gmail "To:" field, MUI Chip patterns).
|
|
278
|
+
* Not native here because `inputValue` is externally controlled for the
|
|
279
|
+
* debounced-search wiring, which suppresses MUI Autocomplete's own default
|
|
280
|
+
* handling of this key combination.
|
|
281
|
+
*/
|
|
282
|
+
const handleInputKeyDown = useCallback((e) => {
|
|
283
|
+
if (e.key === 'Backspace' && displayText === '' && resolvedValues.length > 0) {
|
|
284
|
+
handleRemoveOne(resolvedValues[resolvedValues.length - 1].value);
|
|
285
|
+
}
|
|
286
|
+
}, [displayText, resolvedValues, handleRemoveOne]);
|
|
287
|
+
const isOverflowing = resolvedValues.length > maxChips;
|
|
288
|
+
const selectedTooltip = useMemo(() => resolvedValues.map((o) => String(o.label)).join(', '), [resolvedValues]);
|
|
289
|
+
const atCap = typeof maxSelections === 'number' && selectedIds.length >= maxSelections;
|
|
290
|
+
// ── noOptionsText ─────────────────────────────────────────────────────────
|
|
291
|
+
const noOptionsText = useMemo(() => {
|
|
292
|
+
if (atCap) {
|
|
293
|
+
return `Maximum ${maxSelections} selected — remove one to add another`;
|
|
294
|
+
}
|
|
295
|
+
if (searchQuery.length < minSearchLength) {
|
|
296
|
+
return `Type at least ${minSearchLength} character${minSearchLength === 1 ? '' : 's'} to search`;
|
|
297
|
+
}
|
|
298
|
+
return fetchError ?? 'No results found';
|
|
299
|
+
}, [atCap, maxSelections, searchQuery, minSearchLength, fetchError]);
|
|
300
|
+
const chipSx = useMemo(() => ({
|
|
301
|
+
mr: 0.5,
|
|
302
|
+
height: 20,
|
|
303
|
+
fontSize: '10px',
|
|
304
|
+
borderRadius: '10px',
|
|
305
|
+
'& .MuiChip-label': { px: 0.75, py: 0 },
|
|
306
|
+
}), []);
|
|
307
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
308
|
+
return (jsx(Controller, { control: control, name: name, render: ({ field: { ref, onBlur } }) => (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(Autocomplete, { ref: ref, fullWidth: true, multiple: true, disableCloseOnSelect: true, open: open, onOpen: () => setOpen(true), onClose: () => setOpen(false), value: resolvedValues, inputValue: displayText, onInputChange: handleInputChange, onChange: handleChange, onBlur: onBlur, options: options, getOptionLabel: getOptionLabel, isOptionEqualToValue: isOptionEqualToValue, getOptionDisabled: (option) => {
|
|
309
|
+
if (option?.disable)
|
|
310
|
+
return true;
|
|
311
|
+
// At the cap, only already-selected options stay pickable (so
|
|
312
|
+
// the user can still deselect); everything else disables.
|
|
313
|
+
if (atCap)
|
|
314
|
+
return !selectedIds.includes(String(option.value));
|
|
315
|
+
return false;
|
|
316
|
+
},
|
|
317
|
+
// Options come pre-filtered from the server — disable client-side filtering
|
|
318
|
+
filterOptions: (x) => x, loading: isLoading, disabled: disable, size: "small", autoHighlight: true, selectOnFocus: true, clearOnBlur: false, noOptionsText: noOptionsText, sx: {
|
|
319
|
+
'& .MuiAutocomplete-input': {
|
|
320
|
+
padding: '0px 0px 0px 5px !important',
|
|
321
|
+
},
|
|
322
|
+
'& .MuiAutocomplete-inputRoot': {
|
|
323
|
+
flexWrap: 'nowrap',
|
|
324
|
+
alignItems: 'center',
|
|
325
|
+
},
|
|
326
|
+
// MUI docks its clear/dropdown-arrow group flush against the right
|
|
327
|
+
// edge via `position: absolute; right: 9px`. Shifting it left makes
|
|
328
|
+
// room there for the info icon, which is rendered as its own
|
|
329
|
+
// absolutely-positioned adornment docked at the true right edge —
|
|
330
|
+
// safer than fighting MUI's internal layout of the clear/arrow pair.
|
|
331
|
+
'& .MuiAutocomplete-endAdornment': {
|
|
332
|
+
right: '32px !important',
|
|
333
|
+
},
|
|
334
|
+
'& .MuiOutlinedInput-root': {
|
|
335
|
+
paddingRight: '52px !important',
|
|
336
|
+
},
|
|
337
|
+
'& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root': {
|
|
338
|
+
top: '-5px',
|
|
339
|
+
},
|
|
340
|
+
...itemSx,
|
|
341
|
+
}, slotProps: {
|
|
342
|
+
popper: { sx: { zIndex: 20000 } },
|
|
343
|
+
listbox: { sx: { fontSize: '11px' } },
|
|
344
|
+
}, PaperComponent: ({ children, ...paperProps }) => (jsx(Paper, { elevation: 4, ...paperProps, children: children })), renderOption: (renderProps, option, state) => {
|
|
345
|
+
const { key, ...otherProps } = renderProps;
|
|
346
|
+
return (jsxs("li", { ...otherProps, style: { fontSize: '11px' }, children: [jsx(Checkbox, { checked: state.selected, size: "small", sx: { mr: 1, p: '2px' } }), option.label] }, key));
|
|
347
|
+
}, renderTags: (tagValue, getTagProps) => (jsx(Box, { ref: tagsContainerRef, sx: {
|
|
348
|
+
display: 'flex',
|
|
349
|
+
flexWrap: 'nowrap',
|
|
350
|
+
overflow: 'hidden',
|
|
351
|
+
alignItems: 'center',
|
|
352
|
+
'& :hover': { cursor: 'pointer' },
|
|
353
|
+
}, children: isOverflowing ? (
|
|
354
|
+
// ── Collapsed chip: shows "N selected" with tooltip ──────
|
|
355
|
+
jsx(Tooltip, { title: selectedTooltip, placement: "top", children: jsx(Chip, { size: "small", label: `${tagValue.length} selected`, onClick: () => setOpen(true), onDelete: handleClearAll, clickable: true, sx: chipSx }) })) : (
|
|
356
|
+
// ── Individual chips: up to maxChips items ───────────────
|
|
357
|
+
tagValue.map((option, index) => {
|
|
358
|
+
const { key: tagKey, ...tagProps } = getTagProps({ index });
|
|
359
|
+
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)));
|
|
360
|
+
})) })), renderInput: (params) => (jsx(TextField, { ...params, error: isShowBorderError, label: variant !== 'standard' && labelVariant !== 'static' ? `${label}${required ? ' *' : ''}` : '', placeholder: placeholder, inputProps: {
|
|
361
|
+
...params.inputProps,
|
|
362
|
+
onKeyDown: (e) => {
|
|
363
|
+
params.inputProps.onKeyDown?.(e);
|
|
364
|
+
handleInputKeyDown(e);
|
|
365
|
+
},
|
|
366
|
+
'aria-labelledby': `${name}-label`,
|
|
367
|
+
'aria-describedby': `${name}-helper ${name}-error`,
|
|
368
|
+
'aria-required': required ? true : undefined,
|
|
369
|
+
}, InputProps: {
|
|
370
|
+
...params.InputProps,
|
|
371
|
+
endAdornment: (jsxs(Fragment, { children: [params.InputProps.endAdornment, resolvedValues.length > 0 && (jsx(InputAdornment, { position: "end", sx: { position: 'absolute', right: '2px', top: '50%', transform: 'translateY(-50%)' }, children: jsx(IconButton, { size: "small", edge: "end", onClick: (e) => setInfoAnchorEl(e.currentTarget), "aria-label": `View ${resolvedValues.length} selected item${resolvedValues.length === 1 ? '' : 's'}`, children: jsx(InfoOutlinedIcon, { fontSize: "small" }) }) }))] })),
|
|
372
|
+
} })) }), jsx(Popover, { open: Boolean(infoAnchorEl), anchorEl: infoAnchorEl, onClose: () => setInfoAnchorEl(null), anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' }, slotProps: { paper: { sx: { zIndex: 20001 } } }, children: jsx(List, { dense: true, sx: { minWidth: 220, maxWidth: 320, maxHeight: 300, overflow: 'auto', py: 0.5 }, children: resolvedValues.length === 0 ? (jsx(ListItem, { children: jsx(ListItemText, { primaryTypographyProps: { fontSize: '12px' }, primary: "No items selected" }) })) : (resolvedValues.map((opt) => (jsx(ListItem, { secondaryAction: jsx(IconButton, { size: "small", edge: "end", onClick: () => handleRemoveOne(opt.value), "aria-label": `Remove ${opt.label}`, children: jsx(CloseIcon, { sx: { fontSize: 14 } }) }), sx: { py: 0.25 }, children: jsx(ListItemText, { primaryTypographyProps: { fontSize: '12px' }, primary: opt.label }) }, String(opt.value))))) }) }), typeof maxSelections === 'number' && (jsxs(Typography, { variant: "caption", color: atCap ? 'error' : 'text.secondary', sx: { mt: 0.25, display: 'block' }, children: [selectedIds.length, "/", maxSelections, " selected", atCap ? ' — maximum reached' : ''] })), 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 })] })) }));
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export { MultiSelectSearchApi as default };
|
|
@@ -76,7 +76,7 @@ const SingleSelectSearchApi = ({ props, variant, minSearchLength = 3, debounceMs
|
|
|
76
76
|
// ── Stable references from props ──────────────────────────────────────────
|
|
77
77
|
const { control, errors, item, setValue, clearErrors } = props;
|
|
78
78
|
const { name, label, required, disable, placeholder, helperText, autoFIll: autoFill, // match the (typo'd) field name in FormSectionPropsItem
|
|
79
|
-
setLoadList, api, jwtHeader, searchValue: searchParamKey, sx: itemSx, } = item;
|
|
79
|
+
setLoadList, api, jwtHeader, searchValue: searchParamKey, sx: itemSx, labelVariant, } = item;
|
|
80
80
|
// ── Form value (reactive, no function call in deps) ───────────────────────
|
|
81
81
|
const formValue = useWatch({ control, name });
|
|
82
82
|
const isError = !!errors?.[name];
|
|
@@ -259,7 +259,7 @@ const SingleSelectSearchApi = ({ props, variant, minSearchLength = 3, debounceMs
|
|
|
259
259
|
},
|
|
260
260
|
],
|
|
261
261
|
},
|
|
262
|
-
}, children: jsx(TextField, { ...params, error: isError && !formValue, label: variant !== 'standard'
|
|
262
|
+
}, children: jsx(TextField, { ...params, error: isError && !formValue, label: variant !== 'standard' && labelVariant !== 'static'
|
|
263
263
|
? `${label}${required ? ' *' : ''}`
|
|
264
264
|
: '', placeholder: placeholder, inputProps: {
|
|
265
265
|
...params.inputProps,
|
|
@@ -6,6 +6,7 @@ export { FormCheckBox } from '../CheckBox';
|
|
|
6
6
|
export { default as SingleSelect } from './Select/SingleSelect';
|
|
7
7
|
export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete';
|
|
8
8
|
export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi';
|
|
9
|
+
export { default as MultiSelectSearchApi } from './Select/MultiSelectSearchApi';
|
|
9
10
|
export { default as MultiSelectV1 } from './Select/MultiSelectv1';
|
|
10
11
|
export { default as MultiSelectTable } from './Select/MultiSelectTable';
|
|
11
12
|
export { default as SearchableSelect } from './Select/SearchableSelect';
|
|
@@ -7,6 +7,7 @@ export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
|
|
|
7
7
|
export { default as SingleSelect } from './Select/SingleSelect.esm.js';
|
|
8
8
|
export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete.esm.js';
|
|
9
9
|
export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi.esm.js';
|
|
10
|
+
export { default as MultiSelectSearchApi } from './Select/MultiSelectSearchApi.esm.js';
|
|
10
11
|
export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
|
|
11
12
|
export { default as MultiSelectTable } from './Select/MultiSelectTable.esm.js';
|
|
12
13
|
export { default as SearchableSelect } from './Select/SearchableSelect.esm.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as SingleSelect } from '../FormComponents/Select/SingleSelect';
|
|
2
2
|
export { default as SingleSelectNonAutoComplete } from '../FormComponents/Select/SingleSelectNonAutoComplete';
|
|
3
3
|
export { default as SingleSelectSearchApi } from '../FormComponents/Select/SingleSelectSearchApi';
|
|
4
|
+
export { default as MultiSelectSearchApi } from '../FormComponents/Select/MultiSelectSearchApi';
|
|
4
5
|
export { default as MultiSelectV1 } from '../FormComponents/Select/MultiSelectv1';
|
|
5
6
|
declare const _default: {};
|
|
6
7
|
export default _default;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as SingleSelect } from '../FormComponents/Select/SingleSelect.esm.js';
|
|
2
2
|
export { default as SingleSelectNonAutoComplete } from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
|
|
3
3
|
export { default as SingleSelectSearchApi } from '../FormComponents/Select/SingleSelectSearchApi.esm.js';
|
|
4
|
+
export { default as MultiSelectSearchApi } from '../FormComponents/Select/MultiSelectSearchApi.esm.js';
|
|
4
5
|
export { default as MultiSelectV1 } from '../FormComponents/Select/MultiSelectv1.esm.js';
|
|
5
6
|
|
|
6
7
|
var index = {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as Calendar } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as Calendar } from '../../../../../_virtual/index.esm6.js';
|
|
2
2
|
import require$$0__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';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DateInput } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DateInput } from '../../../../../_virtual/index.esm12.js';
|
|
2
2
|
import require$$0__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 DateRangePicker } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm7.js';
|
|
2
2
|
import require$$0__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 InputRangeField } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as InputRangeField } from '../../../../../_virtual/index.esm13.js';
|
|
2
2
|
import require$$0__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.esm14.js';
|
|
2
2
|
import require$$0__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';
|
|
@@ -11,7 +11,7 @@ export interface TextFieldInputProps {
|
|
|
11
11
|
export interface FormSectionPropsItem {
|
|
12
12
|
name: string;
|
|
13
13
|
label: string;
|
|
14
|
-
inputType: 'text' | 'password' | 'number' | 'select' | 'autocomplete-select' | 'autocomplete-multi-select' | 'single-select-search' | 'datepicker' | 'datetimepicker' | 'multiselect' | 'select-v2' | 'basic-select' | 'searchable-select' | 'decimal' | 'alpha-numerical' | 'yearpicker' | 'dateRangePicker' | 'monthpicker' | 'monthpicker-multi' | 'multiselect' | 'file' | 'multifile' | 'multifile-with-preview' | 'textarea' | 'phoneNumber' | 'pincode' | 'email' | 'toggleSwitch' | 'rich-text-editor' | 'multiEmail' | 'timepicker' | 'checkbox-group' | 'radio-group' | 'checkbox' | 'custom' | 'input-select' | 'date-range-picker' | 'multiselect-table' | '';
|
|
14
|
+
inputType: 'text' | 'password' | 'number' | 'select' | 'autocomplete-select' | 'autocomplete-multi-select' | 'single-select-search' | 'multi-select-search' | 'datepicker' | 'datetimepicker' | 'multiselect' | 'select-v2' | 'basic-select' | 'searchable-select' | 'decimal' | 'alpha-numerical' | 'yearpicker' | 'dateRangePicker' | 'monthpicker' | 'monthpicker-multi' | 'multiselect' | 'file' | 'multifile' | 'multifile-with-preview' | 'textarea' | 'phoneNumber' | 'pincode' | 'email' | 'toggleSwitch' | 'rich-text-editor' | 'multiEmail' | 'timepicker' | 'checkbox-group' | 'radio-group' | 'checkbox' | 'custom' | 'input-select' | 'date-range-picker' | 'multiselect-table' | '';
|
|
15
15
|
options?: OptionsProps[];
|
|
16
16
|
required?: boolean;
|
|
17
17
|
errorMessage?: string;
|