tp-react-elements 2.0.2 → 2.0.4
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/README.md +8 -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/FormComponents/Select/MultiSelect.d.ts +10 -0
- package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +27 -2
- package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +174 -41
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.d.ts +15 -2
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +103 -36
- package/dist/components/PaginatedGrid/mockData.d.ts +9 -1
- package/dist/components/PaginatedGrid/types.d.ts +0 -17
- package/dist/node_modules/react-date-range/dist/components/Calendar/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/package.json +1 -1
- package/dist/components/PaginatedGrid/invoiceApi.d.ts +0 -2
package/README.md
CHANGED
|
@@ -202,8 +202,14 @@ npm run storybook
|
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
- **Docs → Paginated Grid** — API reference plus **four copy-paste examples** (embedded panel, wrapped headers, typography, invoice filters)
|
|
205
|
-
- **Components → PaginatedGrid → Sample Grids** — mock grids (no API): header wrap, compact layout, viewport fill
|
|
206
|
-
- **Components → PaginatedGrid → Example Grid** —
|
|
205
|
+
- **Components → PaginatedGrid → Sample Grids** — mock grids (no API): header wrap, compact layout, viewport fill, multi-select with Apply button, month picker
|
|
206
|
+
- **Components → PaginatedGrid → Example Grid** — invoice grid demo using mock data (no API)
|
|
207
|
+
|
|
208
|
+
**Global Search:** every `GenericPaginatedGrid` includes a built-in search icon in the top-right toolbar — **collapsed by default** (set `showGlobalSearchByDefault={true}` to start expanded). Click the icon to expand a free-text search that filters across all visible columns of the current batch. See **Docs → Paginated Grid → Global search**.
|
|
209
|
+
|
|
210
|
+
**Excel export:** the Export button downloads an `.xlsx` file with the header row styled in `headerBgColor` (bold white text) and columns auto-sized to fit content. Requires `xlsx-js-style` as a peer dependency.
|
|
211
|
+
|
|
212
|
+
**Element IDs:** every interactive element (toolbar buttons, filter chips, pagination controls) receives a stable `id` attribute. Pass `gridId="invoice"` (or omit to default to `exportFilename`) and all elements follow the pattern `{gridId}_refresh_icon`, `{gridId}_dateRange_chip`, `{gridId}_next_page_icon`, etc. See **Docs → Paginated Grid — Element IDs** in Storybook for the full reference table.
|
|
207
213
|
|
|
208
214
|
**Example — compact embedded grid with smaller fonts:**
|
|
209
215
|
|
|
@@ -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 Calendar = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { Calendar as __exports };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
1
|
+
var DateRangePicker = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateRangePicker as __exports };
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MultiSelect` is currently a stub placeholder pending activation.
|
|
3
|
+
*
|
|
4
|
+
* Use one of these fully-implemented alternatives instead:
|
|
5
|
+
* - `MultiSelectAutocomplete` (react-hook-form aware) — Docs → Multi Select Autocomplete
|
|
6
|
+
* - `MultiSelectField` (standalone) — Docs → Multi Select Field
|
|
7
|
+
* - `MultiSelectV1` (react-hook-form aware, used via FormRender `inputType: 'multiselect'`)
|
|
8
|
+
*
|
|
9
|
+
* See Docs → Multi Select (docs/multi-select.mdx) for status details.
|
|
10
|
+
*/
|
|
1
11
|
declare const MultiSelect: () => import("react/jsx-runtime").JSX.Element;
|
|
2
12
|
export default MultiSelect;
|
|
@@ -96,6 +96,19 @@ export type ServerFilterDef = {
|
|
|
96
96
|
}[];
|
|
97
97
|
/** Show this chip in the filter bar on first render. Default: `false`. */
|
|
98
98
|
defaultVisible?: boolean;
|
|
99
|
+
} | {
|
|
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.
|
|
105
|
+
*/
|
|
106
|
+
type: 'monthPicker';
|
|
107
|
+
id: string;
|
|
108
|
+
label: string;
|
|
109
|
+
filterKey: string;
|
|
110
|
+
/** Show this chip in the filter bar on first render. Default: `false`. */
|
|
111
|
+
defaultVisible?: boolean;
|
|
99
112
|
};
|
|
100
113
|
interface AvailableColumn {
|
|
101
114
|
id: string;
|
|
@@ -127,6 +140,18 @@ export interface DynamicFilterBarProps {
|
|
|
127
140
|
filterChipFontSize?: number;
|
|
128
141
|
filterChipMinHeight?: number;
|
|
129
142
|
filterLabelFontSize?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Current value of MRT's global filter (free-text search across every
|
|
145
|
+
* visible column). When provided together with `onGlobalFilterChange`, a
|
|
146
|
+
* search box is rendered next to "Manage Filters".
|
|
147
|
+
*/
|
|
148
|
+
globalFilter?: string;
|
|
149
|
+
/** Called (debounced) as the user types in the global search box. */
|
|
150
|
+
onGlobalFilterChange?: (value: string) => void;
|
|
151
|
+
/** Placeholder text for the global search box. Default: "Search…". */
|
|
152
|
+
globalSearchPlaceholder?: string;
|
|
153
|
+
/** ID prefix for all interactive elements — passed down to chips and ManageFilters. */
|
|
154
|
+
gridId?: string;
|
|
130
155
|
}
|
|
131
|
-
declare const
|
|
132
|
-
export default
|
|
156
|
+
declare const _default: import("react").MemoExoticComponent<({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, maxVisibleFilters, uiDensity, filterChipFontSize, filterChipMinHeight, filterLabelFontSize, globalFilter, onGlobalFilterChange, globalSearchPlaceholder, gridId, }: DynamicFilterBarProps) => import("react/jsx-runtime").JSX.Element>;
|
|
157
|
+
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { memo, useState, useEffect } from 'react';
|
|
3
3
|
import { useForm } from 'react-hook-form';
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
5
|
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
@@ -9,7 +9,7 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
|
9
9
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
10
10
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
11
11
|
import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
|
|
12
|
-
import { Box, Typography, Tooltip, Button, Popover,
|
|
12
|
+
import { TextField, InputAdornment, IconButton, Box, Typography, Tooltip, Button, Popover, Stack, List, ListItemButton, ListItemText, Checkbox, Divider } from '@mui/material';
|
|
13
13
|
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
|
14
14
|
import TuneIcon from '@mui/icons-material/Tune';
|
|
15
15
|
import CloseIcon from '@mui/icons-material/Close';
|
|
@@ -17,6 +17,8 @@ import FilterListIcon from '@mui/icons-material/FilterList';
|
|
|
17
17
|
import CheckIcon from '@mui/icons-material/Check';
|
|
18
18
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
|
19
19
|
import SearchIcon from '@mui/icons-material/Search';
|
|
20
|
+
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
|
21
|
+
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
20
22
|
|
|
21
23
|
// Enable strict-format parsing so dayjs can handle the DD/MM/YYYY strings
|
|
22
24
|
// that DatepickerWrapperV2 passes to onChangeFn
|
|
@@ -30,6 +32,7 @@ const TYPE_PILL = {
|
|
|
30
32
|
dropdown: { label: 'Select', color: '#6a1b9a', bg: '#f3e5f5' },
|
|
31
33
|
searchableDropdown: { label: 'Search', color: '#bf360c', bg: '#fbe9e7' },
|
|
32
34
|
multiSelect: { label: 'Multi', color: '#0277bd', bg: '#e1f5fe' },
|
|
35
|
+
monthPicker: { label: 'Month', color: '#6a1b9a', bg: '#f3e5f5' },
|
|
33
36
|
};
|
|
34
37
|
// Column filter type pills (used in COLUMN FILTERS section of Manage Filters)
|
|
35
38
|
const COLUMN_TYPE_PILL = {
|
|
@@ -78,7 +81,7 @@ const fmtDate = (iso) => {
|
|
|
78
81
|
const yearSuffix = y !== thisYear ? ` '${y.slice(2)}` : '';
|
|
79
82
|
return `${Number(d)} ${months[Number(m) - 1]}${yearSuffix}`;
|
|
80
83
|
};
|
|
81
|
-
const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove, chipStyle }) => {
|
|
84
|
+
const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove, chipStyle, baseId }) => {
|
|
82
85
|
// Inject react-date-range CSS via JS (not CSS import) to prevent it from being
|
|
83
86
|
// bundled into the library's extracted CSS file and leaking into consumer apps.
|
|
84
87
|
useRdrStyles();
|
|
@@ -120,14 +123,14 @@ const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove, chipStyle }
|
|
|
120
123
|
setAnchor(null);
|
|
121
124
|
}
|
|
122
125
|
};
|
|
123
|
-
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipLabel : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: openPicker, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipLabel }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, overflow: 'hidden' } }, children: [jsx(distExports.DateRange, { editableDateInputs: false, onChange: handleRangeChange, moveRangeOnFirstSelection: false, ranges: selRange, months: 2, direction: "horizontal", showDateDisplay: false, dateDisplayFormat: "dd/MM/yyyy", rangeColors: ['#1976d2'] }), active && (jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end', px: 2, pb: 1.5 }, children: jsx(Button, { size: "small", color: "inherit", onClick: () => { onApply('', ''); setAnchor(null); }, children: "Clear" }) }))] })] }));
|
|
126
|
+
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipLabel : '', placement: "top", arrow: true, children: jsx(Button, { id: baseId ? `${baseId}_chip` : undefined, variant: "outlined", size: "small", onClick: openPicker, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { id: baseId ? `${baseId}_remove_icon` : undefined, sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipLabel }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, overflow: 'hidden' } }, children: [jsx(distExports.DateRange, { editableDateInputs: false, onChange: handleRangeChange, moveRangeOnFirstSelection: false, ranges: selRange, months: 2, direction: "horizontal", showDateDisplay: false, dateDisplayFormat: "dd/MM/yyyy", rangeColors: ['#1976d2'] }), active && (jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end', px: 2, pb: 1.5 }, children: jsx(Button, { size: "small", color: "inherit", onClick: () => { onApply('', ''); setAnchor(null); }, children: "Clear" }) }))] })] }));
|
|
124
127
|
};
|
|
125
|
-
const TextFilterChip = ({ label, value, onApply, onRemove, placeholder, chipStyle }) => {
|
|
128
|
+
const TextFilterChip = ({ label, value, onApply, onRemove, placeholder, chipStyle, baseId }) => {
|
|
126
129
|
const [anchor, setAnchor] = useState(null);
|
|
127
130
|
const [draft, setDraft] = useState('');
|
|
128
131
|
const active = Boolean(value);
|
|
129
132
|
const chipText = active ? `${label}: ${value}` : label;
|
|
130
|
-
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: (e) => { setDraft(value); setAnchor(e.currentTarget); }, endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, minWidth: 240 } }, children: jsxs(Box, { sx: { p: 2 }, children: [jsx(Typography, { variant: "caption", fontWeight: 600, color: "text.secondary", display: "block", mb: 1.5, children: label }), jsx(TextField, { value: draft, onChange: (e) => setDraft(e.target.value), placeholder: placeholder, size: "small", fullWidth: true, autoFocus: true, onKeyDown: (e) => {
|
|
133
|
+
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { id: baseId ? `${baseId}_chip` : undefined, variant: "outlined", size: "small", onClick: (e) => { setDraft(value); setAnchor(e.currentTarget); }, endIcon: jsx(CloseIcon, { id: baseId ? `${baseId}_remove_icon` : undefined, sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, minWidth: 240 } }, children: jsxs(Box, { sx: { p: 2 }, children: [jsx(Typography, { variant: "caption", fontWeight: 600, color: "text.secondary", display: "block", mb: 1.5, children: label }), jsx(TextField, { value: draft, onChange: (e) => setDraft(e.target.value), placeholder: placeholder, size: "small", fullWidth: true, autoFocus: true, onKeyDown: (e) => {
|
|
131
134
|
if (e.key === 'Enter') {
|
|
132
135
|
onApply(draft);
|
|
133
136
|
setAnchor(null);
|
|
@@ -136,7 +139,7 @@ const TextFilterChip = ({ label, value, onApply, onRemove, placeholder, chipStyl
|
|
|
136
139
|
setAnchor(null);
|
|
137
140
|
} }), jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", mt: 1.5, children: [active && (jsx(Button, { size: "small", color: "inherit", onClick: () => { onApply(''); setAnchor(null); }, children: "Clear" })), jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: () => { onApply(draft); setAnchor(null); }, children: "Apply" })] })] }) })] }));
|
|
138
141
|
};
|
|
139
|
-
const DatePickerChip = ({ label, value, onApply, onRemove, chipStyle }) => {
|
|
142
|
+
const DatePickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId }) => {
|
|
140
143
|
const [open, setOpen] = useState(false);
|
|
141
144
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
142
145
|
const active = Boolean(value);
|
|
@@ -160,7 +163,7 @@ const DatePickerChip = ({ label, value, onApply, onRemove, chipStyle }) => {
|
|
|
160
163
|
// LocalizationProvider must wrap DatePicker — scoped here so it only
|
|
161
164
|
// loads when the chip is actually used (DatePickerChip is tree-shaken
|
|
162
165
|
// if the 'date' filter type is never included in serverFilterDefs).
|
|
163
|
-
jsxs(LocalizationProvider, { dateAdapter: AdapterDayjs, children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: handleChipClick, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(DatePicker, { value: dayjsValue, open: open, onClose: () => setOpen(false), onChange: handleChange, slotProps: {
|
|
166
|
+
jsxs(LocalizationProvider, { dateAdapter: AdapterDayjs, children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { id: baseId ? `${baseId}_chip` : undefined, variant: "outlined", size: "small", onClick: handleChipClick, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { id: baseId ? `${baseId}_remove_icon` : undefined, sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(DatePicker, { value: dayjsValue, open: open, onClose: () => setOpen(false), onChange: handleChange, slotProps: {
|
|
164
167
|
textField: { sx: { display: 'none' }, size: 'small' },
|
|
165
168
|
popper: {
|
|
166
169
|
anchorEl,
|
|
@@ -168,7 +171,7 @@ const DatePickerChip = ({ label, value, onApply, onRemove, chipStyle }) => {
|
|
|
168
171
|
},
|
|
169
172
|
} })] }));
|
|
170
173
|
};
|
|
171
|
-
const DropdownFilterChip = ({ label, value, options, onApply, onRemove, chipStyle }) => {
|
|
174
|
+
const DropdownFilterChip = ({ label, value, options, onApply, onRemove, chipStyle, baseId }) => {
|
|
172
175
|
const [anchor, setAnchor] = useState(null);
|
|
173
176
|
const active = Boolean(value);
|
|
174
177
|
const selectedLabel = options.find((o) => o.value === value)?.label ?? '';
|
|
@@ -199,7 +202,7 @@ const DropdownFilterChip = ({ label, value, options, onApply, onRemove, chipStyl
|
|
|
199
202
|
],
|
|
200
203
|
};
|
|
201
204
|
const chipText = active ? `${label}: ${selectedLabel}` : label;
|
|
202
|
-
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: (e) => setAnchor(e.currentTarget), startIcon: jsx(ArrowDropDownIcon, { sx: { fontSize: '15px !important', mr: -0.5 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, p: 1.5, minWidth: 240 } }, children: jsx(SingleSelectNonAutoComplete, { props: {
|
|
205
|
+
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { id: baseId ? `${baseId}_chip` : undefined, variant: "outlined", size: "small", onClick: (e) => setAnchor(e.currentTarget), startIcon: jsx(ArrowDropDownIcon, { sx: { fontSize: '15px !important', mr: -0.5 } }), endIcon: jsx(CloseIcon, { id: baseId ? `${baseId}_remove_icon` : undefined, sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsx(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, p: 1.5, minWidth: 240 } }, children: jsx(SingleSelectNonAutoComplete, { props: {
|
|
203
206
|
item,
|
|
204
207
|
register,
|
|
205
208
|
control,
|
|
@@ -210,7 +213,7 @@ const DropdownFilterChip = ({ label, value, options, onApply, onRemove, chipStyl
|
|
|
210
213
|
watch,
|
|
211
214
|
}, variant: "outlined" }) })] }));
|
|
212
215
|
};
|
|
213
|
-
const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeholder = 'Search…', chipStyle, }) => {
|
|
216
|
+
const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeholder = 'Search…', chipStyle, baseId, }) => {
|
|
214
217
|
const [anchor, setAnchor] = useState(null);
|
|
215
218
|
const [search, setSearch] = useState('');
|
|
216
219
|
const active = Boolean(value);
|
|
@@ -228,7 +231,7 @@ const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeh
|
|
|
228
231
|
? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))
|
|
229
232
|
: options;
|
|
230
233
|
const chipText = active ? `${label}: ${selectedLabel}` : label;
|
|
231
|
-
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick: open, startIcon: jsx(SearchIcon, { sx: { fontSize: '14px !important', mr: -0.25 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: {
|
|
234
|
+
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { id: baseId ? `${baseId}_chip` : undefined, variant: "outlined", size: "small", onClick: open, startIcon: jsx(SearchIcon, { sx: { fontSize: '14px !important', mr: -0.25 } }), endIcon: jsx(CloseIcon, { id: baseId ? `${baseId}_remove_icon` : undefined, sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: {
|
|
232
235
|
sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, width: 280, display: 'flex', flexDirection: 'column' },
|
|
233
236
|
}, children: [jsx(Box, { sx: { px: 1.5, pt: 1.5, pb: 0.75 }, children: jsx(TextField, { size: "small", fullWidth: true, autoFocus: true, placeholder: placeholder, value: search, onChange: (e) => setSearch(e.target.value), onKeyDown: (e) => { if (e.key === 'Escape')
|
|
234
237
|
setAnchor(null); }, InputProps: {
|
|
@@ -246,15 +249,24 @@ const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeh
|
|
|
246
249
|
} }), isSelected && (jsx(CheckIcon, { sx: { fontSize: 15, color: 'primary.main', flexShrink: 0 } }))] }, opt.value));
|
|
247
250
|
}))] })] })] }));
|
|
248
251
|
};
|
|
249
|
-
const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemove, chipStyle, }) => {
|
|
252
|
+
const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemove, chipStyle, baseId, }) => {
|
|
250
253
|
const [anchor, setAnchor] = useState(null);
|
|
254
|
+
// Draft is only committed on Apply — prevents an API call per checkbox tick.
|
|
255
|
+
const [draft, setDraft] = useState([]);
|
|
251
256
|
const active = selectedValues.length > 0;
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
const openPopover = (e) => {
|
|
258
|
+
setDraft(selectedValues); // always start from the committed state
|
|
259
|
+
setAnchor(e.currentTarget);
|
|
260
|
+
};
|
|
261
|
+
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));
|
|
265
|
+
const handleApply = () => {
|
|
266
|
+
onApply(draft);
|
|
267
|
+
setAnchor(null);
|
|
257
268
|
};
|
|
269
|
+
// Chip label reflects committed values, not the open draft
|
|
258
270
|
const chipText = (() => {
|
|
259
271
|
if (!active)
|
|
260
272
|
return label;
|
|
@@ -268,20 +280,137 @@ const MultiSelectFilterChip = ({ label, selectedValues, options, onApply, onRemo
|
|
|
268
280
|
}
|
|
269
281
|
return `${label}: ${selectedValues.length} selected`;
|
|
270
282
|
})();
|
|
271
|
-
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { variant: "outlined", size: "small", onClick:
|
|
283
|
+
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { id: baseId ? `${baseId}_chip` : undefined, variant: "outlined", size: "small", onClick: openPopover, startIcon: jsx(FilterListIcon, { sx: { fontSize: '14px !important', mr: -0.25 } }), endIcon: jsx(CloseIcon, { id: baseId ? `${baseId}_remove_icon` : undefined, sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, width: 260 } }, children: [jsxs(Box, { sx: {
|
|
272
284
|
px: 2, py: 0.75,
|
|
273
285
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
274
286
|
borderBottom: '1px solid', borderColor: 'divider',
|
|
275
|
-
}, children: [
|
|
276
|
-
|
|
277
|
-
|
|
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: {
|
|
288
|
+
fontSize: 13,
|
|
289
|
+
fontStyle: 'italic',
|
|
290
|
+
color: allSelected || someSelected ? 'primary.main' : 'text.secondary',
|
|
291
|
+
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) => {
|
|
293
|
+
const checked = draft.includes(opt.value);
|
|
294
|
+
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: {
|
|
278
295
|
fontSize: 13,
|
|
279
296
|
color: checked ? 'primary.main' : 'text.primary',
|
|
280
297
|
fontWeight: checked ? 600 : 400,
|
|
281
298
|
} })] }, opt.value));
|
|
282
|
-
}) })
|
|
299
|
+
}) }), jsx(Box, { sx: {
|
|
300
|
+
px: 1.5, py: 1,
|
|
301
|
+
display: 'flex', justifyContent: 'flex-end',
|
|
302
|
+
borderTop: '1px solid', borderColor: 'divider',
|
|
303
|
+
}, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) })] })] }));
|
|
283
304
|
};
|
|
284
|
-
|
|
305
|
+
// ─── MonthPickerChip ─────────────────────────────────────────────────────────
|
|
306
|
+
//
|
|
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).
|
|
310
|
+
//
|
|
311
|
+
// Value format: comma-separated "YYYY-MM" strings, e.g. "2025-01,2025-03".
|
|
312
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
313
|
+
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
314
|
+
const MonthPickerChip = ({ label, value, onApply, onRemove, chipStyle, baseId }) => {
|
|
315
|
+
const parseYMs = (v) => v ? v.split(',').filter(Boolean) : [];
|
|
316
|
+
const [anchor, setAnchor] = useState(null);
|
|
317
|
+
const [viewYear, setViewYear] = useState(() => new Date().getFullYear());
|
|
318
|
+
const [draft, setDraft] = useState(() => parseYMs(value));
|
|
319
|
+
const committed = parseYMs(value);
|
|
320
|
+
const active = committed.length > 0;
|
|
321
|
+
// Sync view year to earliest selected year when opening
|
|
322
|
+
const openPopover = (e) => {
|
|
323
|
+
const yms = parseYMs(value);
|
|
324
|
+
setDraft(yms);
|
|
325
|
+
if (yms.length > 0) {
|
|
326
|
+
const years = yms.map((ym) => Number(ym.split('-')[0]));
|
|
327
|
+
setViewYear(Math.min(...years));
|
|
328
|
+
}
|
|
329
|
+
setAnchor(e.currentTarget);
|
|
330
|
+
};
|
|
331
|
+
const toggleMonth = (ym) => setDraft((prev) => prev.includes(ym) ? prev.filter((v) => v !== ym) : [...prev, ym]);
|
|
332
|
+
const handleApply = () => {
|
|
333
|
+
// Sort selected months chronologically before storing
|
|
334
|
+
onApply([...draft].sort().join(','));
|
|
335
|
+
setAnchor(null);
|
|
336
|
+
};
|
|
337
|
+
const chipText = (() => {
|
|
338
|
+
if (!active)
|
|
339
|
+
return label;
|
|
340
|
+
if (committed.length === 1) {
|
|
341
|
+
const [y, m] = committed[0].split('-');
|
|
342
|
+
return `${MONTH_LABELS[Number(m) - 1]} ${y}`;
|
|
343
|
+
}
|
|
344
|
+
if (committed.length <= 3) {
|
|
345
|
+
const parts = committed.map((ym) => {
|
|
346
|
+
const [y, m] = ym.split('-');
|
|
347
|
+
return `${MONTH_LABELS[Number(m) - 1]} '${y.slice(2)}`;
|
|
348
|
+
});
|
|
349
|
+
return `${label}: ${parts.join(', ')}`;
|
|
350
|
+
}
|
|
351
|
+
return `${label}: ${committed.length} months`;
|
|
352
|
+
})();
|
|
353
|
+
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: active ? chipText : '', placement: "top", arrow: true, children: jsx(Button, { id: baseId ? `${baseId}_chip` : undefined, variant: "outlined", size: "small", onClick: openPopover, startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { id: baseId ? `${baseId}_remove_icon` : undefined, sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active, chipStyle), children: jsx(ChipLabel, { text: chipText }) }) }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, width: 280 } }, children: [jsxs(Box, { sx: {
|
|
354
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
355
|
+
px: 1.5, py: 0.75,
|
|
356
|
+
borderBottom: '1px solid', borderColor: 'divider',
|
|
357
|
+
}, children: [jsx(IconButton, { id: baseId ? `${baseId}_prev_year_icon` : undefined, 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, { id: baseId ? `${baseId}_next_year_icon` : undefined, 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) => {
|
|
358
|
+
const ym = `${viewYear}-${String(idx + 1).padStart(2, '0')}`;
|
|
359
|
+
const isSelected = draft.includes(ym);
|
|
360
|
+
return (jsx(Button, { id: baseId ? `${baseId}_month_${ym}` : undefined, variant: isSelected ? 'contained' : 'outlined', size: "small", disableElevation: true, onClick: () => toggleMonth(ym), sx: {
|
|
361
|
+
fontSize: 12,
|
|
362
|
+
textTransform: 'none',
|
|
363
|
+
borderRadius: 2,
|
|
364
|
+
py: 0.75,
|
|
365
|
+
borderColor: isSelected ? 'primary.main' : 'divider',
|
|
366
|
+
fontWeight: isSelected ? 700 : 400,
|
|
367
|
+
}, children: mon }, ym));
|
|
368
|
+
}) }), draft.length > 0 && (jsxs(Box, { sx: {
|
|
369
|
+
px: 2, py: 0.75,
|
|
370
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
371
|
+
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: {
|
|
373
|
+
px: 1.5, py: 1,
|
|
374
|
+
display: 'flex', justifyContent: 'flex-end',
|
|
375
|
+
borderTop: '1px solid', borderColor: 'divider',
|
|
376
|
+
}, children: jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: handleApply, children: "Apply" }) })] })] }));
|
|
377
|
+
};
|
|
378
|
+
const GlobalSearchInput = memo(({ value, onChange, chipStyle, placeholder = 'Search…' }) => {
|
|
379
|
+
const [draft, setDraft] = useState(value);
|
|
380
|
+
// Keep local draft in sync when the external value changes elsewhere
|
|
381
|
+
// (e.g. "Clear filters & refresh" resets globalFilter to '').
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
setDraft(value);
|
|
384
|
+
}, [value]);
|
|
385
|
+
// Debounce: push to parent (and trigger MRT's global filter) 250ms after typing stops.
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
if (draft === value)
|
|
388
|
+
return;
|
|
389
|
+
const handle = setTimeout(() => onChange(draft), 250);
|
|
390
|
+
return () => clearTimeout(handle);
|
|
391
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
392
|
+
}, [draft]);
|
|
393
|
+
const handleClear = () => {
|
|
394
|
+
setDraft('');
|
|
395
|
+
onChange('');
|
|
396
|
+
};
|
|
397
|
+
return (jsx(TextField, { value: draft, onChange: (e) => setDraft(e.target.value), placeholder: placeholder, size: "small", variant: "outlined", "aria-label": "Search all columns", InputProps: {
|
|
398
|
+
startAdornment: (jsx(InputAdornment, { position: "start", children: jsx(SearchIcon, { sx: { fontSize: 16, color: 'text.disabled' } }) })),
|
|
399
|
+
endAdornment: draft ? (jsx(InputAdornment, { position: "end", children: jsx(IconButton, { size: "small", onClick: handleClear, "aria-label": "Clear search", sx: { p: 0.25 }, children: jsx(CloseIcon, { sx: { fontSize: '14px !important' } }) }) })) : undefined,
|
|
400
|
+
}, sx: {
|
|
401
|
+
minWidth: 160,
|
|
402
|
+
maxWidth: 220,
|
|
403
|
+
flexShrink: 0,
|
|
404
|
+
'& .MuiOutlinedInput-root': {
|
|
405
|
+
borderRadius: 5,
|
|
406
|
+
fontSize: chipStyle.fontSize,
|
|
407
|
+
height: chipStyle.minHeight,
|
|
408
|
+
},
|
|
409
|
+
'& .MuiOutlinedInput-input': { py: 0 },
|
|
410
|
+
} }));
|
|
411
|
+
});
|
|
412
|
+
GlobalSearchInput.displayName = 'GlobalSearchInput';
|
|
413
|
+
const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, onToggle, maxVisible = 4, chipStyle, gridId, }) => {
|
|
285
414
|
const [anchor, setAnchor] = useState(null);
|
|
286
415
|
const atLimit = visibleIds.length >= maxVisible;
|
|
287
416
|
// Shared pill box renderer
|
|
@@ -296,7 +425,7 @@ const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, o
|
|
|
296
425
|
letterSpacing: 0.4,
|
|
297
426
|
lineHeight: 1.6,
|
|
298
427
|
}, children: label }));
|
|
299
|
-
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: "Add or remove filter chips", children: jsx(Button, { size: "small", variant: "outlined", startIcon: jsx(TuneIcon, { sx: { fontSize: 14 } }), onClick: (e) => setAnchor(e.currentTarget), sx: {
|
|
428
|
+
return (jsxs(Fragment, { children: [jsx(Tooltip, { title: "Add or remove filter chips", children: jsx(Button, { id: gridId ? `${gridId}_manage_filters_icon` : undefined, size: "small", variant: "outlined", startIcon: jsx(TuneIcon, { sx: { fontSize: 14 } }), onClick: (e) => setAnchor(e.currentTarget), sx: {
|
|
300
429
|
borderRadius: 5, textTransform: 'none', fontSize: chipStyle.fontSize,
|
|
301
430
|
borderColor: 'divider', color: 'text.secondary',
|
|
302
431
|
px: chipStyle.px ?? 1.5, minHeight: chipStyle.minHeight,
|
|
@@ -327,7 +456,7 @@ const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, o
|
|
|
327
456
|
} }), selected && jsx(CheckIcon, { sx: { fontSize: 16, color: 'primary.main', flexShrink: 0 } })] }) }) }, col.id));
|
|
328
457
|
}) })] }))] })] })] }));
|
|
329
458
|
};
|
|
330
|
-
const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, maxVisibleFilters = 4, uiDensity = 'default', filterChipFontSize, filterChipMinHeight, filterLabelFontSize, }) => {
|
|
459
|
+
const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, maxVisibleFilters = 4, uiDensity = 'default', filterChipFontSize, filterChipMinHeight, filterLabelFontSize, globalFilter, onGlobalFilterChange, globalSearchPlaceholder, gridId, }) => {
|
|
331
460
|
const chipStyle = {
|
|
332
461
|
fontSize: filterChipFontSize ?? (uiDensity === 'compact' ? 11 : 12),
|
|
333
462
|
minHeight: filterChipMinHeight ?? (uiDensity === 'compact' ? 26 : 30),
|
|
@@ -356,7 +485,8 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
|
|
|
356
485
|
else if (def?.type === 'text' ||
|
|
357
486
|
def?.type === 'dropdown' ||
|
|
358
487
|
def?.type === 'searchableDropdown' ||
|
|
359
|
-
def?.type === 'multiSelect'
|
|
488
|
+
def?.type === 'multiSelect' ||
|
|
489
|
+
def?.type === 'monthPicker') {
|
|
360
490
|
onFilterValuesChange({ ...filterValues, [def.filterKey]: '' });
|
|
361
491
|
}
|
|
362
492
|
else if (def?.type === 'date') {
|
|
@@ -377,46 +507,49 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
|
|
|
377
507
|
return (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 0.75 }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.4, mr: 0.25 }, children: [jsx(FilterListIcon, { sx: { fontSize: labelFontSize + 3, color: 'text.disabled' } }), jsx(Typography, { sx: { fontSize: labelFontSize }, color: "text.secondary", fontWeight: 500, noWrap: true, children: "Filter by:" })] }), visibleFilterIds.map((id) => {
|
|
378
508
|
const def = serverFilterDefs.find((d) => d.id === id);
|
|
379
509
|
if (def?.type === 'dateRange') {
|
|
380
|
-
return (jsx(DateRangeChip, { label: def.label, fromDate: filterValues[def.fromKey] ?? '', toDate: filterValues[def.toKey] ?? '', onApply: (from, to) => onFilterValuesChange({ ...filterValues, [def.fromKey]: from, [def.toKey]: to }), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
|
|
510
|
+
return (jsx(DateRangeChip, { label: def.label, fromDate: filterValues[def.fromKey] ?? '', toDate: filterValues[def.toKey] ?? '', onApply: (from, to) => onFilterValuesChange({ ...filterValues, [def.fromKey]: from, [def.toKey]: to }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
381
511
|
}
|
|
382
512
|
if (def?.type === 'text') {
|
|
383
|
-
return (jsx(TextFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder, chipStyle: chipStyle }, id));
|
|
513
|
+
return (jsx(TextFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder, chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
384
514
|
}
|
|
385
515
|
if (def?.type === 'dropdown') {
|
|
386
|
-
return (jsx(DropdownFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
|
|
516
|
+
return (jsx(DropdownFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
387
517
|
}
|
|
388
518
|
if (def?.type === 'searchableDropdown') {
|
|
389
|
-
return (jsx(SearchableSelectChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder ?? 'Search…', chipStyle: chipStyle }, id));
|
|
519
|
+
return (jsx(SearchableSelectChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder ?? 'Search…', chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
390
520
|
}
|
|
391
521
|
if (def?.type === 'multiSelect') {
|
|
392
|
-
// filterValues[key] stores comma-separated values: "Pending,Shipped"
|
|
393
522
|
const selected = (filterValues[def.filterKey] ?? '')
|
|
394
523
|
.split(',')
|
|
395
524
|
.filter(Boolean);
|
|
396
525
|
return (jsx(MultiSelectFilterChip, { label: def.label, selectedValues: selected, options: def.options, onApply: (vals) => onFilterValuesChange({
|
|
397
526
|
...filterValues,
|
|
398
527
|
[def.filterKey]: vals.join(','),
|
|
399
|
-
}), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
|
|
528
|
+
}), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
529
|
+
}
|
|
530
|
+
if (def?.type === 'monthPicker') {
|
|
531
|
+
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));
|
|
400
532
|
}
|
|
401
533
|
if (def?.type === 'date') {
|
|
402
|
-
return (jsx(DatePickerChip, { label: def.label, value: getColValue(def.columnKey), onApply: (v) => applyColFilter(def.columnKey, v), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
|
|
534
|
+
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));
|
|
403
535
|
}
|
|
404
536
|
// Column filter chip — render the variant that matches the column's filterType
|
|
405
537
|
const col = availableColumns.find((c) => c.id === id);
|
|
406
538
|
if (!col)
|
|
407
539
|
return null;
|
|
408
540
|
if (col.filterType === 'date') {
|
|
409
|
-
return (jsx(DatePickerChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
|
|
541
|
+
return (jsx(DatePickerChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
410
542
|
}
|
|
411
543
|
if (col.filterType === 'select' || col.filterType === 'searchable-select') {
|
|
412
|
-
return (jsx(SearchableSelectChip, { label: col.label, value: getColValue(id), options: col.filterOptions ?? [], onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
|
|
544
|
+
return (jsx(SearchableSelectChip, { label: col.label, value: getColValue(id), options: col.filterOptions ?? [], onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
413
545
|
}
|
|
414
546
|
if (col.filterType === 'multi-select') {
|
|
415
|
-
return (jsx(MultiSelectFilterChip, { label: col.label, selectedValues: getColValues(id), options: col.filterOptions ?? [], onApply: (vals) => applyColMultiFilter(id, vals), onRemove: () => removeFilter(id), chipStyle: chipStyle }, id));
|
|
547
|
+
return (jsx(MultiSelectFilterChip, { label: col.label, selectedValues: getColValues(id), options: col.filterOptions ?? [], onApply: (vals) => applyColMultiFilter(id, vals), onRemove: () => removeFilter(id), chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
416
548
|
}
|
|
417
549
|
// Default: free-text input
|
|
418
|
-
return (jsx(TextFilterChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), placeholder: `Filter by ${col.label}`, chipStyle: chipStyle }, id));
|
|
419
|
-
}), jsx(ManageFiltersButton, { serverFilterDefs: serverFilterDefs, visibleIds: visibleFilterIds, availableColumns: availableColumns, onToggle: onToggleFilter, maxVisible: maxVisibleFilters, chipStyle: chipStyle })] }));
|
|
550
|
+
return (jsx(TextFilterChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), placeholder: `Filter by ${col.label}`, chipStyle: chipStyle, baseId: gridId ? `${gridId}_${id}` : undefined }, id));
|
|
551
|
+
}), jsx(ManageFiltersButton, { serverFilterDefs: serverFilterDefs, visibleIds: visibleFilterIds, availableColumns: availableColumns, onToggle: onToggleFilter, maxVisible: maxVisibleFilters, chipStyle: chipStyle, gridId: gridId }), onGlobalFilterChange && (jsx(GlobalSearchInput, { value: globalFilter ?? '', onChange: onGlobalFilterChange, chipStyle: chipStyle, placeholder: globalSearchPlaceholder }))] }));
|
|
420
552
|
};
|
|
553
|
+
var DynamicFilterBar$1 = memo(DynamicFilterBar);
|
|
421
554
|
|
|
422
|
-
export { DynamicFilterBar as default };
|
|
555
|
+
export { DynamicFilterBar$1 as default };
|
|
@@ -55,8 +55,14 @@ export interface GenericPaginatedGridProps<TData extends object> {
|
|
|
55
55
|
defaultVisibleFilterIds?: string[];
|
|
56
56
|
/** Number of rows fetched per API call. Default: 10 000. */
|
|
57
57
|
batchSize?: number;
|
|
58
|
-
/**
|
|
58
|
+
/** Excel (.xlsx) file name prefix (e.g. "invoice" → "invoice-page1.xlsx"). Default: "export". */
|
|
59
59
|
exportFilename?: string;
|
|
60
|
+
/**
|
|
61
|
+
* ID prefix applied to every interactive element in this grid instance.
|
|
62
|
+
* Useful for automated testing and deep-linking. Defaults to `exportFilename`.
|
|
63
|
+
* Pattern: `{gridId}_refresh_icon`, `{gridId}_export_icon`, `{gridId}_status_chip`, etc.
|
|
64
|
+
*/
|
|
65
|
+
gridId?: string;
|
|
60
66
|
/**
|
|
61
67
|
* Height of the entire grid component.
|
|
62
68
|
* When omitted (default) the grid auto-sizes to fill the remaining viewport
|
|
@@ -91,6 +97,13 @@ export interface GenericPaginatedGridProps<TData extends object> {
|
|
|
91
97
|
uiDensityOptions?: GridUiDensityOptions;
|
|
92
98
|
/** Choices shown in the "Rows per page" footer dropdown. Default: [1 000, 5 000, 10 000]. */
|
|
93
99
|
pageSizeOptions?: number[];
|
|
100
|
+
/**
|
|
101
|
+
* Show the Global Search input expanded by default (instead of requiring a
|
|
102
|
+
* click on the search icon to reveal it). The icon remains in the top-right
|
|
103
|
+
* toolbar actions and can still be used to collapse/expand the input.
|
|
104
|
+
* Default: `false`.
|
|
105
|
+
*/
|
|
106
|
+
showGlobalSearchByDefault?: boolean;
|
|
94
107
|
/**
|
|
95
108
|
* Background colour applied to every header cell.
|
|
96
109
|
* Accepts any valid CSS colour string.
|
|
@@ -185,5 +198,5 @@ export interface GenericPaginatedGridProps<TData extends object> {
|
|
|
185
198
|
*/
|
|
186
199
|
enableExpanding?: boolean;
|
|
187
200
|
}
|
|
188
|
-
declare function GenericPaginatedGrid<TData extends object>({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize, exportFilename, tableHeight, tableMinHeight, tableMaxHeight, autoFillViewport, layoutMode, uiDensity, uiDensityOptions, pageSizeOptions, headerBgColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding, }: GenericPaginatedGridProps<TData>): import("react/jsx-runtime").JSX.Element;
|
|
201
|
+
declare function GenericPaginatedGrid<TData extends object>({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize, exportFilename, gridId, tableHeight, tableMinHeight, tableMaxHeight, autoFillViewport, layoutMode, uiDensity, uiDensityOptions, pageSizeOptions, showGlobalSearchByDefault, headerBgColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding, }: GenericPaginatedGridProps<TData>): import("react/jsx-runtime").JSX.Element;
|
|
189
202
|
export default GenericPaginatedGrid;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import { useMemo, useState, useRef, useLayoutEffect } from 'react';
|
|
3
|
-
import
|
|
2
|
+
import { useMemo, useState, useRef, useLayoutEffect, useCallback } from 'react';
|
|
3
|
+
import * as XLSX from 'xlsx-js-style';
|
|
4
|
+
import { useMaterialReactTable, MRT_ToggleGlobalFilterButton, MRT_ShowHideColumnsButton, MRT_ToggleDensePaddingButton, MRT_ToggleFullScreenButton, MaterialReactTable } from 'material-react-table';
|
|
4
5
|
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
|
5
6
|
import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, CircularProgress } from '@mui/material';
|
|
6
7
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
@@ -52,6 +53,8 @@ const buildBodyGlobalStyles = (bodyFont) => ({
|
|
|
52
53
|
'.tp-gpg-root .MuiTableBody-root .MuiTableCell-root': {
|
|
53
54
|
fontSize: `${bodyFont} !important`,
|
|
54
55
|
lineHeight: 1.35,
|
|
56
|
+
borderRight: '1px solid rgba(224,224,224,1)',
|
|
57
|
+
userSelect: 'text',
|
|
55
58
|
},
|
|
56
59
|
});
|
|
57
60
|
const buildHeaderGlobalStyles = (headerMin, headerMax, headerFont, headerBgColor) => {
|
|
@@ -191,7 +194,7 @@ const OverlayLoader = ({ message }) => (jsxs(Box, { sx: {
|
|
|
191
194
|
bgcolor: 'rgba(255,255,255,0.80)', backdropFilter: 'blur(2px)',
|
|
192
195
|
zIndex: 10, gap: 1.5,
|
|
193
196
|
}, children: [jsx(CircularProgress, { size: 50, thickness: 4 }), jsx(Typography, { variant: "body2", color: "text.secondary", fontWeight: 500, children: message })] }));
|
|
194
|
-
const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoading, canPrev, canNext, onFirst, onPrev, onNext, onLast, onPageSizeChange, pageSizeOptions, density, }) => {
|
|
197
|
+
const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoading, canPrev, canNext, onFirst, onPrev, onNext, onLast, onPageSizeChange, pageSizeOptions, density, gridId, }) => {
|
|
195
198
|
const fs = density.footerFontSize;
|
|
196
199
|
const selectH = density.footerMinHeight - 8;
|
|
197
200
|
const startRow = totalRecords === 0 ? 0 : pageIndex * pageSize + 1;
|
|
@@ -203,7 +206,7 @@ const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoa
|
|
|
203
206
|
bgcolor: 'background.paper', flexWrap: 'wrap',
|
|
204
207
|
minHeight: density.footerMinHeight,
|
|
205
208
|
flexShrink: 0,
|
|
206
|
-
}, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.75 }, children: [jsx(Typography, { sx: { fontSize: fs }, color: "text.secondary", noWrap: true, children: "Rows per page:" }), jsx(Select, { value: pageSize, onChange: (e) => onPageSizeChange(Number(e.target.value)), size: "small", variant: "outlined", sx: {
|
|
209
|
+
}, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.75 }, children: [jsx(Typography, { sx: { fontSize: fs }, color: "text.secondary", noWrap: true, children: "Rows per page:" }), jsx(Select, { value: pageSize, onChange: (e) => onPageSizeChange(Number(e.target.value)), inputProps: { id: `${gridId}_page_size_select` }, size: "small", variant: "outlined", sx: {
|
|
207
210
|
height: selectH, fontSize: fs - 1, minWidth: 72,
|
|
208
211
|
'.MuiSelect-select': { py: '2px !important' },
|
|
209
212
|
'.MuiOutlinedInput-notchedOutline': { borderColor: 'divider' },
|
|
@@ -211,18 +214,19 @@ const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoa
|
|
|
211
214
|
? 'Loading…'
|
|
212
215
|
: totalRecords === 0
|
|
213
216
|
? 'No records'
|
|
214
|
-
: `${startRow.toLocaleString()}–${endRow.toLocaleString()} of ${totalRecords.toLocaleString()}` }), jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.25 }, children: [jsx(Tooltip, { title: "First page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onFirst, disabled: !canPrev, sx: { p: 0.5 }, children: jsx(FirstPageIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Tooltip, { title: "Previous page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onPrev, disabled: !canPrev, sx: { p: 0.5 }, children: jsx(ChevronLeftIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Typography, { fontWeight: 600, sx: {
|
|
217
|
+
: `${startRow.toLocaleString()}–${endRow.toLocaleString()} of ${totalRecords.toLocaleString()}` }), jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.25 }, children: [jsx(Tooltip, { title: "First page", children: jsx("span", { children: jsx(IconButton, { id: `${gridId}_first_page_icon`, size: "small", onClick: onFirst, disabled: !canPrev, sx: { p: 0.5 }, children: jsx(FirstPageIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Tooltip, { title: "Previous page", children: jsx("span", { children: jsx(IconButton, { id: `${gridId}_prev_page_icon`, size: "small", onClick: onPrev, disabled: !canPrev, sx: { p: 0.5 }, children: jsx(ChevronLeftIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Typography, { fontWeight: 600, sx: {
|
|
215
218
|
mx: 0.5,
|
|
216
219
|
minWidth: density.footerMinHeight - 12,
|
|
217
220
|
height: density.footerMinHeight - 12,
|
|
218
221
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
219
222
|
borderRadius: 1, border: '1px solid', borderColor: 'primary.main',
|
|
220
223
|
color: 'primary.main', bgcolor: 'rgba(25,118,210,0.07)', fontSize: fs - 1,
|
|
221
|
-
}, children: pageIndex + 1 }), jsx(Tooltip, { title: "Next page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onNext, disabled: !canNext, sx: { p: 0.5 }, children: jsx(ChevronRightIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Tooltip, { title: "Last page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onLast, disabled: !canNext, sx: { p: 0.5 }, children: jsx(LastPageIcon, { sx: { fontSize: density.footerIconSize } }) }) }) })] })] }));
|
|
224
|
+
}, children: pageIndex + 1 }), jsx(Tooltip, { title: "Next page", children: jsx("span", { children: jsx(IconButton, { id: `${gridId}_next_page_icon`, size: "small", onClick: onNext, disabled: !canNext, sx: { p: 0.5 }, children: jsx(ChevronRightIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Tooltip, { title: "Last page", children: jsx("span", { children: jsx(IconButton, { id: `${gridId}_last_page_icon`, size: "small", onClick: onLast, disabled: !canNext, sx: { p: 0.5 }, children: jsx(LastPageIcon, { sx: { fontSize: density.footerIconSize } }) }) }) })] })] }));
|
|
222
225
|
};
|
|
223
226
|
// ─── GenericPaginatedGrid ─────────────────────────────────────────────────────
|
|
224
|
-
function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize = 10000, exportFilename = 'export', tableHeight, tableMinHeight = '300px', tableMaxHeight, autoFillViewport = true, layoutMode = 'grid', uiDensity = 'default', uiDensityOptions, pageSizeOptions = [1000, 5000, 10000], headerBgColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters = 4, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding = !!renderDetailPanel, }) {
|
|
227
|
+
function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize = 10000, exportFilename = 'export', gridId, tableHeight, tableMinHeight = '300px', tableMaxHeight, autoFillViewport = true, layoutMode = 'grid', uiDensity = 'default', uiDensityOptions, pageSizeOptions = [1000, 5000, 10000], showGlobalSearchByDefault = false, headerBgColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters = 4, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding = !!renderDetailPanel, }) {
|
|
225
228
|
const densityTokens = useMemo(() => ({ ...UI_DENSITY_PRESETS[uiDensity], ...uiDensityOptions }), [uiDensity, uiDensityOptions]);
|
|
229
|
+
const resolvedGridId = gridId ?? exportFilename;
|
|
226
230
|
const resolvedHeaderMinHeight = toCssSize(headerMinHeight, uiDensity === 'compact' ? '34px' : '40px');
|
|
227
231
|
const resolvedHeaderMaxHeight = toCssSize(headerMaxHeight, uiDensity === 'compact' ? '56px' : '72px');
|
|
228
232
|
const resolvedHeaderFontSize = toCssSize(headerFontSize ?? densityTokens.headerFontSize, uiDensity === 'compact' ? '11px' : '12px');
|
|
@@ -241,7 +245,11 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
241
245
|
// 1. explicit defaultVisibleFilterIds prop (override)
|
|
242
246
|
// 2. server filters + column filters that have defaultVisible: true
|
|
243
247
|
// 3. first server filter only (original fallback)
|
|
244
|
-
|
|
248
|
+
//
|
|
249
|
+
// Memoized so identity stays stable across re-renders (e.g. while
|
|
250
|
+
// `isFetching` toggles) — used as a `useCallback` dependency below and as
|
|
251
|
+
// the reset target for "Clear All".
|
|
252
|
+
const defaultVisible = useMemo(() => defaultVisibleFilterIds
|
|
245
253
|
?? (() => {
|
|
246
254
|
// Server-side chips with defaultVisible: true
|
|
247
255
|
const markedServer = serverFilterDefs
|
|
@@ -266,7 +274,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
266
274
|
return marked.length > 0
|
|
267
275
|
? marked
|
|
268
276
|
: serverFilterDefs.length > 0 ? [serverFilterDefs[0].id] : [];
|
|
269
|
-
})();
|
|
277
|
+
})(), [defaultVisibleFilterIds, serverFilterDefs, columns]);
|
|
270
278
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
271
279
|
const [filterValues, setFilterValues] = useState(defaultFilters);
|
|
272
280
|
const [visibleFilterIds, setVisibleFilterIds] = useState(defaultVisible);
|
|
@@ -382,40 +390,64 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
382
390
|
}));
|
|
383
391
|
}, [columns, serverFilterDefs]);
|
|
384
392
|
// ── Handlers ───────────────────────────────────────────────────────────────
|
|
385
|
-
|
|
393
|
+
// Wrapped in useCallback so stable references can be passed down to the
|
|
394
|
+
// memoized DynamicFilterBar/chip components without forcing re-renders on
|
|
395
|
+
// every parent render (e.g. while `isFetching` toggles).
|
|
396
|
+
const handleFilterValuesChange = useCallback((newValues) => {
|
|
386
397
|
setFilterValues(newValues);
|
|
387
398
|
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
|
388
|
-
};
|
|
389
|
-
const handleToggleFilter = (id) => setVisibleFilterIds((prev) => {
|
|
399
|
+
}, []);
|
|
400
|
+
const handleToggleFilter = useCallback((id) => setVisibleFilterIds((prev) => {
|
|
390
401
|
if (prev.includes(id))
|
|
391
402
|
return prev.filter((v) => v !== id); // always allow remove
|
|
392
403
|
if (prev.length >= maxVisibleFilters)
|
|
393
404
|
return prev; // cap reached — ignore add
|
|
394
405
|
return [...prev, id];
|
|
395
|
-
});
|
|
396
|
-
const handleClearAll = () => {
|
|
406
|
+
}), [maxVisibleFilters]);
|
|
407
|
+
const handleClearAll = useCallback(() => {
|
|
397
408
|
setFilterValues(defaultFilters);
|
|
398
409
|
setColumnFilters([]);
|
|
399
410
|
setVisibleFilterIds(defaultVisible);
|
|
411
|
+
setGlobalFilter('');
|
|
400
412
|
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
|
401
|
-
};
|
|
402
|
-
const handleExport = () => {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
}, [defaultFilters, defaultVisible]);
|
|
414
|
+
const handleExport = useCallback(() => {
|
|
415
|
+
const dataCols = columns.filter((c) => 'accessorKey' in c && c.accessorKey);
|
|
416
|
+
// xlsx-js-style requires 8-char ARGB (AA + RRGGBB). Prepend full-opacity "FF".
|
|
417
|
+
const headerArgb = headerBgColor?.startsWith('#')
|
|
418
|
+
? 'FF' + headerBgColor.slice(1).toUpperCase().padEnd(6, '0')
|
|
419
|
+
: 'FF1565C0';
|
|
420
|
+
const headerRow = dataCols.map((c) => c.header ?? '');
|
|
421
|
+
const dataRows = rows.map((row) => dataCols.map((c) => String(row[c.accessorKey] ?? '')));
|
|
422
|
+
const ws = XLSX.utils.aoa_to_sheet([headerRow, ...dataRows]);
|
|
423
|
+
// Style header row
|
|
424
|
+
dataCols.forEach((_, colIdx) => {
|
|
425
|
+
const cellRef = XLSX.utils.encode_cell({ r: 0, c: colIdx });
|
|
426
|
+
if (ws[cellRef]) {
|
|
427
|
+
ws[cellRef].s = {
|
|
428
|
+
fill: { patternType: 'solid', fgColor: { rgb: headerArgb } },
|
|
429
|
+
font: { bold: true, color: { rgb: 'FFFFFFFF' }, sz: 10 },
|
|
430
|
+
alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
|
|
431
|
+
border: {
|
|
432
|
+
bottom: { style: 'thin', color: { rgb: 'FFCCCCCC' } },
|
|
433
|
+
right: { style: 'thin', color: { rgb: 'FFCCCCCC' } },
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
// Auto-fit column widths based on header + data content length
|
|
439
|
+
ws['!cols'] = dataCols.map((c) => {
|
|
440
|
+
const headerLen = (c.header ?? '').length;
|
|
441
|
+
const maxDataLen = rows.reduce((max, row) => {
|
|
442
|
+
const val = String(row[c.accessorKey] ?? '');
|
|
443
|
+
return Math.max(max, val.length);
|
|
444
|
+
}, 0);
|
|
445
|
+
return { wch: Math.max(headerLen, maxDataLen, 8) + 2 };
|
|
446
|
+
});
|
|
447
|
+
const wb = XLSX.utils.book_new();
|
|
448
|
+
XLSX.utils.book_append_sheet(wb, ws, 'Data');
|
|
449
|
+
XLSX.writeFile(wb, `${exportFilename}-page${pagination.pageIndex + 1}.xlsx`);
|
|
450
|
+
}, [columns, rows, exportFilename, pagination.pageIndex, headerBgColor]);
|
|
419
451
|
// ── Table instance ─────────────────────────────────────────────────────────
|
|
420
452
|
const table = useMaterialReactTable({
|
|
421
453
|
columns: processedColumns,
|
|
@@ -484,7 +516,15 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
484
516
|
columnResizeMode: 'onChange',
|
|
485
517
|
enableExpanding,
|
|
486
518
|
renderDetailPanel,
|
|
487
|
-
|
|
519
|
+
// Note: `enableFilters` is the master switch for column AND global
|
|
520
|
+
// filtering. Keep it enabled so the Global Search box (which relies on
|
|
521
|
+
// `getCanGlobalFilter()`) actually filters rows. MRT's native per-column
|
|
522
|
+
// filter inputs are hidden separately via `enableColumnFilters: false` —
|
|
523
|
+
// column-level filtering still works because DynamicFilterBar's chips
|
|
524
|
+
// operate on `columnFilters` state directly (column.getFilterFn() is
|
|
525
|
+
// used regardless of this flag).
|
|
526
|
+
enableFilters: true,
|
|
527
|
+
enableColumnFilters: false,
|
|
488
528
|
enableGlobalFilter: true,
|
|
489
529
|
enableDensityToggle: true,
|
|
490
530
|
enableFullScreenToggle: true,
|
|
@@ -493,6 +533,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
493
533
|
enableRowVirtualization: true,
|
|
494
534
|
initialState: {
|
|
495
535
|
density: densityTokens.mrtDensity,
|
|
536
|
+
showGlobalFilter: showGlobalSearchByDefault,
|
|
496
537
|
},
|
|
497
538
|
muiTableProps: {
|
|
498
539
|
sx: { width: '100%' },
|
|
@@ -525,6 +566,27 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
525
566
|
'& .MuiIconButton-root svg': { fontSize: densityTokens.toolbarIconFontSize === 'small' ? 18 : 20 },
|
|
526
567
|
},
|
|
527
568
|
},
|
|
569
|
+
// Align the Global Search field's icon/size with the other toolbar
|
|
570
|
+
// icons (Show/Hide Columns, Density, Fullscreen) so it blends in
|
|
571
|
+
// instead of appearing oversized.
|
|
572
|
+
muiSearchTextFieldProps: {
|
|
573
|
+
size: 'small',
|
|
574
|
+
variant: 'outlined',
|
|
575
|
+
sx: {
|
|
576
|
+
minWidth: uiDensity === 'compact' ? 160 : 200,
|
|
577
|
+
'& .MuiOutlinedInput-root': {
|
|
578
|
+
height: densityTokens.filterChipMinHeight,
|
|
579
|
+
fontSize: densityTokens.filterChipFontSize,
|
|
580
|
+
},
|
|
581
|
+
// The leading search icon is rendered as a plain <SearchIcon />
|
|
582
|
+
// direct child of the input (not inside an IconButton), so size it
|
|
583
|
+
// to match the other top-toolbar action icons. The trailing clear
|
|
584
|
+
// (×) icon is inside an IconButton/InputAdornment.
|
|
585
|
+
'& .MuiOutlinedInput-root > .MuiSvgIcon-root, & .MuiInputAdornment-root .MuiSvgIcon-root': {
|
|
586
|
+
fontSize: densityTokens.toolbarIconFontSize === 'small' ? 18 : 20,
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
},
|
|
528
590
|
muiTableHeadProps: {
|
|
529
591
|
sx: {
|
|
530
592
|
position: 'sticky',
|
|
@@ -598,10 +660,15 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
|
|
|
598
660
|
muiToolbarAlertBannerProps: isError
|
|
599
661
|
? { color: 'error', children: 'Failed to load data. Please try again.' }
|
|
600
662
|
: undefined,
|
|
601
|
-
renderTopToolbarCustomActions: () => (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0, pr: 1, gap: 0.5 }, 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, { size: "small", onClick: () => { handleClearAll(); refetch(); }, disabled: isFetching, sx: { p: uiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(RefreshIcon, { sx: { fontSize: uiDensity === 'compact' ? 18 : 20 } }) }) }) }), jsx(Tooltip, { title: "Export batch as
|
|
602
|
-
|
|
663
|
+
renderTopToolbarCustomActions: () => (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0, pr: 1, gap: 0.5 }, 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: uiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(RefreshIcon, { sx: { fontSize: uiDensity === '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: uiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(FileDownloadIcon, { sx: { fontSize: uiDensity === 'compact' ? 18 : 20 } }) }) }) })] }), jsx(Box, { sx: { flex: 1, minWidth: 0 }, children: jsx(DynamicFilterBar, { serverFilterDefs: serverFilterDefs, visibleFilterIds: visibleFilterIds, onToggleFilter: handleToggleFilter, filterValues: filterValues, onFilterValuesChange: handleFilterValuesChange, columnFilters: columnFilters, onColumnFilterChange: setColumnFilters, availableColumns: availableColumns, maxVisibleFilters: maxVisibleFilters, uiDensity: uiDensity, filterChipFontSize: densityTokens.filterChipFontSize, filterChipMinHeight: densityTokens.filterChipMinHeight, filterLabelFontSize: densityTokens.filterLabelFontSize, gridId: resolvedGridId }) })] })),
|
|
664
|
+
// Single global search control: MRT's built-in search icon/input in the
|
|
665
|
+
// top-right toolbar actions. The inline search box next to "Manage
|
|
666
|
+
// Filters" is intentionally not rendered (see renderTopToolbarCustomActions)
|
|
667
|
+
// to avoid showing two search controls.
|
|
668
|
+
renderToolbarInternalActions: ({ table: t }) => (jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [jsx(MRT_ToggleGlobalFilterButton, { table: t, id: `${resolvedGridId}_global_search_icon` }), jsx(MRT_ShowHideColumnsButton, { table: t, id: `${resolvedGridId}_columns_icon` }), jsx(MRT_ToggleDensePaddingButton, { table: t, id: `${resolvedGridId}_density_icon` }), jsx(MRT_ToggleFullScreenButton, { table: t, id: `${resolvedGridId}_fullscreen_icon` })] })),
|
|
669
|
+
renderBottomToolbar: ({ table: t }) => (jsx(PaginationFooter, { pageIndex: t.getState().pagination.pageIndex, pageSize: t.getState().pagination.pageSize, totalRecords: totalRecords, isFetching: isFetching, isLoading: isLoading, canPrev: t.getCanPreviousPage() && !isFetching, canNext: t.getCanNextPage() && !isFetching, onFirst: () => t.firstPage(), onPrev: () => t.previousPage(), onNext: () => t.nextPage(), onLast: () => t.lastPage(), onPageSizeChange: (s) => t.setPageSize(s), pageSizeOptions: pageSizeOptions, density: densityTokens, gridId: resolvedGridId })),
|
|
603
670
|
});
|
|
604
|
-
return (jsxs(Box, { ref: containerRef, className: "tp-gpg-root", sx: {
|
|
671
|
+
return (jsxs(Box, { ref: containerRef, id: resolvedGridId, className: "tp-gpg-root", sx: {
|
|
605
672
|
position: 'relative',
|
|
606
673
|
height: resolvedHeight,
|
|
607
674
|
minHeight: tableMinHeight,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mock data + fetch adapters for Storybook sample grids (no API required).
|
|
3
3
|
*/
|
|
4
|
-
import type { TPColumnDef } from './types';
|
|
4
|
+
import type { TPColumnDef, InvoiceDetail } from './types';
|
|
5
5
|
import type { PagedResult } from './GenericPaginatedGrid';
|
|
6
6
|
import type { ServerFilterDef } from './DynamicFilterBar';
|
|
7
7
|
export interface EmployeeRow {
|
|
@@ -54,6 +54,14 @@ export declare const orderColumns: TPColumnDef<OrderRow>[];
|
|
|
54
54
|
export declare const orderFilterDefs: ServerFilterDef[];
|
|
55
55
|
export declare const orderDefaultFilters: Record<string, string>;
|
|
56
56
|
export declare function fetchOrderPage(filters: Record<string, string>, pageIndex: number, pageSize?: number): Promise<PagedResult<OrderRow>>;
|
|
57
|
+
/**
|
|
58
|
+
* Mock `fetchPage` adapter for `InvoiceGrid` — pure in-memory data, no API
|
|
59
|
+
* calls. Filters the `ALL_INVOICES` array using the same filter keys
|
|
60
|
+
* (`FromDate`, `ToDate`, `Division`, `Customer`, `DC`, `ProductCode`) that the
|
|
61
|
+
* server filter chips already produce, so `InvoiceGrid.tsx` and its Storybook
|
|
62
|
+
* stories work unchanged.
|
|
63
|
+
*/
|
|
64
|
+
export declare function fetchInvoicePage(filters: Record<string, string>, pageIndex: number, pageSize?: number): Promise<PagedResult<InvoiceDetail>>;
|
|
57
65
|
export interface OrderLineItem {
|
|
58
66
|
itemId: number;
|
|
59
67
|
productCode: string;
|
|
@@ -107,20 +107,3 @@ export interface InvoiceDetail {
|
|
|
107
107
|
changeddate: string;
|
|
108
108
|
SHIP_TO_PARTY: string;
|
|
109
109
|
}
|
|
110
|
-
export interface InvoiceRequest {
|
|
111
|
-
FromDate: string;
|
|
112
|
-
ToDate: string;
|
|
113
|
-
Division: string;
|
|
114
|
-
Customer: string;
|
|
115
|
-
DC: string;
|
|
116
|
-
ProductCode: string;
|
|
117
|
-
PageNumber: number;
|
|
118
|
-
}
|
|
119
|
-
export interface InvoiceApiResponse {
|
|
120
|
-
PageNumber: number;
|
|
121
|
-
PageSize: number;
|
|
122
|
-
TotalRecords: number;
|
|
123
|
-
TotalPages: number;
|
|
124
|
-
HasNextPage: boolean;
|
|
125
|
-
Data: InvoiceDetail[];
|
|
126
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as Calendar } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as Calendar } from '../../../../../_virtual/index.esm6.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireDayCell } from '../DayCell/index.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DateRangePicker } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm7.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireDateRange } from '../DateRange/index.esm.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __exports as DefinedRange } from '../../../../../_virtual/index.
|
|
1
|
+
import { __exports as DefinedRange } from '../../../../../_virtual/index.esm5.js';
|
|
2
2
|
import React__default from 'react';
|
|
3
3
|
import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
|
|
4
4
|
import { __require as requireStyles } from '../../styles.esm.js';
|
package/package.json
CHANGED