tp-react-elements 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/_virtual/index.esm4.js +2 -2
  2. package/dist/_virtual/index.esm5.js +2 -2
  3. package/dist/_virtual/index.esm6.js +2 -2
  4. package/dist/_virtual/index.esm7.js +2 -2
  5. package/dist/components/DateRangePicker/DateRangePicker.d.ts +0 -3
  6. package/dist/components/DateRangePicker/DateRangePicker.esm.js +4 -0
  7. package/dist/components/Form/FormRender.esm.js +4 -0
  8. package/dist/components/FormComponents/Select/SearchableSelect.d.ts +6 -0
  9. package/dist/components/FormComponents/Select/SearchableSelect.esm.js +78 -0
  10. package/dist/components/FormComponents/index.d.ts +1 -0
  11. package/dist/components/FormComponents/index.esm.js +1 -0
  12. package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +85 -0
  13. package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +324 -0
  14. package/dist/components/PaginatedGrid/GenericPaginatedGrid.d.ts +40 -0
  15. package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +240 -0
  16. package/dist/components/PaginatedGrid/InvoiceGrid.d.ts +2 -0
  17. package/dist/components/PaginatedGrid/columns.d.ts +22 -0
  18. package/dist/components/PaginatedGrid/helpers.d.ts +3 -0
  19. package/dist/components/PaginatedGrid/index.d.ts +4 -0
  20. package/dist/components/PaginatedGrid/index.esm.js +2 -0
  21. package/dist/components/PaginatedGrid/invoiceApi.d.ts +2 -0
  22. package/dist/components/PaginatedGrid/types.d.ts +48 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.esm.css +1 -1
  25. package/dist/index.esm.js +2 -0
  26. package/dist/node_modules/react-date-range/dist/components/Calendar/index.esm.js +1 -1
  27. package/dist/node_modules/react-date-range/dist/components/DateRange/index.esm.js +1 -1
  28. package/dist/node_modules/react-date-range/dist/components/DateRangePicker/index.esm.js +1 -1
  29. package/dist/node_modules/react-date-range/dist/components/DefinedRange/index.esm.js +1 -1
  30. package/dist/utils/Interface/FormInterface.d.ts +1 -1
  31. package/dist/utils/rdrStyles.d.ts +12 -0
  32. package/dist/utils/rdrStyles.esm.js +180 -0
  33. package/package.json +6 -1
@@ -1,3 +1,3 @@
1
- var DateRange = {};
1
+ var Calendar = {};
2
2
 
3
- export { DateRange as __exports };
3
+ export { Calendar as __exports };
@@ -1,3 +1,3 @@
1
- var Calendar = {};
1
+ var DateRange = {};
2
2
 
3
- export { Calendar as __exports };
3
+ export { DateRange as __exports };
@@ -1,3 +1,3 @@
1
- var DateRangePicker = {};
1
+ var DefinedRange = {};
2
2
 
3
- export { DateRangePicker as __exports };
3
+ export { DefinedRange as __exports };
@@ -1,3 +1,3 @@
1
- var DefinedRange = {};
1
+ var DateRangePicker = {};
2
2
 
3
- export { DefinedRange as __exports };
3
+ export { DateRangePicker as __exports };
@@ -1,7 +1,4 @@
1
- import 'react-date-range/dist/styles.css';
2
- import 'react-date-range/dist/theme/default.css';
3
1
  import { FormRenderProps, VariantProps } from '../../utils';
4
- import './DateRangePicker.scss';
5
2
  declare const DateRangePickerComponent: ({ item, control, watch, setValue, getValues, clearErrors, errors, register, variant, ...rest }: FormRenderProps & {
6
3
  variant: VariantProps;
7
4
  }) => import("react/jsx-runtime").JSX.Element;
@@ -2,6 +2,7 @@ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { useState, useEffect } from 'react';
3
3
  import { Controller } from 'react-hook-form';
4
4
  import { d as distExports } from '../../_virtual/index.esm.js';
5
+ import { useRdrStyles } from '../../utils/rdrStyles.esm.js';
5
6
  import dayjs from 'dayjs';
6
7
  import { renderLabel } from '../../utils/Constants/FormConstants.esm.js';
7
8
  import FormBottomField from '../FormComponents/FormBottomField/FormBottomField.esm.js';
@@ -11,6 +12,9 @@ import { parse } from '../../node_modules/date-fns/parse.esm.js';
11
12
  import { isValid } from '../../node_modules/date-fns/isValid.esm.js';
12
13
 
13
14
  const DateRangePickerComponent = ({ item, control, watch, setValue, getValues, clearErrors, errors, register, variant, ...rest }) => {
15
+ // Inject react-date-range CSS via JS (not CSS import) to prevent it from being
16
+ // bundled into the library's extracted CSS file and leaking into consumer apps.
17
+ useRdrStyles();
14
18
  // Using a generic type for anchorEl to avoid environment-specific linting issues.
15
19
  // eslint-disable-next-line react/prop-types
16
20
  const [anchorEl, setAnchorEl] = useState(null);
@@ -11,6 +11,7 @@ import PasswordField from '../FormComponents/PasswordField/PasswordField.esm.js'
11
11
  import SingleSelect from '../FormComponents/Select/SingleSelect.esm.js';
12
12
  import MultiSelectAutocomplete from '../FormComponents/Select/MultiSelectAutocomplete.esm.js';
13
13
  import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
14
+ import SearchableSelect from '../FormComponents/Select/SearchableSelect.esm.js';
14
15
  import FormActiveSwitch from './FormActiveSwitch.esm.js';
15
16
  import FormTextFieldWithSelect from '../FormComponents/FormTextFieldWithSelect/FormTextFieldWithSelect.esm.js';
16
17
  import DateRangePickerComponent from '../DateRangePicker/DateRangePicker.esm.js';
@@ -92,6 +93,9 @@ const RenderForm = (props) => {
92
93
  case 'select-v2':
93
94
  case 'basic-select': // new alias
94
95
  return jsx(SingleSelectNonAutoComplete, { props: props, variant: variant });
96
+ // Searchable select — read-only TextField trigger + search-filtered Popover
97
+ case 'searchable-select':
98
+ return jsx(SearchableSelect, { props: props, variant: variant });
95
99
  case 'input-select':
96
100
  return jsx(FormTextFieldWithSelect, { props: props, variant: variant });
97
101
  // Numeric inputs (integer, pincode, phone)
@@ -0,0 +1,6 @@
1
+ import { FormRenderProps, VariantProps } from '../../../utils';
2
+ declare const SearchableSelect: ({ props, variant, }: {
3
+ props: FormRenderProps;
4
+ variant: VariantProps;
5
+ }) => import("react/jsx-runtime").JSX.Element;
6
+ export default SearchableSelect;
@@ -0,0 +1,78 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState } from 'react';
3
+ import { FormControl, Box, TextField, InputAdornment, Popover, List, Typography, ListItemButton, ListItemText } from '@mui/material';
4
+ import SearchIcon from '@mui/icons-material/Search';
5
+ import CheckIcon from '@mui/icons-material/Check';
6
+ import { useWatch } from 'react-hook-form';
7
+ import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
8
+ import 'dayjs';
9
+ import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
10
+
11
+ const SearchableSelect = ({ props, variant, }) => {
12
+ const [anchorEl, setAnchorEl] = useState(null);
13
+ const [search, setSearch] = useState('');
14
+ const value = useWatch({ control: props.control, name: props.item.name });
15
+ const isError = !!props.errors?.[props.item.name];
16
+ const options = props.item.options ?? [];
17
+ // Find the label that corresponds to the currently selected value
18
+ const selectedOption = options.find((o) => String(o.value) === String(value ?? ''));
19
+ const displayValue = selectedOption ? String(selectedOption.label) : '';
20
+ const handleOpen = (e) => {
21
+ if (props.item.disable)
22
+ return;
23
+ setSearch('');
24
+ setAnchorEl(e.currentTarget);
25
+ };
26
+ const handleClose = () => setAnchorEl(null);
27
+ const handleSelect = (val) => {
28
+ props.setValue(props.item.name, val);
29
+ props.item.onChangeFn && props.item.onChangeFn(val);
30
+ props.clearErrors && props.clearErrors(props.item.name);
31
+ handleClose();
32
+ };
33
+ // Case-insensitive substring filter across option labels
34
+ const filtered = search
35
+ ? options.filter((o) => String(o.label).toLowerCase().includes(search.toLowerCase()))
36
+ : options;
37
+ // Label shown inside the MUI TextField (suppressed in 'standard' variant)
38
+ const muiLabel = variant !== 'standard'
39
+ ? `${props.item.label}${props.item.required ? ' *' : ''}`
40
+ : '';
41
+ return (jsxs(FormControl, { fullWidth: true, sx: { position: 'relative' }, disabled: props.item.disable, children: [renderLabel(variant, props), jsx(Box, { onClick: handleOpen, sx: { cursor: props.item.disable ? 'default' : 'pointer' }, children: jsx(TextField, { fullWidth: true, value: displayValue, label: muiLabel, placeholder: props.item.placeholder || 'Select…', error: isError, size: "small", InputProps: {
42
+ readOnly: true,
43
+ endAdornment: (jsx(InputAdornment, { position: "end", children: jsx(SearchIcon, { sx: { fontSize: 16, color: 'text.disabled' } }) })),
44
+ sx: { cursor: 'inherit' },
45
+ }, inputProps: {
46
+ 'aria-labelledby': `${props.item.name}-label`,
47
+ 'aria-describedby': `${props.item.name}-helper ${props.item.name}-error`,
48
+ 'aria-required': props.item.required ? true : undefined,
49
+ style: { cursor: 'inherit' },
50
+ }, sx: {
51
+ '& .MuiFormLabel-root': { top: -10 },
52
+ '& .MuiInputLabel-shrink': { top: 0 },
53
+ } }) }), jsxs(Popover, { open: Boolean(anchorEl), anchorEl: anchorEl, onClose: handleClose, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: {
54
+ sx: {
55
+ mt: 0.5,
56
+ borderRadius: 2,
57
+ boxShadow: 6,
58
+ // Match the width of the trigger field
59
+ width: anchorEl?.offsetWidth ?? 280,
60
+ display: 'flex',
61
+ flexDirection: 'column',
62
+ },
63
+ }, children: [jsx(Box, { sx: { px: 1.5, pt: 1.5, pb: 0.75 }, children: jsx(TextField, { size: "small", fullWidth: true, autoFocus: true, placeholder: "Search\u2026", value: search, onChange: (e) => setSearch(e.target.value), onKeyDown: (e) => {
64
+ if (e.key === 'Escape')
65
+ handleClose();
66
+ }, InputProps: {
67
+ startAdornment: (jsx(InputAdornment, { position: "start", children: jsx(SearchIcon, { sx: { fontSize: 16, color: 'text.disabled' } }) })),
68
+ }, sx: { '& .MuiOutlinedInput-root': { borderRadius: 2, fontSize: 13 } } }) }), jsx(List, { dense: true, disablePadding: true, sx: { maxHeight: 260, overflowY: 'auto', pb: 0.75 }, children: filtered.length === 0 ? (jsx(Box, { sx: { px: 2, py: 1.5 }, children: jsxs(Typography, { variant: "caption", color: "text.disabled", children: ["No options match \"", search, "\""] }) })) : (filtered.map((opt) => {
69
+ const isSelected = String(value ?? '') === String(opt.value);
70
+ return (jsxs(ListItemButton, { onClick: () => handleSelect(opt.value), selected: isSelected, disabled: opt.disable, sx: { px: 2, py: 0.5 }, children: [jsx(ListItemText, { primary: String(opt.label), primaryTypographyProps: {
71
+ fontSize: 13,
72
+ color: isSelected ? 'primary.main' : 'text.primary',
73
+ fontWeight: isSelected ? 600 : 400,
74
+ } }), isSelected && (jsx(CheckIcon, { sx: { fontSize: 15, color: 'primary.main', flexShrink: 0 } }))] }, String(opt.value)));
75
+ })) })] }), jsx(FormBottomField, { ...props })] }, props.item.name));
76
+ };
77
+
78
+ export { SearchableSelect as default };
@@ -8,6 +8,7 @@ export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNon
8
8
  export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi';
9
9
  export { default as MultiSelectV1 } from './Select/MultiSelectv1';
10
10
  export { default as MultiSelectTable } from './Select/MultiSelectTable';
11
+ export { default as SearchableSelect } from './Select/SearchableSelect';
11
12
  export { FormCheckBoxGroup } from '../CheckBoxGroup';
12
13
  export { FormRadioGroup } from '../RadioGroup';
13
14
  export { DatepickerWrapperV2, Monthpickerrender, YearPickerField, } from '../DatePicker';
@@ -9,6 +9,7 @@ export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNon
9
9
  export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi.esm.js';
10
10
  export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
11
11
  export { default as MultiSelectTable } from './Select/MultiSelectTable.esm.js';
12
+ export { default as SearchableSelect } from './Select/SearchableSelect.esm.js';
12
13
  export { default as FormCheckBoxGroup } from './FormCheckBoxGroup/FormCheckBoxGroup.esm.js';
13
14
  export { default as FormRadioGroup } from './FormRadioGroup/FormRadioGroup.esm.js';
14
15
  export { default as DatepickerWrapperV2 } from './DatePicker/DatepickerWrapperV2.esm.js';
@@ -0,0 +1,85 @@
1
+ import type { MRT_ColumnFiltersState } from 'material-react-table';
2
+ /**
3
+ * Describes one server-side filter chip.
4
+ *
5
+ * - `dateRange` — two-date popover. `fromKey` / `toKey` map to `filterValues`.
6
+ * - `text` — single text-input popover. `filterKey` maps to `filterValues`.
7
+ * - `dropdown` — single-select popover with a fixed list of `options`.
8
+ * `filterKey` maps to `filterValues`.
9
+ * - `searchableDropdown` — like `dropdown` but the popover has a search box so users can
10
+ * quickly filter long option lists. Same `filterKey` / `options`
11
+ * shape as `dropdown`.
12
+ * - `date` — single date-picker chip. Applied client-side via MRT `columnFilters`.
13
+ */
14
+ export type ServerFilterDef = {
15
+ type: 'dateRange';
16
+ id: string;
17
+ label: string;
18
+ fromKey: string;
19
+ toKey: string;
20
+ } | {
21
+ type: 'text';
22
+ id: string;
23
+ label: string;
24
+ filterKey: string;
25
+ placeholder?: string;
26
+ } | {
27
+ type: 'dropdown';
28
+ id: string;
29
+ label: string;
30
+ filterKey: string;
31
+ options: {
32
+ label: string;
33
+ value: string;
34
+ }[];
35
+ } | {
36
+ /**
37
+ * Like `dropdown` but adds a search input at the top of the popover so users
38
+ * can filter through long option lists by typing.
39
+ */
40
+ type: 'searchableDropdown';
41
+ id: string;
42
+ label: string;
43
+ filterKey: string;
44
+ options: {
45
+ label: string;
46
+ value: string;
47
+ }[];
48
+ placeholder?: string;
49
+ } | {
50
+ /**
51
+ * Single-date picker chip — applied client-side against the MRT column
52
+ * whose `accessorKey` matches `columnKey`.
53
+ */
54
+ type: 'date';
55
+ id: string;
56
+ label: string;
57
+ columnKey: string;
58
+ };
59
+ interface AvailableColumn {
60
+ id: string;
61
+ label: string;
62
+ /**
63
+ * Filter chip variant used in the "Column Filters" section of Manage Filters.
64
+ * Mirrors the `meta.filterType` declared on the column definition.
65
+ * Defaults to `'text'` when omitted.
66
+ */
67
+ filterType?: 'text' | 'date' | 'searchableDropdown';
68
+ /** Options for the `'searchableDropdown'` chip variant. */
69
+ filterOptions?: {
70
+ label: string;
71
+ value: string;
72
+ }[];
73
+ }
74
+ export interface DynamicFilterBarProps {
75
+ serverFilterDefs: ServerFilterDef[];
76
+ visibleFilterIds: string[];
77
+ onToggleFilter: (id: string) => void;
78
+ filterValues: Record<string, string>;
79
+ onFilterValuesChange: (values: Record<string, string>) => void;
80
+ columnFilters: MRT_ColumnFiltersState;
81
+ onColumnFilterChange: (f: MRT_ColumnFiltersState) => void;
82
+ availableColumns: AvailableColumn[];
83
+ }
84
+ declare const DynamicFilterBar: ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, }: DynamicFilterBarProps) => import("react/jsx-runtime").JSX.Element;
85
+ export default DynamicFilterBar;
@@ -0,0 +1,324 @@
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 { useRdrStyles } from '../../utils/rdrStyles.esm.js';
8
+ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
9
+ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
10
+ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
11
+ import SingleSelectNonAutoComplete from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
12
+ import { Box, Typography, Button, Popover, TextField, Stack, InputAdornment, List, ListItemButton, ListItemText, Tooltip, Divider } from '@mui/material';
13
+ import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
14
+ import TuneIcon from '@mui/icons-material/Tune';
15
+ import CloseIcon from '@mui/icons-material/Close';
16
+ import FilterListIcon from '@mui/icons-material/FilterList';
17
+ import CheckIcon from '@mui/icons-material/Check';
18
+ import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
19
+ import SearchIcon from '@mui/icons-material/Search';
20
+
21
+ // Enable strict-format parsing so dayjs can handle the DD/MM/YYYY strings
22
+ // that DatepickerWrapperV2 passes to onChangeFn
23
+ dayjs.extend(customParseFormat);
24
+ // ─── Type pill metadata ───────────────────────────────────────────────────────
25
+ const TYPE_PILL = {
26
+ dateRange: { label: 'Range', color: '#1565c0', bg: '#e3f2fd' },
27
+ date: { label: 'Date', color: '#00695c', bg: '#e0f2f1' },
28
+ text: { label: 'Text', color: '#2e7d32', bg: '#e8f5e9' },
29
+ dropdown: { label: 'Select', color: '#6a1b9a', bg: '#f3e5f5' },
30
+ searchableDropdown: { label: 'Search', color: '#bf360c', bg: '#fbe9e7' },
31
+ };
32
+ // ─── Chip shared style ────────────────────────────────────────────────────────
33
+ const chipSx = (active) => ({
34
+ borderRadius: 5,
35
+ textTransform: 'none',
36
+ fontSize: 12,
37
+ fontWeight: active ? 600 : 400,
38
+ borderColor: active ? 'primary.main' : 'divider',
39
+ color: active ? 'primary.main' : 'text.secondary',
40
+ bgcolor: active ? 'rgba(25,118,210,0.08)' : 'transparent',
41
+ whiteSpace: 'nowrap',
42
+ px: 1.5,
43
+ minHeight: 30,
44
+ '&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(25,118,210,0.08)' },
45
+ });
46
+ // ─── Date helper ──────────────────────────────────────────────────────────────
47
+ const fmtDate = (iso) => {
48
+ if (!iso)
49
+ return '';
50
+ const [y, m, d] = iso.split('-');
51
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
52
+ const thisYear = new Date().getFullYear().toString();
53
+ const yearSuffix = y !== thisYear ? ` '${y.slice(2)}` : '';
54
+ return `${Number(d)} ${months[Number(m) - 1]}${yearSuffix}`;
55
+ };
56
+ const DateRangeChip = ({ label, fromDate, toDate, onApply, onRemove }) => {
57
+ // Inject react-date-range CSS via JS (not CSS import) to prevent it from being
58
+ // bundled into the library's extracted CSS file and leaking into consumer apps.
59
+ useRdrStyles();
60
+ const [anchor, setAnchor] = useState(null);
61
+ const active = Boolean(fromDate || toDate);
62
+ const chipLabel = active ? `${fmtDate(fromDate)} – ${fmtDate(toDate)}` : label;
63
+ // Convert ISO "YYYY-MM-DD" → Date, using noon to avoid timezone day-shift
64
+ const isoToDate = (iso) => iso ? new Date(iso + 'T12:00:00') : new Date();
65
+ // Build the Range array expected by react-date-range
66
+ const buildRange = () => [{
67
+ startDate: fromDate ? isoToDate(fromDate) : undefined,
68
+ endDate: toDate ? isoToDate(toDate) : undefined,
69
+ key: 'selection',
70
+ }];
71
+ const [selRange, setSelRange] = useState(buildRange);
72
+ // Sync when the parent resets values (e.g. Clear All)
73
+ useEffect(() => {
74
+ setSelRange(buildRange());
75
+ }, [fromDate, toDate]); // eslint-disable-line react-hooks/exhaustive-deps
76
+ const openPicker = (e) => {
77
+ setSelRange(buildRange());
78
+ setAnchor(e.currentTarget);
79
+ };
80
+ // Convert a Date to ISO "YYYY-MM-DD" without relying on toISOString (avoids UTC shift)
81
+ const toISO = (d) => {
82
+ const y = d.getFullYear();
83
+ const m = String(d.getMonth() + 1).padStart(2, '0');
84
+ const dd = String(d.getDate()).padStart(2, '0');
85
+ return `${y}-${m}-${dd}`;
86
+ };
87
+ const handleRangeChange = (ranges) => {
88
+ const sel = ranges.selection;
89
+ setSelRange([sel]);
90
+ // Auto-apply & close as soon as a complete range is chosen (start ≠ end day)
91
+ if (sel.startDate &&
92
+ sel.endDate &&
93
+ sel.startDate.getTime() !== sel.endDate.getTime()) {
94
+ onApply(toISO(sel.startDate), toISO(sel.endDate));
95
+ setAnchor(null);
96
+ }
97
+ };
98
+ 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" }) }))] })] }));
99
+ };
100
+ const TextFilterChip = ({ label, value, onApply, onRemove, placeholder }) => {
101
+ const [anchor, setAnchor] = useState(null);
102
+ const [draft, setDraft] = useState('');
103
+ const active = Boolean(value);
104
+ 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) => {
105
+ if (e.key === 'Enter') {
106
+ onApply(draft);
107
+ setAnchor(null);
108
+ }
109
+ if (e.key === 'Escape')
110
+ setAnchor(null);
111
+ } }), 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" })] })] }) })] }));
112
+ };
113
+ const DatePickerChip = ({ label, value, onApply, onRemove }) => {
114
+ const [open, setOpen] = useState(false);
115
+ const [anchorEl, setAnchorEl] = useState(null);
116
+ const active = Boolean(value);
117
+ // dayjs value expected by MUI X DatePicker (null = no selection)
118
+ const dayjsValue = value ? dayjs(value, 'YYYY-MM-DD') : null;
119
+ const handleChipClick = (e) => {
120
+ setAnchorEl(e.currentTarget);
121
+ setOpen(true);
122
+ };
123
+ const handleChange = (newVal) => {
124
+ if (newVal && dayjs.isDayjs(newVal) && newVal.isValid()) {
125
+ onApply(newVal.format('YYYY-MM-DD'));
126
+ }
127
+ else {
128
+ onApply('');
129
+ }
130
+ setOpen(false);
131
+ };
132
+ return (
133
+ // LocalizationProvider must wrap DatePicker — scoped here so it only
134
+ // loads when the chip is actually used (DatePickerChip is tree-shaken
135
+ // if the 'date' filter type is never included in serverFilterDefs).
136
+ jsxs(LocalizationProvider, { dateAdapter: AdapterDayjs, 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), children: active ? `${label}: ${fmtDate(value)}` : label }), jsx(DatePicker, { value: dayjsValue, open: open, onClose: () => setOpen(false), onChange: handleChange, slotProps: {
137
+ textField: { sx: { display: 'none' }, size: 'small' },
138
+ popper: {
139
+ anchorEl,
140
+ placement: 'bottom-start',
141
+ },
142
+ } })] }));
143
+ };
144
+ const DropdownFilterChip = ({ label, value, options, onApply, onRemove }) => {
145
+ const [anchor, setAnchor] = useState(null);
146
+ const active = Boolean(value);
147
+ const selectedLabel = options.find((o) => o.value === value)?.label ?? '';
148
+ const rhf = useForm({ defaultValues: { select: value || '' } });
149
+ const { register, control, getValues, clearErrors, watch } = rhf;
150
+ // Sync form value when external value changes (e.g. Clear All)
151
+ useEffect(() => {
152
+ rhf.setValue('select', value || '');
153
+ }, [value]); // eslint-disable-line react-hooks/exhaustive-deps
154
+ // Intercept setValue: SingleSelectNonAutoComplete calls props.setValue(name, val)
155
+ // on every selection. We forward to react-hook-form AND close the popover with
156
+ // the chosen value — but only if the popover is currently open (anchor !== null),
157
+ // to avoid reacting to external syncs from the parent.
158
+ const interceptedSetValue = (name, val, opts) => {
159
+ rhf.setValue(name, val, opts);
160
+ if (anchor !== null) {
161
+ onApply(val ?? '');
162
+ setAnchor(null);
163
+ }
164
+ };
165
+ const item = {
166
+ name: 'select',
167
+ label,
168
+ inputType: 'basic-select',
169
+ options: [
170
+ { label: 'All', value: '' },
171
+ ...options.map((o) => ({ label: o.label, value: o.value })),
172
+ ],
173
+ };
174
+ 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: {
175
+ item,
176
+ register,
177
+ control,
178
+ errors: {},
179
+ getValues,
180
+ clearErrors,
181
+ setValue: interceptedSetValue,
182
+ watch,
183
+ }, variant: "outlined" }) })] }));
184
+ };
185
+ const SearchableSelectChip = ({ label, value, options, onApply, onRemove, placeholder = 'Search…', }) => {
186
+ const [anchor, setAnchor] = useState(null);
187
+ const [search, setSearch] = useState('');
188
+ const active = Boolean(value);
189
+ const selectedLabel = options.find((o) => o.value === value)?.label ?? '';
190
+ const open = (e) => {
191
+ setSearch(''); // always start with all options visible
192
+ setAnchor(e.currentTarget);
193
+ };
194
+ const select = (val) => {
195
+ onApply(val);
196
+ setAnchor(null);
197
+ };
198
+ // Case-insensitive filter; always show "All" row on top
199
+ const filtered = search
200
+ ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))
201
+ : options;
202
+ return (jsxs(Fragment, { 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), children: active ? `${label}: ${selectedLabel}` : label }), jsxs(Popover, { open: Boolean(anchor), anchorEl: anchor, onClose: () => setAnchor(null), anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, PaperProps: {
203
+ sx: { mt: 0.75, borderRadius: 2, boxShadow: 6, width: 280, display: 'flex', flexDirection: 'column' },
204
+ }, 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')
205
+ setAnchor(null); }, InputProps: {
206
+ startAdornment: (jsx(InputAdornment, { position: "start", children: jsx(SearchIcon, { sx: { fontSize: 16, color: 'text.disabled' } }) })),
207
+ }, sx: { '& .MuiOutlinedInput-root': { borderRadius: 2, fontSize: 13 } } }) }), jsxs(List, { dense: true, disablePadding: true, sx: { maxHeight: 260, overflowY: 'auto', pb: 0.75 }, children: [!search && (jsxs(ListItemButton, { onClick: () => select(''), selected: !active, sx: { px: 2, py: 0.5 }, children: [jsx(ListItemText, { primary: "All", primaryTypographyProps: {
208
+ fontSize: 13,
209
+ color: !active ? 'primary.main' : 'text.secondary',
210
+ fontStyle: 'italic',
211
+ } }), !active && jsx(CheckIcon, { sx: { fontSize: 15, color: 'primary.main', flexShrink: 0 } })] })), filtered.length === 0 ? (jsx(Box, { sx: { px: 2, py: 1.5 }, children: jsxs(Typography, { variant: "caption", color: "text.disabled", children: ["No options match \"", search, "\""] }) })) : (filtered.map((opt) => {
212
+ const isSelected = value === opt.value;
213
+ return (jsxs(ListItemButton, { onClick: () => select(opt.value), selected: isSelected, sx: { px: 2, py: 0.5 }, children: [jsx(ListItemText, { primary: opt.label, primaryTypographyProps: {
214
+ fontSize: 13,
215
+ color: isSelected ? 'primary.main' : 'text.primary',
216
+ fontWeight: isSelected ? 600 : 400,
217
+ } }), isSelected && (jsx(CheckIcon, { sx: { fontSize: 15, color: 'primary.main', flexShrink: 0 } }))] }, opt.value));
218
+ }))] })] })] }));
219
+ };
220
+ const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, onToggle, }) => {
221
+ const [anchor, setAnchor] = useState(null);
222
+ 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: {
223
+ borderRadius: 5, textTransform: 'none', fontSize: 12,
224
+ borderColor: 'divider', color: 'text.secondary',
225
+ px: 1.5, minHeight: 30,
226
+ '&:hover': { borderColor: 'primary.main', color: 'primary.main' },
227
+ }, 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) => {
228
+ const selected = visibleIds.includes(def.id);
229
+ const pill = TYPE_PILL[def.type];
230
+ return (jsxs(ListItemButton, { onClick: () => onToggle(def.id), sx: { px: 2, py: 0.5 }, children: [jsx(Box, { component: "span", sx: {
231
+ mr: 1.25, flexShrink: 0,
232
+ fontSize: 10, fontWeight: 700,
233
+ px: 0.75, py: '2px',
234
+ borderRadius: '4px',
235
+ bgcolor: pill.bg,
236
+ color: pill.color,
237
+ textTransform: 'uppercase',
238
+ letterSpacing: 0.4,
239
+ lineHeight: 1.6,
240
+ }, children: pill.label }), jsx(ListItemText, { primary: def.label, primaryTypographyProps: {
241
+ fontSize: 13,
242
+ color: selected ? 'primary.main' : 'text.primary',
243
+ } }), selected && jsx(CheckIcon, { sx: { fontSize: 16, color: 'primary.main', flexShrink: 0 } })] }, def.id));
244
+ }) })] })), 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) => {
245
+ const selected = visibleIds.includes(col.id);
246
+ // Pick the type pill based on the column's declared filter type
247
+ const pill = col.filterType === 'date'
248
+ ? TYPE_PILL.date
249
+ : col.filterType === 'searchableDropdown'
250
+ ? TYPE_PILL.searchableDropdown
251
+ : TYPE_PILL.text;
252
+ return (jsxs(ListItemButton, { onClick: () => onToggle(col.id), sx: { px: 2, py: 0.4 }, children: [jsx(Box, { component: "span", sx: {
253
+ mr: 1.25, flexShrink: 0,
254
+ fontSize: 10, fontWeight: 700,
255
+ px: 0.75, py: '2px',
256
+ borderRadius: '4px',
257
+ bgcolor: pill.bg,
258
+ color: pill.color,
259
+ textTransform: 'uppercase',
260
+ letterSpacing: 0.4,
261
+ lineHeight: 1.6,
262
+ }, children: pill.label }), jsx(ListItemText, { primary: col.label, primaryTypographyProps: {
263
+ fontSize: 13,
264
+ color: selected ? 'primary.main' : 'text.primary',
265
+ } }), selected && jsx(CheckIcon, { sx: { fontSize: 16, color: 'primary.main', flexShrink: 0 } })] }, col.id));
266
+ }) })] }))] }) })] }));
267
+ };
268
+ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter, filterValues, onFilterValuesChange, columnFilters, onColumnFilterChange, availableColumns, }) => {
269
+ const getColValue = (id) => columnFilters.find((f) => f.id === id)?.value ?? '';
270
+ const applyColFilter = (id, value) => {
271
+ const rest = columnFilters.filter((f) => f.id !== id);
272
+ onColumnFilterChange(value ? [...rest, { id, value }] : rest);
273
+ };
274
+ const removeFilter = (id) => {
275
+ const def = serverFilterDefs.find((d) => d.id === id);
276
+ if (def?.type === 'dateRange') {
277
+ onFilterValuesChange({ ...filterValues, [def.fromKey]: '', [def.toKey]: '' });
278
+ }
279
+ else if (def?.type === 'text' ||
280
+ def?.type === 'dropdown' ||
281
+ def?.type === 'searchableDropdown') {
282
+ onFilterValuesChange({ ...filterValues, [def.filterKey]: '' });
283
+ }
284
+ else if (def?.type === 'date') {
285
+ applyColFilter(def.columnKey, '');
286
+ }
287
+ else {
288
+ applyColFilter(id, '');
289
+ }
290
+ onToggleFilter(id);
291
+ };
292
+ 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) => {
293
+ const def = serverFilterDefs.find((d) => d.id === id);
294
+ if (def?.type === 'dateRange') {
295
+ 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));
296
+ }
297
+ if (def?.type === 'text') {
298
+ return (jsx(TextFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id), placeholder: def.placeholder }, id));
299
+ }
300
+ if (def?.type === 'dropdown') {
301
+ return (jsx(DropdownFilterChip, { label: def.label, value: filterValues[def.filterKey] ?? '', options: def.options, onApply: (v) => onFilterValuesChange({ ...filterValues, [def.filterKey]: v }), onRemove: () => removeFilter(id) }, id));
302
+ }
303
+ if (def?.type === 'searchableDropdown') {
304
+ 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…' }, id));
305
+ }
306
+ if (def?.type === 'date') {
307
+ return (jsx(DatePickerChip, { label: def.label, value: getColValue(def.columnKey), onApply: (v) => applyColFilter(def.columnKey, v), onRemove: () => removeFilter(id) }, id));
308
+ }
309
+ // Column filter chip — render the variant that matches the column's filterType
310
+ const col = availableColumns.find((c) => c.id === id);
311
+ if (!col)
312
+ return null;
313
+ if (col.filterType === 'date') {
314
+ return (jsx(DatePickerChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id) }, id));
315
+ }
316
+ if (col.filterType === 'searchableDropdown') {
317
+ return (jsx(SearchableSelectChip, { label: col.label, value: getColValue(id), options: col.filterOptions ?? [], onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id) }, id));
318
+ }
319
+ // Default: free-text input
320
+ return (jsx(TextFilterChip, { label: col.label, value: getColValue(id), onApply: (v) => applyColFilter(id, v), onRemove: () => removeFilter(id), placeholder: `Filter by ${col.label}` }, id));
321
+ }), jsx(ManageFiltersButton, { serverFilterDefs: serverFilterDefs, visibleIds: visibleFilterIds, availableColumns: availableColumns, onToggle: onToggleFilter })] }));
322
+ };
323
+
324
+ 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;