tp-react-elements 3.0.17 → 3.0.20
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.esm5.js +2 -2
- package/dist/_virtual/index.esm6.js +2 -2
- package/dist/_virtual/index.esm7.js +2 -2
- package/dist/components/Form/FormRender.esm.js +11 -8
- 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 +13 -29
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +23 -13
- 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/DateRange/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateRangePicker/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 DateRangePicker = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRangePicker 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 Calendar = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Calendar as __exports };
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import require$$0__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';
|
|
@@ -17,7 +9,16 @@ import FormTextFieldWithSelect from '../FormComponents/FormTextFieldWithSelect/F
|
|
|
17
9
|
import DateRangePickerComponent from '../DateRangePicker/DateRangePicker.esm.js';
|
|
18
10
|
import RichTextEditorWrapper from '../FormComponents/RichTextEditor/RichTextEditorWrapper.esm.js';
|
|
19
11
|
import SingleSelectSearchApi from '../FormComponents/Select/SingleSelectSearchApi.esm.js';
|
|
12
|
+
import MultiSelectSearchApi from '../FormComponents/Select/MultiSelectSearchApi.esm.js';
|
|
20
13
|
import DatetimepickerWrapper from '../FormComponents/DatePicker/DatetimepickerWrapper.esm.js';
|
|
14
|
+
import FormTextAreaField from '../FormComponents/FormTextAreaField/FormTextAreaField.esm.js';
|
|
15
|
+
import FormCheckBox from '../FormComponents/FormCheckBox/FormCheckBox.esm.js';
|
|
16
|
+
import FormRadioGroup from '../FormComponents/FormRadioGroup/FormRadioGroup.esm.js';
|
|
17
|
+
import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
|
|
18
|
+
import FormNumberFieldDecimal from '../FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js';
|
|
19
|
+
import FormNumberField from '../FormComponents/FormNumberField/FormNumberField.esm.js';
|
|
20
|
+
import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js';
|
|
21
|
+
import FormTextField from '../FormComponents/FormTextField/FormTextField.esm.js';
|
|
21
22
|
|
|
22
23
|
// Lazy-loaded heavy components
|
|
23
24
|
// const RichTextEditorWrapper = lazy(() =>
|
|
@@ -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';
|
|
@@ -1,42 +1,26 @@
|
|
|
1
|
-
export { default as FormNumberField } from './FormNumberField/FormNumberField.esm.js';
|
|
2
|
-
export { default as FormNumberFieldDecimal } from './FormNumberField/FormNumberFieldDecimal.esm.js';
|
|
3
|
-
export { default as FormTextAreaField } from './FormTextAreaField/FormTextAreaField.esm.js';
|
|
4
|
-
export { default as FormTextField } from './FormTextField/FormTextField.esm.js';
|
|
5
|
-
export { default as PasswordField } from './PasswordField/PasswordField.esm.js';
|
|
6
|
-
export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
|
|
7
1
|
export { default as SingleSelect } from './Select/SingleSelect.esm.js';
|
|
8
2
|
export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete.esm.js';
|
|
9
3
|
export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi.esm.js';
|
|
4
|
+
export { default as MultiSelectSearchApi } from './Select/MultiSelectSearchApi.esm.js';
|
|
10
5
|
export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
|
|
11
6
|
export { default as MultiSelectTable } from './Select/MultiSelectTable.esm.js';
|
|
12
7
|
export { default as SearchableSelect } from './Select/SearchableSelect.esm.js';
|
|
13
|
-
export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
|
|
14
|
-
export { default as FormRadioGroup } from './FormRadioGroup/FormRadioGroup.esm.js';
|
|
15
8
|
export { default as DatepickerWrapperV2 } from './DatePicker/DatepickerWrapperV2.esm.js';
|
|
16
|
-
export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
|
|
17
|
-
import 'react/jsx-runtime';
|
|
18
|
-
import 'react';
|
|
19
|
-
import '@mui/material';
|
|
20
|
-
import '@mui/icons-material/CalendarToday';
|
|
21
|
-
import '@mui/icons-material/ChevronLeft';
|
|
22
|
-
import '@mui/icons-material/ChevronRight';
|
|
23
|
-
import 'react-hook-form';
|
|
24
9
|
export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
|
|
25
|
-
|
|
26
|
-
export { default as
|
|
27
|
-
export { default as
|
|
10
|
+
export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
|
|
11
|
+
export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
|
|
12
|
+
export { default as FormErrorField } from './FormErrorField/FormErrorField.esm.js';
|
|
13
|
+
export { default as FormNumberField } from './FormNumberField/FormNumberField.esm.js';
|
|
14
|
+
export { default as FormNumberFieldDecimal } from './FormNumberField/FormNumberFieldDecimal.esm.js';
|
|
15
|
+
export { default as FormRadioGroup } from './FormRadioGroup/FormRadioGroup.esm.js';
|
|
28
16
|
export { default as FormRenderFileUpload } from './FileUpload/FormRenderFileUpload.esm.js';
|
|
29
17
|
export { default as FormRenderMultiFileUpload } from './FileUpload/FormRenderMultiFileUpload.esm.js';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import '@mui/icons-material/NavigateBefore';
|
|
33
|
-
import '@mui/icons-material/NavigateNext';
|
|
34
|
-
import '@mui/icons-material/Delete';
|
|
35
|
-
import '@mui/icons-material/UploadFile';
|
|
36
|
-
import '@mui/icons-material/PictureAsPdf';
|
|
37
|
-
import '@mui/icons-material/TableChart';
|
|
38
|
-
import '@mui/icons-material/Description';
|
|
39
|
-
export { default as FormErrorField } from './FormErrorField/FormErrorField.esm.js';
|
|
18
|
+
export { default as FormTextAreaField } from './FormTextAreaField/FormTextAreaField.esm.js';
|
|
19
|
+
export { default as FormTextField } from './FormTextField/FormTextField.esm.js';
|
|
40
20
|
export { default as HelperText } from './HelperText/HelperText.esm.js';
|
|
21
|
+
export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
|
|
22
|
+
export { default as PasswordField } from './PasswordField/PasswordField.esm.js';
|
|
41
23
|
export { default as RichTextEditor } from './RichTextEditor/RichTextEditor.esm.js';
|
|
42
24
|
export { default as RichTextEditorWrapper } from './RichTextEditor/RichTextEditorWrapper.esm.js';
|
|
25
|
+
export { default as TimePicker } from './TimePicker/TimePicker.esm.js';
|
|
26
|
+
export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
|
|
@@ -390,19 +390,19 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
390
390
|
.map((d) => d.id);
|
|
391
391
|
// Column filter chips with defaultVisible: true.
|
|
392
392
|
// Mirror the same exclusion rules used by availableColumns:
|
|
393
|
-
// • must have
|
|
393
|
+
// • must have a filter identifier (accessorKey, falling back to id)
|
|
394
394
|
// • must not have enableColumnFilter: false
|
|
395
395
|
// • must not be managed by a server-side 'date' filter def
|
|
396
396
|
const serverManagedColKeys = new Set(serverFilterDefs
|
|
397
397
|
.filter((d) => d.type === 'date')
|
|
398
398
|
.map((d) => d.columnKey));
|
|
399
399
|
const markedCols = columns
|
|
400
|
-
.
|
|
401
|
-
|
|
400
|
+
.map((c) => ({ c, key: c.accessorKey ?? c.id }))
|
|
401
|
+
.filter(({ c, key }) => !!key &&
|
|
402
402
|
c.defaultVisible &&
|
|
403
403
|
c.enableColumnFilter !== false &&
|
|
404
|
-
!serverManagedColKeys.has(
|
|
405
|
-
.map((
|
|
404
|
+
!serverManagedColKeys.has(key))
|
|
405
|
+
.map(({ key }) => key);
|
|
406
406
|
const marked = [...markedServer, ...markedCols];
|
|
407
407
|
return marked.length > 0
|
|
408
408
|
? marked
|
|
@@ -563,23 +563,33 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
563
563
|
const getColumnCanGlobalFilter = useCallback(() => true, []);
|
|
564
564
|
// ── Derive column list for filter chip picker ──────────────────────────────
|
|
565
565
|
//
|
|
566
|
+
// A column's filter identifier is its `accessorKey` when set, falling back to
|
|
567
|
+
// its `id` — TanStack Table's `row.getValue(columnId)` resolves correctly
|
|
568
|
+
// either way, whether the column was defined via `accessorKey` or via
|
|
569
|
+
// `accessorFn` + an explicit `id` (the standard pattern for a computed/mapped
|
|
570
|
+
// display column, e.g. a status label derived from two boolean fields). Using
|
|
571
|
+
// `accessorKey`-only here would silently exclude every such computed column
|
|
572
|
+
// from Manage Filters even when it has a real `filterType`/`options` already
|
|
573
|
+
// configured — a real, previously-observed gap across several grids.
|
|
574
|
+
//
|
|
566
575
|
// A column is excluded from the "Column Filters" section of Manage Filters
|
|
567
576
|
// when ANY of the following are true:
|
|
568
|
-
// 1. It has
|
|
569
|
-
// 2.
|
|
577
|
+
// 1. It has neither `accessorKey` nor `id` (no stable filter identifier).
|
|
578
|
+
// 2. That identifier is already handled by a server-side 'date' filter def.
|
|
570
579
|
// 3. Its MRT `enableColumnFilter` prop is explicitly set to `false` —
|
|
571
|
-
// use this for numeric/computed columns that don't need a filter chip
|
|
580
|
+
// use this for numeric/computed columns that don't need a filter chip
|
|
581
|
+
// (e.g. an actions column).
|
|
572
582
|
const availableColumns = useMemo(() => {
|
|
573
583
|
const serverManagedColKeys = new Set(serverFilterDefs
|
|
574
584
|
.filter((d) => d.type === 'date')
|
|
575
585
|
.map((d) => d.columnKey));
|
|
576
586
|
return columns
|
|
577
|
-
.
|
|
578
|
-
|
|
579
|
-
!serverManagedColKeys.has(
|
|
587
|
+
.map((c) => ({ c, key: c.accessorKey ?? c.id }))
|
|
588
|
+
.filter(({ c, key }) => !!key &&
|
|
589
|
+
!serverManagedColKeys.has(key) &&
|
|
580
590
|
c.enableColumnFilter !== false)
|
|
581
|
-
.map((c) => ({
|
|
582
|
-
id:
|
|
591
|
+
.map(({ c, key }) => ({
|
|
592
|
+
id: key,
|
|
583
593
|
label: c.header,
|
|
584
594
|
// Pass filterType directly from TPColumnDef (no meta unwrapping needed)
|
|
585
595
|
filterType: c.filterType,
|
|
@@ -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.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 requireDayCell } from '../DayCell/index.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DateRange } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DateRange } 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 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.esm5.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';
|
|
@@ -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;
|