tp-react-elements 3.0.0 → 3.0.2

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.
Files changed (24) hide show
  1. package/dist/_virtual/index.esm13.js +2 -2
  2. package/dist/_virtual/index.esm14.js +2 -2
  3. package/dist/_virtual/index.esm4.js +2 -2
  4. package/dist/_virtual/index.esm5.js +2 -2
  5. package/dist/_virtual/index.esm6.js +2 -2
  6. package/dist/_virtual/index.esm7.js +2 -2
  7. package/dist/components/DatePicker/index.d.ts +1 -0
  8. package/dist/components/DatePicker/index.esm.js +2 -1
  9. package/dist/components/Form/FormConstants.esm.js +1 -0
  10. package/dist/components/Form/FormRender.esm.js +13 -9
  11. package/dist/components/FormComponents/DatePicker/MonthPickerMultiRender.d.ts +6 -0
  12. package/dist/components/FormComponents/DatePicker/MonthPickerMultiRender.esm.js +117 -0
  13. package/dist/components/FormComponents/index.esm.js +29 -12
  14. package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +12 -4
  15. package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +48 -16
  16. package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +67 -21
  17. package/dist/node_modules/react-date-range/dist/components/Calendar/index.esm.js +1 -1
  18. package/dist/node_modules/react-date-range/dist/components/DateInput/index.esm.js +1 -1
  19. package/dist/node_modules/react-date-range/dist/components/DateRange/index.esm.js +1 -1
  20. package/dist/node_modules/react-date-range/dist/components/DateRangePicker/index.esm.js +1 -1
  21. package/dist/node_modules/react-date-range/dist/components/DefinedRange/index.esm.js +1 -1
  22. package/dist/node_modules/react-date-range/dist/components/Month/index.esm.js +1 -1
  23. package/dist/utils/Interface/FormInterface.d.ts +1 -1
  24. package/package.json +1 -1
@@ -1,3 +1,3 @@
1
- var Month = {};
1
+ var DateInput = {};
2
2
 
3
- export { Month as __exports };
3
+ export { DateInput as __exports };
@@ -1,3 +1,3 @@
1
- var DateInput = {};
1
+ var Month = {};
2
2
 
3
- export { DateInput as __exports };
3
+ export { Month as __exports };
@@ -1,3 +1,3 @@
1
- var DefinedRange = {};
1
+ var DateRange = {};
2
2
 
3
- export { DefinedRange as __exports };
3
+ export { DateRange as __exports };
@@ -1,3 +1,3 @@
1
- var DateRangePicker = {};
1
+ var DefinedRange = {};
2
2
 
3
- export { DateRangePicker as __exports };
3
+ export { DefinedRange as __exports };
@@ -1,3 +1,3 @@
1
- var Calendar = {};
1
+ var DateRangePicker = {};
2
2
 
3
- export { Calendar as __exports };
3
+ export { DateRangePicker as __exports };
@@ -1,3 +1,3 @@
1
- var DateRange = {};
1
+ var Calendar = {};
2
2
 
3
- export { DateRange as __exports };
3
+ export { Calendar as __exports };
@@ -1,3 +1,4 @@
1
1
  export { default as DatepickerWrapperV2 } from '../FormComponents/DatePicker/DatepickerWrapperV2';
2
2
  export { default as Monthpickerrender } from '../FormComponents/DatePicker/Monthpickerrender';
3
+ export { default as MonthPickerMultiRender } from '../FormComponents/DatePicker/MonthPickerMultiRender';
3
4
  export { default as YearPickerField } from '../FormComponents/YearPickerField/YearPickerField';
@@ -1,7 +1,8 @@
1
1
  import DatepickerWrapperV2 from '../FormComponents/DatePicker/DatepickerWrapperV2.esm.js';
2
2
  import Monthpickerrender from '../FormComponents/DatePicker/Monthpickerrender.esm.js';
3
+ import MonthPickerMultiRender from '../FormComponents/DatePicker/MonthPickerMultiRender.esm.js';
3
4
  export { default as YearPickerField } from '../FormComponents/YearPickerField/YearPickerField.esm.js';
4
5
 
5
6
 
6
7
 
7
- export { DatepickerWrapperV2, Monthpickerrender };
8
+ export { DatepickerWrapperV2, MonthPickerMultiRender, Monthpickerrender };
@@ -282,6 +282,7 @@ const useFormValidatingContext = (formArray) => {
282
282
  renderCustomError(field);
283
283
  break;
284
284
  case 'monthpicker':
285
+ case 'monthpicker-multi':
285
286
  initialValues[field.name] = null;
286
287
  if (field.required && field.errorMessage) {
287
288
  validationShape[field.name] = (validationShape[field.name] || Yup.string())
@@ -1,5 +1,13 @@
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';
3
11
  import SingleSelect from '../FormComponents/Select/SingleSelect.esm.js';
4
12
  import MultiSelectAutocomplete from '../FormComponents/Select/MultiSelectAutocomplete.esm.js';
5
13
  import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
@@ -10,14 +18,6 @@ import DateRangePickerComponent from '../DateRangePicker/DateRangePicker.esm.js'
10
18
  import RichTextEditorWrapper from '../FormComponents/RichTextEditor/RichTextEditorWrapper.esm.js';
11
19
  import SingleSelectSearchApi from '../FormComponents/Select/SingleSelectSearchApi.esm.js';
12
20
  import DatetimepickerWrapper from '../FormComponents/DatePicker/DatetimepickerWrapper.esm.js';
13
- import FormTextAreaField from '../FormComponents/FormTextAreaField/FormTextAreaField.esm.js';
14
- import FormCheckBox from '../FormComponents/FormCheckBox/FormCheckBox.esm.js';
15
- import FormRadioGroup from '../FormComponents/FormRadioGroup/FormRadioGroup.esm.js';
16
- import FormCheckBoxGroup from '../FormComponents/FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
17
- import FormNumberFieldDecimal from '../FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js';
18
- import FormNumberField from '../FormComponents/FormNumberField/FormNumberField.esm.js';
19
- import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js';
20
- import FormTextField from '../FormComponents/FormTextField/FormTextField.esm.js';
21
21
 
22
22
  // Lazy-loaded heavy components
23
23
  // const RichTextEditorWrapper = lazy(() =>
@@ -30,6 +30,7 @@ const FormRenderMultiFileUpload = lazy(() => import('../FileUpload/index.esm.js'
30
30
  const MultiFileWithPreview = lazy(() => import('../FileUpload/index.esm.js').then((m) => ({ default: m.MultiFileWithPreview })));
31
31
  const DatepickerWrapperV2 = lazy(() => import('../DatePicker/index.esm.js').then((m) => ({ default: m.DatepickerWrapperV2 })));
32
32
  const Monthpickerrender = lazy(() => import('../DatePicker/index.esm.js').then((m) => ({ default: m.Monthpickerrender })));
33
+ const MonthPickerMultiRender = lazy(() => import('../DatePicker/index.esm.js').then((m) => ({ default: m.MonthPickerMultiRender })));
33
34
  const TimePickerFieldWrapper = lazy(() => import('../TimePicker/index.esm.js').then((m) => ({ default: m.TimePicker })));
34
35
  // Loading fallback component
35
36
  const LoadingFallback = () => jsx("div", { style: { minHeight: '36px' }, children: "Loading..." });
@@ -119,9 +120,12 @@ const RenderForm = (props) => {
119
120
  // Time picker
120
121
  case 'timepicker':
121
122
  return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(TimePickerFieldWrapper, { props: props, variant: variant }) }));
122
- // Month picker
123
+ // Month picker (single month)
123
124
  case 'monthpicker':
124
125
  return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(Monthpickerrender, { props: props, variant: variant }) }));
126
+ // Month picker (multiple months)
127
+ case 'monthpicker-multi':
128
+ return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(MonthPickerMultiRender, { props: props, variant: variant }) }));
125
129
  // Multi-select dropdown
126
130
  case 'multiselect':
127
131
  return (jsx(Suspense, { fallback: jsx(LoadingFallback, {}), children: jsx(MultiSelectV1, { props: props, variant: variant }) }));
@@ -0,0 +1,6 @@
1
+ import { FormRenderProps, VariantProps } from '../../../utils';
2
+ declare const MonthPickerMultiRender: ({ props, variant, }: {
3
+ props: FormRenderProps;
4
+ variant: VariantProps;
5
+ }) => import("react/jsx-runtime").JSX.Element;
6
+ export default MonthPickerMultiRender;
@@ -0,0 +1,117 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState, useMemo, useCallback } from 'react';
3
+ import { FormControl, Box, TextField, InputAdornment, Popover, IconButton, Typography, Button } from '@mui/material';
4
+ import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
5
+ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
6
+ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
7
+ import { useWatch } from 'react-hook-form';
8
+ import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
9
+ import 'dayjs';
10
+ import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
11
+
12
+ const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
13
+ const parseMY = (s) => {
14
+ const m = /^(\d{2})\/(\d{4})$/.exec(s.trim());
15
+ return m ? [Number(m[1]), Number(m[2])] : null; // [month, year]
16
+ };
17
+ const toKey = (month, year) => year * 100 + month; // sortable comparable int
18
+ const toMY = (month, year) => `${String(month).padStart(2, '0')}/${year}`;
19
+ const parseValue = (value) => value ? value.split(',').map((s) => s.trim()).filter(Boolean) : [];
20
+ const MonthPickerMultiRender = ({ props, variant, }) => {
21
+ const [anchorEl, setAnchorEl] = useState(null);
22
+ const [viewYear, setViewYear] = useState(() => new Date().getFullYear());
23
+ const [draft, setDraft] = useState([]);
24
+ const value = useWatch({ control: props.control, name: props.item.name });
25
+ const committed = useMemo(() => parseValue(value), [value]);
26
+ const isError = !!props.errors?.[props.item.name];
27
+ const minKey = useMemo(() => {
28
+ const parsed = props.item.minDate ? parseMY(props.item.minDate) : null;
29
+ return parsed ? toKey(parsed[0], parsed[1]) : null;
30
+ }, [props.item.minDate]);
31
+ const maxKey = useMemo(() => {
32
+ const parsed = props.item.maxDate ? parseMY(props.item.maxDate) : null;
33
+ return parsed ? toKey(parsed[0], parsed[1]) : null;
34
+ }, [props.item.maxDate]);
35
+ const handleOpen = useCallback((e) => {
36
+ if (props.item.disable)
37
+ return;
38
+ const committedNow = parseValue(value);
39
+ setDraft(committedNow);
40
+ if (committedNow.length > 0) {
41
+ const years = committedNow.map((s) => Number(s.split('/')[1]));
42
+ setViewYear(Math.min(...years));
43
+ }
44
+ setAnchorEl(e.currentTarget);
45
+ }, [props.item.disable, value]);
46
+ const handleClose = useCallback(() => setAnchorEl(null), []);
47
+ const toggleMonth = useCallback((my) => {
48
+ setDraft((prev) => (prev.includes(my) ? prev.filter((v) => v !== my) : [...prev, my]));
49
+ }, []);
50
+ const handleApply = useCallback(() => {
51
+ const sorted = [...draft].sort((a, b) => {
52
+ const [am, ay] = a.split('/').map(Number);
53
+ const [bm, by] = b.split('/').map(Number);
54
+ return toKey(am, ay) - toKey(bm, by);
55
+ });
56
+ const next = sorted.join(',');
57
+ props.setValue(props.item.name, next);
58
+ props.item.onChangeFn && props.item.onChangeFn(next);
59
+ props.clearErrors && props.clearErrors(props.item.name);
60
+ setAnchorEl(null);
61
+ }, [draft, props]);
62
+ const displayValue = useMemo(() => {
63
+ if (committed.length === 0)
64
+ return '';
65
+ if (committed.length <= 3) {
66
+ return committed
67
+ .map((my) => {
68
+ const [m, y] = my.split('/').map(Number);
69
+ return `${MONTH_LABELS[m - 1]} ${y}`;
70
+ })
71
+ .join(', ');
72
+ }
73
+ return `${committed.length} months selected`;
74
+ }, [committed]);
75
+ const muiLabel = variant !== 'standard' ? `${props.item.label}${props.item.required ? ' *' : ''}` : '';
76
+ return (jsxs(FormControl, { fullWidth: true, sx: { position: 'relative' }, disabled: props.item.disable, children: [renderLabel(variant, props), jsx(Box, { onClick: handleOpen, sx: { cursor: props.item.disable ? 'default' : 'pointer' }, children: jsx(TextField, { fullWidth: true, value: displayValue, label: muiLabel, placeholder: props.item.placeholder || 'Select month(s)…', error: isError, size: "small", InputProps: {
77
+ readOnly: true,
78
+ endAdornment: (jsx(InputAdornment, { position: "end", children: jsx(CalendarTodayIcon, { sx: { fontSize: 15, color: 'text.disabled' } }) })),
79
+ sx: { cursor: 'inherit' },
80
+ }, inputProps: {
81
+ 'aria-labelledby': `${props.item.name}-label`,
82
+ 'aria-describedby': `${props.item.name}-helper ${props.item.name}-error`,
83
+ 'aria-required': props.item.required ? true : undefined,
84
+ style: { cursor: 'inherit' },
85
+ }, sx: {
86
+ '& .MuiFormLabel-root': { top: -10 },
87
+ '& .MuiInputLabel-shrink': { top: 0 },
88
+ } }) }), jsxs(Popover, { open: Boolean(anchorEl), anchorEl: anchorEl, onClose: handleClose, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.5, borderRadius: 2, boxShadow: 6, width: 280 } }, children: [jsxs(Box, { sx: {
89
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
90
+ px: 1.5, py: 0.75,
91
+ borderBottom: '1px solid', borderColor: 'divider',
92
+ }, children: [jsx(IconButton, { size: "small", onClick: () => setViewYear((y) => y - 1), children: jsx(ChevronLeftIcon, { sx: { fontSize: 18 } }) }), jsx(Typography, { fontWeight: 600, sx: { fontSize: 14, userSelect: 'none' }, children: viewYear }), jsx(IconButton, { size: "small", onClick: () => setViewYear((y) => y + 1), children: jsx(ChevronRightIcon, { sx: { fontSize: 18 } }) })] }), jsx(Box, { sx: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 0.75, p: 1.5 }, children: MONTH_LABELS.map((mon, idx) => {
93
+ const monthNum = idx + 1;
94
+ const my = toMY(monthNum, viewYear);
95
+ const key = toKey(monthNum, viewYear);
96
+ const outOfRange = (minKey !== null && key < minKey) || (maxKey !== null && key > maxKey);
97
+ const isSelected = draft.includes(my);
98
+ return (jsx(Button, { variant: isSelected ? 'contained' : 'outlined', size: "small", disableElevation: true, disabled: outOfRange, onClick: () => toggleMonth(my), sx: {
99
+ fontSize: 12,
100
+ textTransform: 'none',
101
+ borderRadius: 2,
102
+ py: 0.75,
103
+ borderColor: isSelected ? 'primary.main' : 'divider',
104
+ fontWeight: isSelected ? 700 : 400,
105
+ }, children: mon }, my));
106
+ }) }), draft.length > 0 && (jsxs(Box, { sx: {
107
+ px: 2, py: 0.75,
108
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
109
+ borderTop: '1px solid', borderColor: 'divider',
110
+ }, children: [jsxs(Typography, { sx: { fontSize: 11, color: 'primary.main', fontWeight: 600 }, children: [draft.length, " month", draft.length > 1 ? 's' : '', " selected"] }), jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear" })] })), jsx(Box, { sx: {
111
+ px: 1.5, py: 1,
112
+ display: 'flex', justifyContent: 'flex-end',
113
+ borderTop: '1px solid', borderColor: 'divider',
114
+ }, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) })] }), jsx(FormBottomField, { ...props })] }, props.item.name));
115
+ };
116
+
117
+ export { MonthPickerMultiRender as default };
@@ -1,25 +1,42 @@
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';
1
7
  export { default as SingleSelect } from './Select/SingleSelect.esm.js';
2
8
  export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete.esm.js';
3
9
  export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi.esm.js';
4
10
  export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
5
11
  export { default as MultiSelectTable } from './Select/MultiSelectTable.esm.js';
6
12
  export { default as SearchableSelect } from './Select/SearchableSelect.esm.js';
7
- export { default as DatepickerWrapperV2 } from './DatePicker/DatepickerWrapperV2.esm.js';
8
- export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
9
- export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
10
13
  export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
11
- export { default as FormErrorField } from './FormErrorField/FormErrorField.esm.js';
12
- export { default as FormNumberField } from './FormNumberField/FormNumberField.esm.js';
13
- export { default as FormNumberFieldDecimal } from './FormNumberField/FormNumberFieldDecimal.esm.js';
14
14
  export { default as FormRadioGroup } from './FormRadioGroup/FormRadioGroup.esm.js';
15
+ 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
+ export { default as FormBottomField } from './FormBottomField/FormBottomField.esm.js';
25
+ import 'dayjs';
26
+ export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
27
+ export { default as TimePicker } from './TimePicker/TimePicker.esm.js';
15
28
  export { default as FormRenderFileUpload } from './FileUpload/FormRenderFileUpload.esm.js';
16
29
  export { default as FormRenderMultiFileUpload } from './FileUpload/FormRenderMultiFileUpload.esm.js';
17
- export { default as FormTextAreaField } from './FormTextAreaField/FormTextAreaField.esm.js';
18
- export { default as FormTextField } from './FormTextField/FormTextField.esm.js';
30
+ import '@mui/icons-material/Visibility';
31
+ import '@mui/icons-material/Close';
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';
19
40
  export { default as HelperText } from './HelperText/HelperText.esm.js';
20
- export { default as Monthpickerrender } from './DatePicker/Monthpickerrender.esm.js';
21
- export { default as PasswordField } from './PasswordField/PasswordField.esm.js';
22
41
  export { default as RichTextEditor } from './RichTextEditor/RichTextEditor.esm.js';
23
42
  export { default as RichTextEditorWrapper } from './RichTextEditor/RichTextEditorWrapper.esm.js';
24
- export { default as TimePicker } from './TimePicker/TimePicker.esm.js';
25
- export { default as YearPickerField } from './YearPickerField/YearPickerField.esm.js';
@@ -98,10 +98,11 @@ export type ServerFilterDef = {
98
98
  defaultVisible?: boolean;
99
99
  } | {
100
100
  /**
101
- * Month-picker chip — lets users select **one or more months** across years.
102
- * Selected months are stored as a comma-separated string of `"YYYY-MM"` values
103
- * in `filterValues[filterKey]` (e.g. `"2025-01,2025-03"`).
104
- * Split on `","` in your `fetchPage` handler to build the server query.
101
+ * Month-picker chip — by default lets users select **one or more months**
102
+ * across years. Selected months are stored as a comma-separated string of
103
+ * `"YYYY-MM"` values in `filterValues[filterKey]` (e.g. `"2025-01,2025-03"`,
104
+ * or just `"2025-01"` when `single: true`). Split on `","` in your
105
+ * `fetchPage` handler to build the server query.
105
106
  */
106
107
  type: 'monthPicker';
107
108
  id: string;
@@ -109,6 +110,13 @@ export type ServerFilterDef = {
109
110
  filterKey: string;
110
111
  /** Show this chip in the filter bar on first render. Default: `false`. */
111
112
  defaultVisible?: boolean;
113
+ /**
114
+ * Restrict selection to a single month instead of the default
115
+ * multi-select behaviour. Clicking a month immediately applies and
116
+ * closes the popover (no Apply button), matching other single-value
117
+ * chips like the date picker. Default: `false` (multi-select).
118
+ */
119
+ single?: boolean;
112
120
  };
113
121
  interface AvailableColumn {
114
122
  id: string;
@@ -253,15 +253,28 @@ const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemo
253
253
  const [anchor, setAnchor] = useState(null);
254
254
  // Draft is only committed on Apply — prevents an API call per checkbox tick.
255
255
  const [draft, setDraft] = useState([]);
256
+ const [search, setSearch] = useState('');
256
257
  const active = selectedValues.length > 0;
257
258
  const openPopover = (e) => {
258
259
  setDraft(selectedValues); // always start from the committed state
260
+ setSearch(''); // always start with all options visible
259
261
  setAnchor(e.currentTarget);
260
262
  };
261
263
  const toggleDraft = (value) => setDraft((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]);
262
- const allSelected = draft.length === options.length && options.length > 0;
263
- const someSelected = draft.length > 0 && !allSelected;
264
- const toggleAll = () => setDraft(allSelected ? [] : options.map((o) => o.value));
264
+ // Case-insensitive filter — same pattern as SearchableSelectChip.
265
+ const filteredOptions = search
266
+ ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))
267
+ : options;
268
+ // "All" reflects/toggles only the currently visible (filtered) options —
269
+ // degrades to the original whole-list behaviour when search is empty.
270
+ const allSelected = filteredOptions.length > 0 && filteredOptions.every((o) => draft.includes(o.value));
271
+ const someSelected = !allSelected && filteredOptions.some((o) => draft.includes(o.value));
272
+ const toggleAll = () => setDraft((prev) => {
273
+ const filteredValues = filteredOptions.map((o) => o.value);
274
+ return allSelected
275
+ ? prev.filter((v) => !filteredValues.includes(v))
276
+ : [...new Set([...prev, ...filteredValues])];
277
+ });
265
278
  const handleApply = () => {
266
279
  onApply(draft);
267
280
  setAnchor(null);
@@ -284,19 +297,22 @@ const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemo
284
297
  px: 2, py: 0.75,
285
298
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
286
299
  borderBottom: '1px solid', borderColor: 'divider',
287
- }, children: [jsx(Typography, { sx: { fontSize: 11, color: draft.length > 0 ? 'primary.main' : 'text.secondary', fontWeight: draft.length > 0 ? 600 : 400 }, children: draft.length > 0 ? `${draft.length} of ${options.length} selected` : 'Select options' }), draft.length > 0 && (jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear all" }))] }), jsxs(ListItemButton, { onClick: toggleAll, sx: { px: 1, py: 0.25 }, children: [jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, size: "small", disableRipple: true, sx: { p: 0.5, mr: 0.5, '& .MuiSvgIcon-root': { fontSize: 16 } } }), jsx(ListItemText, { primary: "All", primaryTypographyProps: {
300
+ }, children: [jsx(Typography, { sx: { fontSize: 11, color: draft.length > 0 ? 'primary.main' : 'text.secondary', fontWeight: draft.length > 0 ? 600 : 400 }, children: draft.length > 0 ? `${draft.length} of ${options.length} selected` : 'Select options' }), draft.length > 0 && (jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear all" }))] }), jsx(Box, { sx: { px: 1.5, pt: 1, pb: 0.5 }, children: jsx(TextField, { size: "small", fullWidth: true, autoFocus: true, placeholder: "Search\u2026", value: search, onChange: (e) => setSearch(e.target.value), onKeyDown: (e) => { if (e.key === 'Escape')
301
+ setAnchor(null); }, InputProps: {
302
+ startAdornment: (jsx(InputAdornment, { position: "start", children: jsx(SearchIcon, { sx: { fontSize: 16, color: 'text.disabled' } }) })),
303
+ }, sx: { '& .MuiOutlinedInput-root': { borderRadius: 2, fontSize: 13 } } }) }), jsxs(ListItemButton, { onClick: toggleAll, sx: { px: 1, py: 0.25 }, children: [jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, size: "small", disableRipple: true, sx: { p: 0.5, mr: 0.5, '& .MuiSvgIcon-root': { fontSize: 16 } } }), jsx(ListItemText, { primary: "All", primaryTypographyProps: {
288
304
  fontSize: 13,
289
305
  fontStyle: 'italic',
290
306
  color: allSelected || someSelected ? 'primary.main' : 'text.secondary',
291
307
  fontWeight: allSelected ? 600 : 400,
292
- } })] }), jsx(Divider, {}), jsx(List, { dense: true, disablePadding: true, sx: { maxHeight: 240, overflowY: 'auto', py: 0.5 }, children: options.map((opt) => {
308
+ } })] }), jsx(Divider, {}), jsx(List, { dense: true, disablePadding: true, sx: { maxHeight: 240, overflowY: 'auto', py: 0.5 }, children: filteredOptions.length === 0 ? (jsx(Box, { sx: { px: 2, py: 1.5 }, children: jsxs(Typography, { variant: "caption", color: "text.disabled", children: ["No options match \"", search, "\""] }) })) : (filteredOptions.map((opt) => {
293
309
  const checked = draft.includes(opt.value);
294
310
  return (jsxs(ListItemButton, { onClick: () => toggleDraft(opt.value), sx: { px: 1, py: 0.25 }, children: [jsx(Checkbox, { checked: checked, size: "small", disableRipple: true, sx: { p: 0.5, mr: 0.5, '& .MuiSvgIcon-root': { fontSize: 16 } } }), jsx(ListItemText, { primary: opt.label, primaryTypographyProps: {
295
311
  fontSize: 13,
296
312
  color: checked ? 'primary.main' : 'text.primary',
297
313
  fontWeight: checked ? 600 : 400,
298
314
  } })] }, opt.value));
299
- }) }), jsx(Box, { sx: {
315
+ })) }), jsx(Box, { sx: {
300
316
  px: 1.5, py: 1,
301
317
  display: 'flex', justifyContent: 'flex-end',
302
318
  borderTop: '1px solid', borderColor: 'divider',
@@ -304,14 +320,19 @@ const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemo
304
320
  };
305
321
  // ─── MonthPickerChip ─────────────────────────────────────────────────────────
306
322
  //
307
- // Multi-month selection chip. Users navigate by year and tick individual months.
308
- // Draft changes are buffered locally; `onApply` is called once when Apply is
309
- // clicked (single API call per filter action).
323
+ // By default a multi-month selection chip: users navigate by year and tick
324
+ // individual months, draft changes are buffered locally, and `onApply` is
325
+ // called once when Apply is clicked (single API call per filter action).
310
326
  //
311
- // Value format: comma-separated "YYYY-MM" strings, e.g. "2025-01,2025-03".
327
+ // Pass `single` to restrict to one month at a time — clicking a month then
328
+ // immediately applies + closes the popover instead of buffering, matching
329
+ // the other single-value chips (e.g. DatePickerChip).
330
+ //
331
+ // Value format: comma-separated "YYYY-MM" strings, e.g. "2025-01,2025-03"
332
+ // (or just "2025-01" when `single` is set).
312
333
  // ─────────────────────────────────────────────────────────────────────────────
313
334
  const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
314
- const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId }) => {
335
+ const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId, single = false }) => {
315
336
  const parseYMs = (v) => v ? v.split(',').filter(Boolean) : [];
316
337
  const [anchor, setAnchor] = useState(null);
317
338
  const [viewYear, setViewYear] = useState(() => new Date().getFullYear());
@@ -328,7 +349,18 @@ const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId })
328
349
  }
329
350
  setAnchor(e.currentTarget);
330
351
  };
331
- const toggleMonth = (ym) => setDraft((prev) => prev.includes(ym) ? prev.filter((v) => v !== ym) : [...prev, ym]);
352
+ const toggleMonth = (ym) => {
353
+ if (single) {
354
+ // Clicking the already-selected month clears it; otherwise replace
355
+ // the selection and commit immediately (no Apply button in this mode).
356
+ const next = draft.includes(ym) ? [] : [ym];
357
+ setDraft(next);
358
+ onApply(next.join(','));
359
+ setAnchor(null);
360
+ return;
361
+ }
362
+ setDraft((prev) => prev.includes(ym) ? prev.filter((v) => v !== ym) : [...prev, ym]);
363
+ };
332
364
  const handleApply = () => {
333
365
  // Sort selected months chronologically before storing
334
366
  onApply([...draft].sort().join(','));
@@ -365,15 +397,15 @@ const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId })
365
397
  borderColor: isSelected ? 'primary.main' : 'divider',
366
398
  fontWeight: isSelected ? 700 : 400,
367
399
  }, children: mon }, ym));
368
- }) }), draft.length > 0 && (jsxs(Box, { sx: {
400
+ }) }), !single && draft.length > 0 && (jsxs(Box, { sx: {
369
401
  px: 2, py: 0.75,
370
402
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
371
403
  borderTop: '1px solid', borderColor: 'divider',
372
- }, children: [jsxs(Typography, { sx: { fontSize: 11, color: 'primary.main', fontWeight: 600 }, children: [draft.length, " month", draft.length > 1 ? 's' : '', " selected"] }), jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear" })] })), jsx(Box, { sx: {
404
+ }, children: [jsxs(Typography, { sx: { fontSize: 11, color: 'primary.main', fontWeight: 600 }, children: [draft.length, " month", draft.length > 1 ? 's' : '', " selected"] }), jsx(Button, { size: "small", color: "inherit", sx: { fontSize: 11, py: 0, textTransform: 'none', minWidth: 'auto' }, onClick: () => setDraft([]), children: "Clear" })] })), !single && (jsx(Box, { sx: {
373
405
  px: 1.5, py: 1,
374
406
  display: 'flex', justifyContent: 'flex-end',
375
407
  borderTop: '1px solid', borderColor: 'divider',
376
- }, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) })] })] }));
408
+ }, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) }))] })] }));
377
409
  };
378
410
  const GlobalSearchInput = memo(({ value, onChange, chipStyle, placeholder = 'Search…' }) => {
379
411
  const [draft, setDraft] = useState(value);
@@ -531,7 +563,7 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
531
563
  }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
532
564
  }
533
565
  if (def?.type === 'monthPicker') {
534
- return (jsx(MonthPickerChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
566
+ return (jsx(MonthPickerChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined, single: def.single }, id));
535
567
  }
536
568
  if (def?.type === 'date') {
537
569
  return (jsx(DatePickerChip, { label: def.label, value: getColValue(def.columnKey), onApply: (v) => applyColFilter(def.columnKey, v), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
@@ -2,7 +2,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { useMemo, useState, useRef, useLayoutEffect, useCallback, useEffect } from 'react';
3
3
  import { MRT_ToggleGlobalFilterButton, MRT_ShowHideColumnsButton, MRT_ToggleDensePaddingButton, MRT_ToggleFullScreenButton, useMaterialReactTable, MaterialReactTable } from 'material-react-table';
4
4
  import { useQuery, keepPreviousData } from '@tanstack/react-query';
5
- import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, Skeleton, CircularProgress } from '@mui/material';
5
+ import { Box, Tooltip, IconButton, Menu, MenuItem, GlobalStyles, Typography, Select, Skeleton } from '@mui/material';
6
6
  import RefreshIcon from '@mui/icons-material/Refresh';
7
7
  import FileDownloadIcon from '@mui/icons-material/FileDownload';
8
8
  import FirstPageIcon from '@mui/icons-material/FirstPage';
@@ -12,6 +12,11 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
12
12
  import DynamicFilterBar from './DynamicFilterBar.esm.js';
13
13
  import { getXLSX } from '../../utils/Constants/xlsx-js-style.esm.js';
14
14
 
15
+ // MRT's default overscan (rows rendered outside the visible viewport) is
16
+ // modest; bumping it reduces blank-row flash during fast wheel-scrolling
17
+ // through large virtualized batches. Static (no prop dependency), so it's
18
+ // hoisted for a stable reference across renders.
19
+ const ROW_VIRTUALIZER_OPTIONS = { overscan: 8 };
15
20
  const UI_DENSITY_PRESETS = {
16
21
  default: {
17
22
  footerMinHeight: 48,
@@ -120,6 +125,19 @@ const buildHeaderGlobalStyles = (headerMin, headerMax, headerFont, headerBgColor
120
125
  width: '100% !important',
121
126
  fontSize: `${headerFont} !important`,
122
127
  },
128
+ // Column resize handle: MRT's own default sx wins over the equivalent
129
+ // rule in buildHeaderCellSx (same CSS specificity, applied later in the
130
+ // emotion cache), leaving the drag hotspot a short ~24px strip centered
131
+ // in a much taller header cell — most of the column border is then
132
+ // unresponsive to drag. GlobalStyles + !important guarantees this wins.
133
+ '.tp-gpg-root .MuiTableHead-root .Mui-TableHeadCell-ResizeHandle-Wrapper': {
134
+ height: '100% !important',
135
+ top: '0 !important',
136
+ },
137
+ '.tp-gpg-root .MuiTableHead-root .Mui-TableHeadCell-ResizeHandle-Divider': {
138
+ height: '100% !important',
139
+ borderRight: 'none !important',
140
+ },
123
141
  };
124
142
  };
125
143
  /** Per-cell sx merged into MRT (via muiTableHeadCellProps). */
@@ -151,8 +169,6 @@ const buildHeaderCellSx = (align, headerMin, headerMax, headerFont, headerBgColo
151
169
  borderBottom: '2px solid rgba(0,0,0,0.15)',
152
170
  '& button[aria-label="Move"]': { display: 'none !important' },
153
171
  '& .MuiTableSortLabel-icon': { display: 'none !important' },
154
- '& .Mui-TableHeadCell-ResizeHandle-Wrapper': { height: '100%', top: 0 },
155
- '& .Mui-TableHeadCell-ResizeHandle-Divider': { borderRight: 'none' },
156
172
  '& .Mui-TableHeadCell-Content': {
157
173
  alignItems: 'center',
158
174
  justifyContent: flexJustify,
@@ -186,19 +202,19 @@ const buildHeaderCellSx = (align, headerMin, headerMax, headerFont, headerBgColo
186
202
  },
187
203
  };
188
204
  };
189
- // ─── Internal: overlay loader ─────────────────────────────────────────────────
190
- const OverlayLoader = ({ message }) => (jsxs(Box, { sx: {
191
- position: 'absolute', inset: 0,
192
- display: 'flex', flexDirection: 'column',
193
- alignItems: 'center', justifyContent: 'center',
194
- bgcolor: 'rgba(255,255,255,0.80)', backdropFilter: 'blur(2px)',
195
- zIndex: 10, gap: 1.5,
196
- }, children: [jsx(CircularProgress, { size: 50, thickness: 4 }), jsx(Typography, { variant: "body2", color: "text.secondary", fontWeight: 500, children: message })] }));
197
- // ─── Internal: skeleton loader (initial load — no data on screen yet) ────────
198
- const SkeletonOverlay = ({ columnCount, rowCount = 10, }) => (jsx(Box, { sx: {
205
+ // ─── Internal: skeleton loader ────────────────────────────────────────────────
206
+ //
207
+ // Used for BOTH loading states:
208
+ // - Initial load (isLoading, no data on screen yet) → opaque background
209
+ // - Refetch with existing data (isFetching, page/filter/sort change)
210
+ // → translucent + blurred so the stale rows are still faintly visible
211
+ // underneath, matching the previous OverlayLoader's backdrop treatment
212
+ // but with skeleton rows instead of a generic spinner.
213
+ const SkeletonOverlay = ({ columnCount, rowCount = 10, translucent = false, }) => (jsx(Box, { sx: {
199
214
  position: 'absolute', inset: 0,
200
215
  display: 'flex', flexDirection: 'column',
201
- bgcolor: 'background.paper',
216
+ bgcolor: translucent ? 'rgba(255,255,255,0.80)' : 'background.paper',
217
+ ...(translucent ? { backdropFilter: 'blur(2px)' } : {}),
202
218
  zIndex: 10, p: 1.5, gap: 1,
203
219
  overflow: 'hidden',
204
220
  }, children: Array.from({ length: rowCount }).map((_, rowIdx) => (jsx(Box, { sx: { display: 'flex', gap: 1.5 }, children: Array.from({ length: Math.max(columnCount, 1) }).map((_, colIdx) => (jsx(Skeleton, { variant: "rounded", height: 28, sx: { flex: 1, minWidth: 0 }, animation: "wave" }, colIdx))) }, rowIdx))) }));
@@ -353,6 +369,18 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
353
369
  const [columnFilters, setColumnFilters] = useState([]);
354
370
  const [globalFilter, setGlobalFilter] = useState('');
355
371
  const [columnPinning, setColumnPinning] = useState({});
372
+ const [exportMenuAnchor, setExportMenuAnchor] = useState(null);
373
+ // Whether any column filter or Global Search is currently narrowing what's
374
+ // displayed. When true, Export offers a choice (filtered vs. complete);
375
+ // when false there's nothing to choose between, so it downloads directly.
376
+ const isFiltered = columnFilters.length > 0 || globalFilter.trim() !== '';
377
+ // Always points at the latest MRT table instance (assigned right after
378
+ // useMaterialReactTable below) so handleExport — defined earlier in this
379
+ // component body — can read the CURRENT filtered row model at click time
380
+ // without needing `table` in its own dependency array (which would create
381
+ // a circular reference, since `table`'s own options include the toolbar
382
+ // renderer that uses handleExport).
383
+ const tableRef = useRef(null);
356
384
  // ── Viewport-fill height (mount + resize only — never on scroll) ─────────────
357
385
  //
358
386
  // Previously a scroll listener recalculated height as the grid moved within the
@@ -519,17 +547,24 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
519
547
  setGlobalFilter('');
520
548
  setPagination((p) => ({ ...p, pageIndex: 0 }));
521
549
  }, [defaultFilters, defaultVisible]);
522
- const handleExport = useCallback(async () => {
550
+ const handleExport = useCallback(async (scope = 'all') => {
523
551
  // Loaded on demand (and cached) so the xlsx-js-style library is excluded
524
552
  // from the initial bundle for consumers who never click Export.
525
553
  const XLSX = await getXLSX();
554
+ // "filtered" = exactly what's currently displayed (column filters + global
555
+ // search applied) — sourced from MRT's own row model so it can never drift
556
+ // from what the user sees. "all" = every row in the current fetched batch,
557
+ // ignoring column filters/global search (unchanged pre-existing behavior).
558
+ const exportRows = scope === 'filtered' && tableRef.current
559
+ ? tableRef.current.getFilteredRowModel().rows.map((r) => r.original)
560
+ : rows;
526
561
  const dataCols = columns.filter((c) => 'accessorKey' in c && c.accessorKey);
527
562
  // xlsx-js-style requires 8-char ARGB (AA + RRGGBB). Prepend full-opacity "FF".
528
563
  const toArgb = (hex, fallback = 'FF1565C0') => hex?.startsWith('#') ? 'FF' + hex.slice(1).toUpperCase().padEnd(6, '0') : fallback;
529
564
  const headerArgb = toArgb(exportHeaderBgColor ?? headerBgColor);
530
565
  const textArgb = toArgb(exportHeaderTextColor, 'FFFFFFFF');
531
566
  const headerRow = dataCols.map((c) => c.header ?? '');
532
- const dataRows = rows.map((row) => dataCols.map((c) => String(row[c.accessorKey] ?? '')));
567
+ const dataRows = exportRows.map((row) => dataCols.map((c) => String(row[c.accessorKey] ?? '')));
533
568
  const ws = XLSX.utils.aoa_to_sheet([headerRow, ...dataRows]);
534
569
  // Style header row
535
570
  dataCols.forEach((_, colIdx) => {
@@ -549,7 +584,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
549
584
  // Auto-fit column widths — single O(n) pass accumulates max length per column
550
585
  // instead of one reduce() per column (was O(n × cols)).
551
586
  const colWidths = dataCols.map((c) => (c.header ?? '').length);
552
- for (const row of rows) {
587
+ for (const row of exportRows) {
553
588
  const r = row;
554
589
  for (let ci = 0; ci < dataCols.length; ci++) {
555
590
  const len = String(r[dataCols[ci].accessorKey] ?? '').length;
@@ -560,7 +595,8 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
560
595
  ws['!cols'] = colWidths.map((w) => ({ wch: Math.max(w, 8) + 2 }));
561
596
  const wb = XLSX.utils.book_new();
562
597
  XLSX.utils.book_append_sheet(wb, ws, 'Data');
563
- XLSX.writeFile(wb, `${exportFilename}-page${pagination.pageIndex + 1}.xlsx`);
598
+ const suffix = scope === 'filtered' ? '-filtered' : '';
599
+ XLSX.writeFile(wb, `${exportFilename}${suffix}-page${pagination.pageIndex + 1}.xlsx`);
564
600
  }, [columns, rows, exportFilename, pagination.pageIndex, headerBgColor, exportHeaderBgColor, exportHeaderTextColor]);
565
601
  // ── Memoized cell-prop builders ───────────────────────────────────────────
566
602
  // These are functions passed to MRT per-cell. Without useCallback they get
@@ -625,9 +661,11 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
625
661
  alignItems: 'center',
626
662
  flexWrap: isNarrow ? 'wrap' : 'nowrap',
627
663
  flex: 1, minWidth: 0, pr: 1, gap: 0.5,
628
- }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexShrink: 0, gap: 0.25 }, children: [jsx(Tooltip, { title: "Clear filters & refresh", children: jsx("span", { children: jsx(IconButton, { id: `${resolvedGridId}_refresh_icon`, size: "small", onClick: () => { handleClearAll(); refetch(); }, disabled: isFetching, sx: { p: resolvedUiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(RefreshIcon, { sx: { fontSize: resolvedUiDensity === 'compact' ? 18 : 20 } }) }) }) }), jsx(Tooltip, { title: "Export batch as Excel (.xlsx)", children: jsx("span", { children: jsx(IconButton, { id: `${resolvedGridId}_export_icon`, size: "small", onClick: handleExport, disabled: rows.length === 0 || isFetching, sx: { p: resolvedUiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(FileDownloadIcon, { sx: { fontSize: resolvedUiDensity === 'compact' ? 18 : 20 } }) }) }) })] }), jsx(Box, { sx: { flex: 1, minWidth: isNarrow ? '100%' : 0 }, children: jsx(DynamicFilterBar, { serverFilterDefs: serverFilterDefs, visibleFilterIds: visibleFilterIds, onToggleFilter: handleToggleFilter, filterValues: filterValues, onFilterValuesChange: handleFilterValuesChange, columnFilters: columnFilters, onColumnFilterChange: setColumnFilters, availableColumns: availableColumns, maxVisibleFilters: effectiveMaxVisibleFilters, uiDensity: resolvedUiDensity, filterChipFontSize: densityTokens.filterChipFontSize, filterChipMinHeight: densityTokens.filterChipMinHeight, filterLabelFontSize: densityTokens.filterLabelFontSize, gridId: resolvedGridId }) })] })), [
664
+ }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexShrink: 0, gap: 0.25 }, children: [jsx(Tooltip, { title: "Clear filters & refresh", children: jsx("span", { children: jsx(IconButton, { id: `${resolvedGridId}_refresh_icon`, size: "small", onClick: () => { handleClearAll(); refetch(); }, disabled: isFetching, sx: { p: resolvedUiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(RefreshIcon, { sx: { fontSize: resolvedUiDensity === 'compact' ? 18 : 20 } }) }) }) }), jsx(Tooltip, { title: isFiltered
665
+ ? 'Export as Excel (.xlsx) — choose filtered or complete data'
666
+ : 'Export as Excel (.xlsx)', children: jsx("span", { children: jsx(IconButton, { id: `${resolvedGridId}_export_icon`, size: "small", onClick: (e) => isFiltered ? setExportMenuAnchor(e.currentTarget) : handleExport('all'), disabled: rows.length === 0 || isFetching, sx: { p: resolvedUiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(FileDownloadIcon, { sx: { fontSize: resolvedUiDensity === 'compact' ? 18 : 20 } }) }) }) }), jsxs(Menu, { id: `${resolvedGridId}_export_menu`, anchorEl: exportMenuAnchor, open: Boolean(exportMenuAnchor), onClose: () => setExportMenuAnchor(null), children: [jsxs(MenuItem, { id: `${resolvedGridId}_export_filtered_option`, onClick: () => { setExportMenuAnchor(null); handleExport('filtered'); }, children: ["Filtered data (", (tableRef.current?.getFilteredRowModel().rows.length ?? rows.length).toLocaleString(), " rows)"] }), jsxs(MenuItem, { id: `${resolvedGridId}_export_all_option`, onClick: () => { setExportMenuAnchor(null); handleExport('all'); }, children: ["Complete data (", rows.length.toLocaleString(), " rows)"] })] })] }), jsx(Box, { sx: { flex: 1, minWidth: isNarrow ? '100%' : 0 }, children: jsx(DynamicFilterBar, { serverFilterDefs: serverFilterDefs, visibleFilterIds: visibleFilterIds, onToggleFilter: handleToggleFilter, filterValues: filterValues, onFilterValuesChange: handleFilterValuesChange, columnFilters: columnFilters, onColumnFilterChange: setColumnFilters, availableColumns: availableColumns, maxVisibleFilters: effectiveMaxVisibleFilters, uiDensity: resolvedUiDensity, filterChipFontSize: densityTokens.filterChipFontSize, filterChipMinHeight: densityTokens.filterChipMinHeight, filterLabelFontSize: densityTokens.filterLabelFontSize, gridId: resolvedGridId }) })] })), [
629
667
  resolvedGridId, isFetching, rows.length, resolvedUiDensity, isNarrow,
630
- handleClearAll, refetch, handleExport,
668
+ handleClearAll, refetch, handleExport, isFiltered, exportMenuAnchor,
631
669
  serverFilterDefs, visibleFilterIds, handleToggleFilter,
632
670
  filterValues, handleFilterValuesChange, columnFilters, setColumnFilters,
633
671
  availableColumns, effectiveMaxVisibleFilters, densityTokens, handleToggleFilter,
@@ -731,6 +769,11 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
731
769
  enableHiding: true,
732
770
  enableStickyHeader: true,
733
771
  enableRowVirtualization: true,
772
+ // A larger overscan renders more off-screen rows up front, reducing the
773
+ // brief blank-row flash that can appear when scrolling quickly through
774
+ // a large in-memory batch (rows are already fetched — virtualization
775
+ // only governs what's mounted to the DOM at any given moment).
776
+ rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
734
777
  initialState: {
735
778
  density: densityTokens.mrtDensity,
736
779
  showGlobalFilter: showGlobalSearchByDefault,
@@ -816,6 +859,9 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
816
859
  renderToolbarInternalActions,
817
860
  renderBottomToolbar,
818
861
  });
862
+ // Keep the latest table instance available to handleExport (see tableRef
863
+ // declaration above) without adding `table` to its dependency array.
864
+ tableRef.current = table;
819
865
  return (jsxs(Box, { ref: containerRef, id: resolvedGridId, className: "tp-gpg-root", sx: {
820
866
  position: 'relative',
821
867
  height: resolvedHeight,
@@ -824,7 +870,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
824
870
  display: 'flex',
825
871
  flexDirection: 'column',
826
872
  overflow: 'hidden',
827
- }, children: [jsx(GlobalStyles, { styles: tableStyleOverrides }), isLoading && jsx(SkeletonOverlay, { columnCount: processedColumns.length }), !isLoading && isFetching && jsx(OverlayLoader, { message: "Updating data\u2026" }), jsx(Box, { sx: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: jsx(MaterialReactTable, { table: table }) })] }));
873
+ }, children: [jsx(GlobalStyles, { styles: tableStyleOverrides }), isLoading && jsx(SkeletonOverlay, { columnCount: processedColumns.length }), !isLoading && isFetching && (jsx(SkeletonOverlay, { columnCount: processedColumns.length, translucent: true })), jsx(Box, { sx: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: jsx(MaterialReactTable, { table: table }) })] }));
828
874
  }
829
875
 
830
876
  export { GenericPaginatedGrid as default };
@@ -1,4 +1,4 @@
1
- import { __exports as Calendar } from '../../../../../_virtual/index.esm6.js';
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 DateInput } from '../../../../../_virtual/index.esm14.js';
1
+ import { __exports as DateInput } 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
  import { __require as requireClassnames } from '../../../../classnames/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRange } from '../../../../../_virtual/index.esm7.js';
1
+ import { __exports as DateRange } from '../../../../../_virtual/index.esm4.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.esm5.js';
1
+ import { __exports as DateRangePicker } 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 requireDateRange } from '../DateRange/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DefinedRange } from '../../../../../_virtual/index.esm4.js';
1
+ import { __exports as DefinedRange } 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 requireStyles } from '../../styles.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as Month } from '../../../../../_virtual/index.esm13.js';
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' | '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' | '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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tp-react-elements",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "React form components library built with React Hook Form and Yup",