tp-react-elements 1.3.1 → 1.3.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.esm12.js +2 -2
- package/dist/_virtual/index.esm13.js +2 -2
- package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +59 -0
- package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +269 -0
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.d.ts +40 -0
- package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +222 -0
- package/dist/components/PaginatedGrid/InvoiceGrid.d.ts +2 -0
- package/dist/components/PaginatedGrid/columns.d.ts +3 -0
- package/dist/components/PaginatedGrid/helpers.d.ts +3 -0
- package/dist/components/PaginatedGrid/index.d.ts +4 -0
- package/dist/components/PaginatedGrid/index.esm.js +2 -0
- package/dist/components/PaginatedGrid/invoiceApi.d.ts +2 -0
- package/dist/components/PaginatedGrid/types.d.ts +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/environmentManager.esm.js +22 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/focusManager.esm.js +65 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/notifyManager.esm.js +83 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/onlineManager.esm.js +55 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/query.esm.js +16 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/queryObserver.esm.js +463 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/retryer.esm.js +8 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/subscribable.esm.js +24 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/thenable.esm.js +34 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/timeoutManager.esm.js +66 -0
- package/dist/node_modules/@tanstack/query-core/build/modern/utils.esm.js +115 -0
- package/dist/node_modules/@tanstack/react-query/build/modern/IsRestoringProvider.esm.js +7 -0
- package/dist/node_modules/@tanstack/react-query/build/modern/QueryClientProvider.esm.js +15 -0
- package/dist/node_modules/@tanstack/react-query/build/modern/QueryErrorResetBoundary.esm.js +21 -0
- package/dist/node_modules/@tanstack/react-query/build/modern/errorBoundaryUtils.esm.js +27 -0
- package/dist/node_modules/@tanstack/react-query/build/modern/suspense.esm.js +22 -0
- package/dist/node_modules/@tanstack/react-query/build/modern/useBaseQuery.esm.js +94 -0
- package/dist/node_modules/@tanstack/react-query/build/modern/useQuery.esm.js +8 -0
- package/dist/node_modules/react-date-range/dist/components/DateInput/index.esm.js +1 -1
- package/dist/node_modules/react-date-range/dist/components/Month/index.esm.js +1 -1
- package/package.json +2 -1
|
@@ -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 DateInput = {};
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { DateInput as __exports };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import 'react-date-range/dist/styles.css';
|
|
2
|
+
import 'react-date-range/dist/theme/default.css';
|
|
3
|
+
import type { MRT_ColumnFiltersState } from 'material-react-table';
|
|
4
|
+
/**
|
|
5
|
+
* Describes one server-side filter chip.
|
|
6
|
+
*
|
|
7
|
+
* - `dateRange` — two-date popover. `fromKey` / `toKey` map to `filterValues`.
|
|
8
|
+
* - `text` — single text-input popover. `filterKey` maps to `filterValues`.
|
|
9
|
+
* - `dropdown` — single-select popover with a fixed list of `options`.
|
|
10
|
+
* `filterKey` maps to `filterValues`.
|
|
11
|
+
* - `date` — single date-picker chip. Applied client-side via MRT `columnFilters`.
|
|
12
|
+
*/
|
|
13
|
+
export type ServerFilterDef = {
|
|
14
|
+
type: 'dateRange';
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
fromKey: string;
|
|
18
|
+
toKey: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'text';
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
filterKey: string;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'dropdown';
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
filterKey: string;
|
|
30
|
+
options: {
|
|
31
|
+
label: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}[];
|
|
34
|
+
} | {
|
|
35
|
+
/**
|
|
36
|
+
* Single-date picker chip — applied client-side against the MRT column
|
|
37
|
+
* whose `accessorKey` matches `columnKey`.
|
|
38
|
+
*/
|
|
39
|
+
type: 'date';
|
|
40
|
+
id: string;
|
|
41
|
+
label: string;
|
|
42
|
+
columnKey: string;
|
|
43
|
+
};
|
|
44
|
+
interface AvailableColumn {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
}
|
|
48
|
+
export interface DynamicFilterBarProps {
|
|
49
|
+
serverFilterDefs: ServerFilterDef[];
|
|
50
|
+
visibleFilterIds: string[];
|
|
51
|
+
onToggleFilter: (id: string) => void;
|
|
52
|
+
filterValues: Record<string, string>;
|
|
53
|
+
onFilterValuesChange: (values: Record<string, string>) => void;
|
|
54
|
+
columnFilters: MRT_ColumnFiltersState;
|
|
55
|
+
onColumnFilterChange: (f: MRT_ColumnFiltersState) => void;
|
|
56
|
+
availableColumns: AvailableColumn[];
|
|
57
|
+
}
|
|
58
|
+
declare const DynamicFilterBar: ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, }: DynamicFilterBarProps) => import("react/jsx-runtime").JSX.Element;
|
|
59
|
+
export default DynamicFilterBar;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useForm } from 'react-hook-form';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
|
6
|
+
import { d as distExports } from '../../_virtual/index.esm.js';
|
|
7
|
+
import DatepickerWrapperV2 from '../FormComponents/DatePicker/DatepickerWrapperV2.esm.js';
|
|
8
|
+
import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
|
|
9
|
+
import { Box, Typography, Button, Popover, TextField, Stack, Tooltip, List, ListItemButton, ListItemText, Divider } from '@mui/material';
|
|
10
|
+
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
|
11
|
+
import TuneIcon from '@mui/icons-material/Tune';
|
|
12
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
13
|
+
import FilterListIcon from '@mui/icons-material/FilterList';
|
|
14
|
+
import CheckIcon from '@mui/icons-material/Check';
|
|
15
|
+
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
|
16
|
+
|
|
17
|
+
// Enable strict-format parsing so dayjs can handle the DD/MM/YYYY strings
|
|
18
|
+
// that DatepickerWrapperV2 passes to onChangeFn
|
|
19
|
+
dayjs.extend(customParseFormat);
|
|
20
|
+
// ─── Type pill metadata ───────────────────────────────────────────────────────
|
|
21
|
+
const TYPE_PILL = {
|
|
22
|
+
dateRange: { label: 'Range', color: '#1565c0', bg: '#e3f2fd' },
|
|
23
|
+
date: { label: 'Date', color: '#00695c', bg: '#e0f2f1' },
|
|
24
|
+
text: { label: 'Text', color: '#2e7d32', bg: '#e8f5e9' },
|
|
25
|
+
dropdown: { label: 'Select', color: '#6a1b9a', bg: '#f3e5f5' },
|
|
26
|
+
};
|
|
27
|
+
// ─── Chip shared style ────────────────────────────────────────────────────────
|
|
28
|
+
const chipSx = (active) => ({
|
|
29
|
+
borderRadius: 5,
|
|
30
|
+
textTransform: 'none',
|
|
31
|
+
fontSize: 12,
|
|
32
|
+
fontWeight: active ? 600 : 400,
|
|
33
|
+
borderColor: active ? 'primary.main' : 'divider',
|
|
34
|
+
color: active ? 'primary.main' : 'text.secondary',
|
|
35
|
+
bgcolor: active ? 'rgba(25,118,210,0.08)' : 'transparent',
|
|
36
|
+
whiteSpace: 'nowrap',
|
|
37
|
+
px: 1.5,
|
|
38
|
+
minHeight: 30,
|
|
39
|
+
'&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(25,118,210,0.08)' },
|
|
40
|
+
});
|
|
41
|
+
// ─── Date helper ──────────────────────────────────────────────────────────────
|
|
42
|
+
const fmtDate = (iso) => {
|
|
43
|
+
if (!iso)
|
|
44
|
+
return '';
|
|
45
|
+
const [y, m, d] = iso.split('-');
|
|
46
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
47
|
+
const thisYear = new Date().getFullYear().toString();
|
|
48
|
+
const yearSuffix = y !== thisYear ? ` '${y.slice(2)}` : '';
|
|
49
|
+
return `${Number(d)} ${months[Number(m) - 1]}${yearSuffix}`;
|
|
50
|
+
};
|
|
51
|
+
const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove }) => {
|
|
52
|
+
const [anchor, setAnchor] = useState(null);
|
|
53
|
+
const active = Boolean(fromDate || toDate);
|
|
54
|
+
const chipLabel = active ? `${fmtDate(fromDate)} – ${fmtDate(toDate)}` : label;
|
|
55
|
+
// Convert ISO "YYYY-MM-DD" → Date, using noon to avoid timezone day-shift
|
|
56
|
+
const isoToDate = (iso) => iso ? new Date(iso + 'T12:00:00') : new Date();
|
|
57
|
+
// Build the Range array expected by react-date-range
|
|
58
|
+
const buildRange = () => [{
|
|
59
|
+
startDate: fromDate ? isoToDate(fromDate) : undefined,
|
|
60
|
+
endDate: toDate ? isoToDate(toDate) : undefined,
|
|
61
|
+
key: 'selection',
|
|
62
|
+
}];
|
|
63
|
+
const [selRange, setSelRange] = useState(buildRange);
|
|
64
|
+
// Sync when the parent resets values (e.g. Clear All)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
setSelRange(buildRange());
|
|
67
|
+
}, [fromDate, toDate]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
68
|
+
const openPicker = (e) => {
|
|
69
|
+
setSelRange(buildRange());
|
|
70
|
+
setAnchor(e.currentTarget);
|
|
71
|
+
};
|
|
72
|
+
// Convert a Date to ISO "YYYY-MM-DD" without relying on toISOString (avoids UTC shift)
|
|
73
|
+
const toISO = (d) => {
|
|
74
|
+
const y = d.getFullYear();
|
|
75
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
76
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
77
|
+
return `${y}-${m}-${dd}`;
|
|
78
|
+
};
|
|
79
|
+
const handleRangeChange = (ranges) => {
|
|
80
|
+
const sel = ranges.selection;
|
|
81
|
+
setSelRange([sel]);
|
|
82
|
+
// Auto-apply & close as soon as a complete range is chosen (start ≠ end day)
|
|
83
|
+
if (sel.startDate &&
|
|
84
|
+
sel.endDate &&
|
|
85
|
+
sel.startDate.getTime() !== sel.endDate.getTime()) {
|
|
86
|
+
onApply(toISO(sel.startDate), toISO(sel.endDate));
|
|
87
|
+
setAnchor(null);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return (jsxs(Fragment, { 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), children: 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" }) }))] })] }));
|
|
91
|
+
};
|
|
92
|
+
const TextFilterChip = ({ label, value, onApply, onRemove, placeholder }) => {
|
|
93
|
+
const [anchor, setAnchor] = useState(null);
|
|
94
|
+
const [draft, setDraft] = useState('');
|
|
95
|
+
const active = Boolean(value);
|
|
96
|
+
return (jsxs(Fragment, { 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), children: active ? `${label}: ${value}` : label }), 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) => {
|
|
97
|
+
if (e.key === 'Enter') {
|
|
98
|
+
onApply(draft);
|
|
99
|
+
setAnchor(null);
|
|
100
|
+
}
|
|
101
|
+
if (e.key === 'Escape')
|
|
102
|
+
setAnchor(null);
|
|
103
|
+
} }), 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" })] })] }) })] }));
|
|
104
|
+
};
|
|
105
|
+
const DatePickerChip = ({ label, value, onApply, onRemove }) => {
|
|
106
|
+
const [anchor, setAnchor] = useState(null);
|
|
107
|
+
const active = Boolean(value);
|
|
108
|
+
const { register, control, getValues, clearErrors, setValue, watch } = useForm({
|
|
109
|
+
defaultValues: { date: value || null },
|
|
110
|
+
});
|
|
111
|
+
// Sync form value whenever the external value prop changes (e.g. on Clear All)
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
setValue('date', value || null);
|
|
114
|
+
}, [value, setValue]);
|
|
115
|
+
const item = {
|
|
116
|
+
name: 'date',
|
|
117
|
+
label,
|
|
118
|
+
inputType: 'datepicker',
|
|
119
|
+
onChangeFn: (val) => {
|
|
120
|
+
// DatepickerWrapperV2 calls onChangeFn with one of:
|
|
121
|
+
// • a dayjs-like object (has .format()) — use it directly
|
|
122
|
+
// • a "DD/MM/YYYY" formatted string — parse with strict mode
|
|
123
|
+
// • null / undefined — clear the filter
|
|
124
|
+
let iso = '';
|
|
125
|
+
if (!val) {
|
|
126
|
+
iso = '';
|
|
127
|
+
}
|
|
128
|
+
else if (typeof val.format === 'function') {
|
|
129
|
+
iso = val.format('YYYY-MM-DD');
|
|
130
|
+
}
|
|
131
|
+
else if (typeof val === 'string' && val.trim()) {
|
|
132
|
+
const parsed = dayjs(val.trim(), 'DD/MM/YYYY', true);
|
|
133
|
+
iso = parsed.isValid() ? parsed.format('YYYY-MM-DD') : '';
|
|
134
|
+
}
|
|
135
|
+
onApply(iso);
|
|
136
|
+
// Auto-close the popover as soon as a date is picked — no need to click "Done"
|
|
137
|
+
setAnchor(null);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return (jsxs(Fragment, { children: [jsx(Button, { variant: "outlined", size: "small", onClick: (e) => setAnchor(e.currentTarget), startIcon: jsx(CalendarTodayIcon, { sx: { fontSize: 13 } }), endIcon: jsx(CloseIcon, { sx: { fontSize: '12px !important' }, onClick: (e) => { e.stopPropagation(); onRemove(); } }), sx: chipSx(active), children: active ? `${label}: ${fmtDate(value)}` : label }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: { sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, p: 2, minWidth: 260 } }, children: [jsx(Typography, { variant: "caption", fontWeight: 600, color: "text.secondary", display: "block", mb: 1.5, children: label }), jsx(DatepickerWrapperV2, { props: { item, register, control, errors: {}, getValues, clearErrors, setValue, watch }, variant: "outlined" }), jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", mt: 1.5, children: [active && (jsx(Button, { size: "small", color: "inherit", onClick: () => { onApply(''); setValue('date', null); setAnchor(null); }, children: "Clear" })), jsx(Button, { size: "small", variant: "contained", disableElevation: true, onClick: () => setAnchor(null), children: "Done" })] })] })] }));
|
|
141
|
+
};
|
|
142
|
+
const DropdownFilterChip = ({ label, value, options, onApply, onRemove }) => {
|
|
143
|
+
const [anchor, setAnchor] = useState(null);
|
|
144
|
+
const active = Boolean(value);
|
|
145
|
+
const selectedLabel = options.find((o) => o.value === value)?.label ?? '';
|
|
146
|
+
const rhf = useForm({ defaultValues: { select: value || '' } });
|
|
147
|
+
const { register, control, getValues, clearErrors, watch } = rhf;
|
|
148
|
+
// Sync form value when external value changes (e.g. Clear All)
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
rhf.setValue('select', value || '');
|
|
151
|
+
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
152
|
+
// Intercept setValue: SingleSelectNonAutoComplete calls props.setValue(name, val)
|
|
153
|
+
// on every selection. We forward to react-hook-form AND close the popover with
|
|
154
|
+
// the chosen value — but only if the popover is currently open (anchor !== null),
|
|
155
|
+
// to avoid reacting to external syncs from the parent.
|
|
156
|
+
const interceptedSetValue = (name, val, opts) => {
|
|
157
|
+
rhf.setValue(name, val, opts);
|
|
158
|
+
if (anchor !== null) {
|
|
159
|
+
onApply(val ?? '');
|
|
160
|
+
setAnchor(null);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const item = {
|
|
164
|
+
name: 'select',
|
|
165
|
+
label,
|
|
166
|
+
inputType: 'basic-select',
|
|
167
|
+
options: [
|
|
168
|
+
{ label: 'All', value: '' },
|
|
169
|
+
...options.map((o) => ({ label: o.label, value: o.value })),
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
return (jsxs(Fragment, { 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), children: active ? `${label}: ${selectedLabel}` : label }), 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: {
|
|
173
|
+
item,
|
|
174
|
+
register,
|
|
175
|
+
control,
|
|
176
|
+
errors: {},
|
|
177
|
+
getValues,
|
|
178
|
+
clearErrors,
|
|
179
|
+
setValue: interceptedSetValue,
|
|
180
|
+
watch,
|
|
181
|
+
}, variant: "outlined" }) })] }));
|
|
182
|
+
};
|
|
183
|
+
const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, onToggle, }) => {
|
|
184
|
+
const [anchor, setAnchor] = useState(null);
|
|
185
|
+
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: {
|
|
186
|
+
borderRadius: 5, textTransform: 'none', fontSize: 12,
|
|
187
|
+
borderColor: 'divider', color: 'text.secondary',
|
|
188
|
+
px: 1.5, minHeight: 30,
|
|
189
|
+
'&:hover': { borderColor: 'primary.main', color: 'primary.main' },
|
|
190
|
+
}, children: "Manage Filters" }) }), jsx(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: { py: 1 }, children: [serverFilterDefs.length > 0 && (jsxs(Fragment, { children: [jsx(Typography, { variant: "caption", fontWeight: 700, color: "text.disabled", sx: { px: 2, pb: 0.5, display: 'block', textTransform: 'uppercase', letterSpacing: 0.5 }, children: "Server Filters" }), jsx(List, { dense: true, disablePadding: true, children: serverFilterDefs.map((def) => {
|
|
191
|
+
const selected = visibleIds.includes(def.id);
|
|
192
|
+
const pill = TYPE_PILL[def.type];
|
|
193
|
+
return (jsxs(ListItemButton, { onClick: () => onToggle(def.id), sx: { px: 2, py: 0.5 }, children: [jsx(Box, { component: "span", sx: {
|
|
194
|
+
mr: 1.25, flexShrink: 0,
|
|
195
|
+
fontSize: 10, fontWeight: 700,
|
|
196
|
+
px: 0.75, py: '2px',
|
|
197
|
+
borderRadius: '4px',
|
|
198
|
+
bgcolor: pill.bg,
|
|
199
|
+
color: pill.color,
|
|
200
|
+
textTransform: 'uppercase',
|
|
201
|
+
letterSpacing: 0.4,
|
|
202
|
+
lineHeight: 1.6,
|
|
203
|
+
}, children: pill.label }), jsx(ListItemText, { primary: def.label, primaryTypographyProps: {
|
|
204
|
+
fontSize: 13,
|
|
205
|
+
color: selected ? 'primary.main' : 'text.primary',
|
|
206
|
+
} }), selected && jsx(CheckIcon, { sx: { fontSize: 16, color: 'primary.main', flexShrink: 0 } })] }, def.id));
|
|
207
|
+
}) })] })), availableColumns.length > 0 && (jsxs(Fragment, { children: [jsx(Divider, { sx: { my: 1 } }), jsx(Typography, { variant: "caption", fontWeight: 700, color: "text.disabled", sx: { px: 2, pb: 0.5, display: 'block', textTransform: 'uppercase', letterSpacing: 0.5 }, children: "Column Filters" }), jsx(List, { dense: true, disablePadding: true, sx: { maxHeight: 260, overflowY: 'auto' }, children: availableColumns.map((col) => {
|
|
208
|
+
const selected = visibleIds.includes(col.id);
|
|
209
|
+
return (jsxs(ListItemButton, { onClick: () => onToggle(col.id), sx: { px: 2, py: 0.4 }, children: [jsx(Box, { component: "span", sx: {
|
|
210
|
+
mr: 1.25, flexShrink: 0,
|
|
211
|
+
fontSize: 10, fontWeight: 700,
|
|
212
|
+
px: 0.75, py: '2px',
|
|
213
|
+
borderRadius: '4px',
|
|
214
|
+
bgcolor: TYPE_PILL.text.bg,
|
|
215
|
+
color: TYPE_PILL.text.color,
|
|
216
|
+
textTransform: 'uppercase',
|
|
217
|
+
letterSpacing: 0.4,
|
|
218
|
+
lineHeight: 1.6,
|
|
219
|
+
}, children: "Text" }), jsx(ListItemText, { primary: col.label, primaryTypographyProps: {
|
|
220
|
+
fontSize: 13,
|
|
221
|
+
color: selected ? 'primary.main' : 'text.primary',
|
|
222
|
+
} }), selected && jsx(CheckIcon, { sx: { fontSize: 16, color: 'primary.main', flexShrink: 0 } })] }, col.id));
|
|
223
|
+
}) })] }))] }) })] }));
|
|
224
|
+
};
|
|
225
|
+
const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, }) => {
|
|
226
|
+
const getColValue = (id) => columnFilters.find((f) => f.id === id)?.value ?? '';
|
|
227
|
+
const applyColFilter = (id, value) => {
|
|
228
|
+
const rest = columnFilters.filter((f) => f.id !== id);
|
|
229
|
+
onColumnFilterChange(value ? [...rest, { id, value }] : rest);
|
|
230
|
+
};
|
|
231
|
+
const removeFilter = (id) => {
|
|
232
|
+
const def = serverFilterDefs.find((d) => d.id === id);
|
|
233
|
+
if (def?.type === 'dateRange') {
|
|
234
|
+
onFilterValuesChange({ ...filterValues, [def.fromKey]: '', [def.toKey]: '' });
|
|
235
|
+
}
|
|
236
|
+
else if (def?.type === 'text' || def?.type === 'dropdown') {
|
|
237
|
+
onFilterValuesChange({ ...filterValues, [def.filterKey]: '' });
|
|
238
|
+
}
|
|
239
|
+
else if (def?.type === 'date') {
|
|
240
|
+
applyColFilter(def.columnKey, '');
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
applyColFilter(id, '');
|
|
244
|
+
}
|
|
245
|
+
onToggleFilter(id);
|
|
246
|
+
};
|
|
247
|
+
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: 15, color: 'text.disabled' } }), jsx(Typography, { variant: "body2", fontSize: 12, color: "text.secondary", fontWeight: 500, noWrap: true, children: "Filter by:" })] }), visibleFilterIds.map((id) => {
|
|
248
|
+
const def = serverFilterDefs.find((d) => d.id === id);
|
|
249
|
+
if (def?.type === 'dateRange') {
|
|
250
|
+
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) }, id));
|
|
251
|
+
}
|
|
252
|
+
if (def?.type === 'text') {
|
|
253
|
+
return (jsx(TextFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder }, id));
|
|
254
|
+
}
|
|
255
|
+
if (def?.type === 'dropdown') {
|
|
256
|
+
return (jsx(DropdownFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id) }, id));
|
|
257
|
+
}
|
|
258
|
+
if (def?.type === 'date') {
|
|
259
|
+
return (jsx(DatePickerChip, { label: def.label, value: getColValue(def.columnKey), onApply: (v) => applyColFilter(def.columnKey, v), onRemove: () => removeFilter(id) }, id));
|
|
260
|
+
}
|
|
261
|
+
// Column filter chip
|
|
262
|
+
const col = availableColumns.find((c) => c.id === id);
|
|
263
|
+
if (!col)
|
|
264
|
+
return null;
|
|
265
|
+
return (jsx(TextFilterChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), placeholder: `Filter by ${col.label}` }, id));
|
|
266
|
+
}), jsx(ManageFiltersButton, { serverFilterDefs: serverFilterDefs, visibleIds: visibleFilterIds, availableColumns: availableColumns, onToggle: onToggleFilter })] }));
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export { DynamicFilterBar as default };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type MRT_ColumnDef } from 'material-react-table';
|
|
2
|
+
import { type ServerFilterDef } from './DynamicFilterBar';
|
|
3
|
+
export type { ServerFilterDef };
|
|
4
|
+
/** Shape that fetchPage must return. */
|
|
5
|
+
export interface PagedResult<TData> {
|
|
6
|
+
data: TData[];
|
|
7
|
+
totalRecords: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GenericPaginatedGridProps<TData extends object> {
|
|
10
|
+
/** MRT column definitions typed to your data shape. */
|
|
11
|
+
columns: MRT_ColumnDef<TData>[];
|
|
12
|
+
/**
|
|
13
|
+
* Async function called on every page/filter change.
|
|
14
|
+
* Receives the current flat filter map and the 0-based page index.
|
|
15
|
+
*/
|
|
16
|
+
fetchPage: (filters: Record<string, string>, pageIndex: number) => Promise<PagedResult<TData>>;
|
|
17
|
+
/** Base react-query cache key. */
|
|
18
|
+
queryKey: unknown[];
|
|
19
|
+
/** Server-side filter chip definitions. */
|
|
20
|
+
serverFilterDefs: ServerFilterDef[];
|
|
21
|
+
/** Initial key → value map for every server filter. */
|
|
22
|
+
defaultFilters: Record<string, string>;
|
|
23
|
+
/** Which chip IDs are visible on first render. Defaults to first entry. */
|
|
24
|
+
defaultVisibleFilterIds?: string[];
|
|
25
|
+
/** Number of rows fetched per API call. Default: 10 000. */
|
|
26
|
+
batchSize?: number;
|
|
27
|
+
/** CSV file name prefix. Default: "export". */
|
|
28
|
+
exportFilename?: string;
|
|
29
|
+
/** max-height of the scrollable table body. Default: "60vh". */
|
|
30
|
+
tableHeight?: string;
|
|
31
|
+
/** Choices shown in the "Rows per page" footer dropdown. Default: [1 000, 5 000, 10 000]. */
|
|
32
|
+
pageSizeOptions?: number[];
|
|
33
|
+
/**
|
|
34
|
+
* Background colour applied to every header cell.
|
|
35
|
+
* Accepts any valid CSS colour string.
|
|
36
|
+
*/
|
|
37
|
+
headerBgColor?: string;
|
|
38
|
+
}
|
|
39
|
+
declare function GenericPaginatedGrid<TData extends object>({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize, exportFilename, tableHeight, pageSizeOptions, headerBgColor, }: GenericPaginatedGridProps<TData>): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export default GenericPaginatedGrid;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { useMaterialReactTable, MaterialReactTable } from 'material-react-table';
|
|
4
|
+
import { keepPreviousData } from '../../node_modules/@tanstack/query-core/build/modern/utils.esm.js';
|
|
5
|
+
import { useQuery } from '../../node_modules/@tanstack/react-query/build/modern/useQuery.esm.js';
|
|
6
|
+
import { Box, Tooltip, IconButton, Button, GlobalStyles, Typography, Select, MenuItem, CircularProgress } from '@mui/material';
|
|
7
|
+
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
8
|
+
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
|
9
|
+
import FirstPageIcon from '@mui/icons-material/FirstPage';
|
|
10
|
+
import LastPageIcon from '@mui/icons-material/LastPage';
|
|
11
|
+
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
|
12
|
+
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
13
|
+
import DynamicFilterBar from './DynamicFilterBar.esm.js';
|
|
14
|
+
|
|
15
|
+
// ─── Internal: overlay loader ─────────────────────────────────────────────────
|
|
16
|
+
const OverlayLoader = ({ message }) => (jsxs(Box, { sx: {
|
|
17
|
+
position: 'absolute', inset: 0,
|
|
18
|
+
display: 'flex', flexDirection: 'column',
|
|
19
|
+
alignItems: 'center', justifyContent: 'center',
|
|
20
|
+
bgcolor: 'rgba(255,255,255,0.80)', backdropFilter: 'blur(2px)',
|
|
21
|
+
zIndex: 10, gap: 1.5,
|
|
22
|
+
}, children: [jsx(CircularProgress, { size: 50, thickness: 4 }), jsx(Typography, { variant: "body2", color: "text.secondary", fontWeight: 500, children: message })] }));
|
|
23
|
+
const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoading, canPrev, canNext, onFirst, onPrev, onNext, onLast, onPageSizeChange, pageSizeOptions, }) => {
|
|
24
|
+
const startRow = totalRecords === 0 ? 0 : pageIndex * pageSize + 1;
|
|
25
|
+
const endRow = Math.min((pageIndex + 1) * pageSize, totalRecords);
|
|
26
|
+
return (jsxs(Box, { sx: {
|
|
27
|
+
display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
|
|
28
|
+
gap: { xs: 1.5, md: 3 }, px: 2, py: 0.75,
|
|
29
|
+
borderTop: '1px solid', borderColor: 'divider',
|
|
30
|
+
bgcolor: 'background.paper', flexWrap: 'wrap', minHeight: 48,
|
|
31
|
+
}, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [jsx(Typography, { variant: "body2", 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: {
|
|
32
|
+
height: 28, fontSize: 13, minWidth: 80,
|
|
33
|
+
'.MuiSelect-select': { py: '3px !important' },
|
|
34
|
+
'.MuiOutlinedInput-notchedOutline': { borderColor: 'divider' },
|
|
35
|
+
}, children: pageSizeOptions.map((opt) => (jsx(MenuItem, { value: opt, sx: { fontSize: 13 }, children: opt.toLocaleString() }, opt))) })] }), jsx(Typography, { variant: "body2", color: "text.secondary", noWrap: true, children: isLoading || (isFetching && totalRecords === 0)
|
|
36
|
+
? 'Loading…'
|
|
37
|
+
: totalRecords === 0
|
|
38
|
+
? 'No records'
|
|
39
|
+
: `${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, children: jsx(FirstPageIcon, { sx: { fontSize: 20 } }) }) }) }), jsx(Tooltip, { title: "Previous page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onPrev, disabled: !canPrev, children: jsx(ChevronLeftIcon, { sx: { fontSize: 20 } }) }) }) }), jsx(Typography, { variant: "body2", fontWeight: 600, sx: {
|
|
40
|
+
mx: 0.5, minWidth: 28, height: 28,
|
|
41
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
42
|
+
borderRadius: 1, border: '1px solid', borderColor: 'primary.main',
|
|
43
|
+
color: 'primary.main', bgcolor: 'rgba(25,118,210,0.07)', fontSize: 13,
|
|
44
|
+
}, children: pageIndex + 1 }), jsx(Tooltip, { title: "Next page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onNext, disabled: !canNext, children: jsx(ChevronRightIcon, { sx: { fontSize: 20 } }) }) }) }), jsx(Tooltip, { title: "Last page", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: onLast, disabled: !canNext, children: jsx(LastPageIcon, { sx: { fontSize: 20 } }) }) }) })] })] }));
|
|
45
|
+
};
|
|
46
|
+
// ─── GenericPaginatedGrid ─────────────────────────────────────────────────────
|
|
47
|
+
function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize = 10000, exportFilename = 'export', tableHeight = '60vh', pageSizeOptions = [1000, 5000, 10000], headerBgColor, }) {
|
|
48
|
+
const defaultVisible = defaultVisibleFilterIds
|
|
49
|
+
?? (serverFilterDefs.length > 0 ? [serverFilterDefs[0].id] : []);
|
|
50
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
51
|
+
const [filterValues, setFilterValues] = useState(defaultFilters);
|
|
52
|
+
const [visibleFilterIds, setVisibleFilterIds] = useState(defaultVisible);
|
|
53
|
+
const [pagination, setPagination] = useState({
|
|
54
|
+
pageIndex: 0,
|
|
55
|
+
pageSize: batchSize,
|
|
56
|
+
});
|
|
57
|
+
const [sorting, setSorting] = useState([]);
|
|
58
|
+
const [columnFilters, setColumnFilters] = useState([]);
|
|
59
|
+
const [globalFilter, setGlobalFilter] = useState('');
|
|
60
|
+
const [columnPinning, setColumnPinning] = useState({});
|
|
61
|
+
// ── Data fetching ──────────────────────────────────────────────────────────
|
|
62
|
+
const { data, isLoading, isError, isFetching, refetch } = useQuery({
|
|
63
|
+
queryKey: [...queryKey, filterValues, pagination.pageIndex],
|
|
64
|
+
queryFn: () => fetchPage(filterValues, pagination.pageIndex),
|
|
65
|
+
placeholderData: keepPreviousData,
|
|
66
|
+
staleTime: 30000,
|
|
67
|
+
});
|
|
68
|
+
const rows = data?.data ?? [];
|
|
69
|
+
const totalRecords = data?.totalRecords ?? 0;
|
|
70
|
+
// ── Derive column list for filter chip picker ──────────────────────────────
|
|
71
|
+
const availableColumns = useMemo(() => {
|
|
72
|
+
const serverManagedColKeys = new Set(serverFilterDefs
|
|
73
|
+
.filter((d) => d.type === 'date')
|
|
74
|
+
.map((d) => d.columnKey));
|
|
75
|
+
return columns
|
|
76
|
+
.filter((c) => 'accessorKey' in c && c.accessorKey && !serverManagedColKeys.has(c.accessorKey))
|
|
77
|
+
.map((c) => ({ id: c.accessorKey, label: c.header }));
|
|
78
|
+
}, [columns, serverFilterDefs]);
|
|
79
|
+
// ── Handlers ───────────────────────────────────────────────────────────────
|
|
80
|
+
const handleFilterValuesChange = (newValues) => {
|
|
81
|
+
setFilterValues(newValues);
|
|
82
|
+
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
|
83
|
+
};
|
|
84
|
+
const handleToggleFilter = (id) => setVisibleFilterIds((prev) => prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id]);
|
|
85
|
+
const handleClearAll = () => {
|
|
86
|
+
setFilterValues(defaultFilters);
|
|
87
|
+
setColumnFilters([]);
|
|
88
|
+
setVisibleFilterIds(defaultVisible);
|
|
89
|
+
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
|
90
|
+
};
|
|
91
|
+
const handleExport = () => {
|
|
92
|
+
const headers = columns.map((c) => c.header).join(',');
|
|
93
|
+
const csvRows = rows.map((row) => columns
|
|
94
|
+
.map((c) => {
|
|
95
|
+
const key = c.accessorKey ?? '';
|
|
96
|
+
const val = row[key] ?? '';
|
|
97
|
+
return `"${String(val).replace(/"/g, '""')}"`;
|
|
98
|
+
})
|
|
99
|
+
.join(','));
|
|
100
|
+
const blob = new Blob([[headers, ...csvRows].join('\n')], { type: 'text/csv' });
|
|
101
|
+
const url = URL.createObjectURL(blob);
|
|
102
|
+
const a = document.createElement('a');
|
|
103
|
+
a.href = url;
|
|
104
|
+
a.download = `${exportFilename}-page${pagination.pageIndex + 1}.csv`;
|
|
105
|
+
a.click();
|
|
106
|
+
URL.revokeObjectURL(url);
|
|
107
|
+
};
|
|
108
|
+
const hasAnyValue = Object.values(filterValues).some(Boolean) ||
|
|
109
|
+
columnFilters.some((f) => f.value);
|
|
110
|
+
// ── Table instance ─────────────────────────────────────────────────────────
|
|
111
|
+
const table = useMaterialReactTable({
|
|
112
|
+
columns,
|
|
113
|
+
data: rows,
|
|
114
|
+
manualPagination: true,
|
|
115
|
+
rowCount: totalRecords,
|
|
116
|
+
manualSorting: false,
|
|
117
|
+
manualFiltering: false,
|
|
118
|
+
state: {
|
|
119
|
+
pagination,
|
|
120
|
+
sorting,
|
|
121
|
+
columnFilters,
|
|
122
|
+
globalFilter,
|
|
123
|
+
columnPinning,
|
|
124
|
+
showAlertBanner: isError,
|
|
125
|
+
showProgressBars: false,
|
|
126
|
+
},
|
|
127
|
+
onPaginationChange: setPagination,
|
|
128
|
+
onSortingChange: setSorting,
|
|
129
|
+
onColumnFiltersChange: setColumnFilters,
|
|
130
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
131
|
+
onColumnPinningChange: (updater) => {
|
|
132
|
+
setColumnPinning((prev) => {
|
|
133
|
+
const next = typeof updater === 'function' ? updater(prev) : updater;
|
|
134
|
+
return { ...next, right: [] };
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
enableColumnResizing: true,
|
|
138
|
+
enableColumnOrdering: true,
|
|
139
|
+
enableColumnActions: false,
|
|
140
|
+
enableColumnPinning: true,
|
|
141
|
+
columnResizeMode: 'onChange',
|
|
142
|
+
enableFilters: false,
|
|
143
|
+
enableGlobalFilter: true,
|
|
144
|
+
enableDensityToggle: true,
|
|
145
|
+
enableFullScreenToggle: true,
|
|
146
|
+
enableHiding: true,
|
|
147
|
+
enableStickyHeader: true,
|
|
148
|
+
enableRowVirtualization: true,
|
|
149
|
+
muiTableContainerProps: { sx: { maxHeight: tableHeight } },
|
|
150
|
+
muiTopToolbarProps: {
|
|
151
|
+
sx: {
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
flexWrap: 'nowrap',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
muiTableHeadCellProps: ({ column }) => {
|
|
157
|
+
const colProps = column.columnDef.muiTableHeadCellProps;
|
|
158
|
+
const align = colProps && typeof colProps !== 'function'
|
|
159
|
+
? colProps.align
|
|
160
|
+
: undefined;
|
|
161
|
+
const justifyContent = align === 'right' ? 'flex-start'
|
|
162
|
+
: align === 'center' ? 'center'
|
|
163
|
+
: 'flex-end';
|
|
164
|
+
return {
|
|
165
|
+
align: (align ?? 'left'),
|
|
166
|
+
sx: {
|
|
167
|
+
...(headerBgColor ? { backgroundColor: headerBgColor } : {}),
|
|
168
|
+
verticalAlign: 'bottom',
|
|
169
|
+
whiteSpace: 'normal',
|
|
170
|
+
borderRight: '1px solid rgba(224,224,224,1)',
|
|
171
|
+
borderBottom: '2px solid rgba(0,0,0,0.15)',
|
|
172
|
+
'& button[aria-label="Move"]': { display: 'none' },
|
|
173
|
+
'& .MuiTableSortLabel-icon': { display: 'none' },
|
|
174
|
+
'& .Mui-TableHeadCell-ResizeHandle-Wrapper': {
|
|
175
|
+
height: '100%',
|
|
176
|
+
top: 0,
|
|
177
|
+
},
|
|
178
|
+
'& .Mui-TableHeadCell-ResizeHandle-Divider': {
|
|
179
|
+
borderRight: 'none',
|
|
180
|
+
},
|
|
181
|
+
'& .Mui-TableHeadCell-Content': {
|
|
182
|
+
alignItems: 'flex-end',
|
|
183
|
+
justifyContent,
|
|
184
|
+
},
|
|
185
|
+
'& .Mui-TableHeadCell-Content-Labels': {
|
|
186
|
+
flexWrap: 'wrap',
|
|
187
|
+
whiteSpace: 'normal',
|
|
188
|
+
overflowWrap: 'normal',
|
|
189
|
+
lineHeight: 1.3,
|
|
190
|
+
justifyContent,
|
|
191
|
+
},
|
|
192
|
+
'& .Mui-TableHeadCell-Content-Wrapper': {
|
|
193
|
+
whiteSpace: 'normal',
|
|
194
|
+
overflow: 'visible',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
muiTableBodyCellProps: {
|
|
200
|
+
sx: {
|
|
201
|
+
whiteSpace: 'nowrap',
|
|
202
|
+
borderRight: '1px solid rgba(224,224,224,1)',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
muiToolbarAlertBannerProps: isError
|
|
206
|
+
? { color: 'error', children: 'Failed to load data. Please try again.' }
|
|
207
|
+
: undefined,
|
|
208
|
+
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: "Refresh current batch", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: () => refetch(), disabled: isFetching, children: jsx(RefreshIcon, { fontSize: "small" }) }) }) }), jsx(Tooltip, { title: "Export batch as CSV", children: jsx("span", { children: jsx(IconButton, { size: "small", onClick: handleExport, disabled: rows.length === 0 || isFetching, children: jsx(FileDownloadIcon, { fontSize: "small" }) }) }) })] }), 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 }) }), hasAnyValue && (jsx(Button, { size: "small", onClick: handleClearAll, sx: {
|
|
209
|
+
flexShrink: 0,
|
|
210
|
+
textTransform: 'none',
|
|
211
|
+
fontSize: 12,
|
|
212
|
+
color: 'error.main',
|
|
213
|
+
minWidth: 'auto',
|
|
214
|
+
px: 1,
|
|
215
|
+
'&:hover': { bgcolor: 'rgba(211,47,47,0.06)' },
|
|
216
|
+
}, children: "Clear All" }))] })),
|
|
217
|
+
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 })),
|
|
218
|
+
});
|
|
219
|
+
return (jsxs(Box, { sx: { position: 'relative' }, children: [jsx(GlobalStyles, { styles: { 'button[aria-label="Pin to right"]': { display: 'none' } } }), isLoading && jsx(OverlayLoader, { message: "Loading data\u2026" }), !isLoading && isFetching && jsx(OverlayLoader, { message: "Updating data\u2026" }), jsx(MaterialReactTable, { table: table })] }));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { GenericPaginatedGrid as default };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as GenericPaginatedGrid } from './GenericPaginatedGrid';
|
|
2
|
+
export type { GenericPaginatedGridProps, PagedResult } from './GenericPaginatedGrid';
|
|
3
|
+
export { default as DynamicFilterBar } from './DynamicFilterBar';
|
|
4
|
+
export type { ServerFilterDef } from './DynamicFilterBar';
|