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.
- package/dist/_virtual/index.esm13.js +2 -2
- package/dist/_virtual/index.esm14.js +2 -2
- package/dist/_virtual/index.esm4.js +2 -2
- package/dist/_virtual/index.esm5.js +2 -2
- package/dist/_virtual/index.esm6.js +2 -2
- package/dist/_virtual/index.esm7.js +2 -2
- package/dist/components/DatePicker/index.d.ts +1 -0
- package/dist/components/DatePicker/index.esm.js +2 -1
- package/dist/components/Form/FormConstants.esm.js +1 -0
- package/dist/components/Form/FormRender.esm.js +13 -9
- package/dist/components/FormComponents/DatePicker/MonthPickerMultiRender.d.ts +6 -0
- package/dist/components/FormComponents/DatePicker/MonthPickerMultiRender.esm.js +117 -0
- package/dist/components/FormComponents/index.esm.js +29 -12
- package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +12 -4
- package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +48 -16
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +67 -21
- package/dist/node_modules/react-date-range/dist/components/Calendar/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateInput/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateRange/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DateRangePicker/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/DefinedRange/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/Month/index.esm.js +1 -1
- package/dist/utils/Interface/FormInterface.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateInput = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateInput as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var Month = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Month 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 DefinedRange = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DefinedRange as __exports };
|
|
@@ -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 Calendar = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
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,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
|
-
|
|
18
|
-
|
|
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**
|
|
102
|
-
* Selected months are stored as a comma-separated string of
|
|
103
|
-
* in `filterValues[filterKey]` (e.g. `"2025-01,2025-03"
|
|
104
|
-
*
|
|
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
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
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" }))] }),
|
|
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
|
-
//
|
|
308
|
-
//
|
|
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
|
-
//
|
|
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) =>
|
|
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,
|
|
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:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|