tp-react-elements 2.0.4 → 2.0.7

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 (38) hide show
  1. package/dist/_virtual/index.esm11.js +2 -2
  2. package/dist/_virtual/index.esm12.js +2 -2
  3. package/dist/_virtual/index.esm13.js +2 -2
  4. package/dist/_virtual/index.esm14.js +2 -2
  5. package/dist/_virtual/index.esm4.js +2 -2
  6. package/dist/_virtual/index.esm6.js +2 -2
  7. package/dist/_virtual/index.esm7.js +2 -2
  8. package/dist/components/DateRangePicker/DateRangePicker.esm.js +49 -72
  9. package/dist/components/FormComponents/DatePicker/DatepickerWrapperV2.esm.js +10 -15
  10. package/dist/components/FormComponents/DatePicker/DatetimepickerWrapper.esm.js +53 -61
  11. package/dist/components/FormComponents/FileUpload/FormRenderFileUpload.esm.js +54 -68
  12. package/dist/components/FormComponents/FileUpload/FormRenderMultiFileUpload.esm.js +61 -76
  13. package/dist/components/FormComponents/FileUpload/MultiFileWithPreview.esm.js +103 -132
  14. package/dist/components/FormComponents/FormNumberField/FormNumberField.d.ts +0 -38
  15. package/dist/components/FormComponents/FormNumberField/FormNumberField.esm.js +32 -97
  16. package/dist/components/FormComponents/FormNumberField/FormNumberFieldDecimal.esm.js +44 -38
  17. package/dist/components/FormComponents/FormTextField/FormTextField.esm.js +3 -3
  18. package/dist/components/FormComponents/FormTextFieldWithSelect/FormTextFieldWithSelect.esm.js +3 -2
  19. package/dist/components/FormComponents/PasswordField/PasswordField.esm.js +4 -4
  20. package/dist/components/FormComponents/RichTextEditor/RichTextEditor.esm.js +23 -22
  21. package/dist/components/FormComponents/Select/SearchableSelect.esm.js +9 -9
  22. package/dist/components/FormComponents/TimePicker/TimePicker.esm.js +19 -19
  23. package/dist/components/ModalField/ConfirmationDialog.esm.js +3 -3
  24. package/dist/components/ModalField/ModalField.esm.js +5 -12
  25. package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +1 -1
  26. package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +27 -24
  27. package/dist/components/PaginatedGrid/GenericPaginatedGrid.d.ts +36 -2
  28. package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +196 -105
  29. package/dist/components/SelectField/MultiSelectFieldComponent.esm.js +3 -3
  30. package/dist/components/SessionTimeOut/SessionTimeOut.esm.js +33 -34
  31. package/dist/node_modules/react-date-range/dist/components/Calendar/index.esm.js +1 -1
  32. package/dist/node_modules/react-date-range/dist/components/DateInput/index.esm.js +1 -1
  33. package/dist/node_modules/react-date-range/dist/components/DateRange/index.esm.js +1 -1
  34. package/dist/node_modules/react-date-range/dist/components/DateRangePicker/index.esm.js +1 -1
  35. package/dist/node_modules/react-date-range/dist/components/DayCell/index.esm.js +1 -1
  36. package/dist/node_modules/react-date-range/dist/components/InputRangeField/index.esm.js +1 -1
  37. package/dist/node_modules/react-date-range/dist/components/Month/index.esm.js +1 -1
  38. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { memo, useState, useEffect } from 'react';
2
+ import { memo, useState, useEffect, useCallback } from 'react';
3
3
  import { useForm } from 'react-hook-form';
4
4
  import dayjs from 'dayjs';
5
5
  import customParseFormat from 'dayjs/plugin/customParseFormat';
@@ -410,21 +410,21 @@ const GlobalSearchInput = memo(({ value, onChange, chipStyle, placeholder = 'Sea
410
410
  } }));
411
411
  });
412
412
  GlobalSearchInput.displayName = 'GlobalSearchInput';
413
+ // ─── PillBox — module-level so ManageFiltersButton never creates a new type ───
414
+ const PillBox = ({ bg, color, label }) => (jsx(Box, { component: "span", sx: {
415
+ mr: 1.25, flexShrink: 0,
416
+ fontSize: 10, fontWeight: 700,
417
+ px: 0.75, py: '2px',
418
+ borderRadius: '4px',
419
+ bgcolor: bg,
420
+ color,
421
+ textTransform: 'uppercase',
422
+ letterSpacing: 0.4,
423
+ lineHeight: 1.6,
424
+ }, children: label }));
413
425
  const ManageFiltersButton = ({ serverFilterDefs, visibleIds, availableColumns, onToggle, maxVisible = 4, chipStyle, gridId, }) => {
414
426
  const [anchor, setAnchor] = useState(null);
415
427
  const atLimit = visibleIds.length >= maxVisible;
416
- // Shared pill box renderer
417
- const PillBox = ({ bg, color, label }) => (jsx(Box, { component: "span", sx: {
418
- mr: 1.25, flexShrink: 0,
419
- fontSize: 10, fontWeight: 700,
420
- px: 0.75, py: '2px',
421
- borderRadius: '4px',
422
- bgcolor: bg,
423
- color,
424
- textTransform: 'uppercase',
425
- letterSpacing: 0.4,
426
- lineHeight: 1.6,
427
- }, children: label }));
428
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: {
429
429
  borderRadius: 5, textTransform: 'none', fontSize: chipStyle.fontSize,
430
430
  borderColor: 'divider', color: 'text.secondary',
@@ -464,20 +464,24 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
464
464
  const labelFontSize = filterLabelFontSize ?? (uiDensity === 'compact' ? 11 : 12);
465
465
  // ── Single-value column filter helpers ──────────────────────────────────────
466
466
  const getColValue = (id) => columnFilters.find((f) => f.id === id)?.value ?? '';
467
- const applyColFilter = (id, value) => {
468
- const rest = columnFilters.filter((f) => f.id !== id);
469
- onColumnFilterChange(value ? [...rest, { id, value }] : rest);
470
- };
467
+ const applyColFilter = useCallback((id, value) => {
468
+ onColumnFilterChange((prev) => {
469
+ const rest = prev.filter((f) => f.id !== id);
470
+ return value ? [...rest, { id, value }] : rest;
471
+ });
472
+ }, [onColumnFilterChange]);
471
473
  // ── Multi-value column filter helpers ────────────────────────────────────────
472
474
  const getColValues = (id) => {
473
475
  const val = columnFilters.find((f) => f.id === id)?.value;
474
476
  return Array.isArray(val) ? val : [];
475
477
  };
476
- const applyColMultiFilter = (id, values) => {
477
- const rest = columnFilters.filter((f) => f.id !== id);
478
- onColumnFilterChange(values.length > 0 ? [...rest, { id, value: values }] : rest);
479
- };
480
- const removeFilter = (id) => {
478
+ const applyColMultiFilter = useCallback((id, values) => {
479
+ onColumnFilterChange((prev) => {
480
+ const rest = prev.filter((f) => f.id !== id);
481
+ return values.length > 0 ? [...rest, { id, value: values }] : rest;
482
+ });
483
+ }, [onColumnFilterChange]);
484
+ const removeFilter = useCallback((id) => {
481
485
  const def = serverFilterDefs.find((d) => d.id === id);
482
486
  if (def?.type === 'dateRange') {
483
487
  onFilterValuesChange({ ...filterValues, [def.fromKey]: '', [def.toKey]: '' });
@@ -493,7 +497,6 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
493
497
  applyColFilter(def.columnKey, '');
494
498
  }
495
499
  else {
496
- // Column filter — multi-select stores string[], others store string
497
500
  const colDef = availableColumns.find((c) => c.id === id);
498
501
  if (colDef?.filterType === 'multi-select') {
499
502
  applyColMultiFilter(id, []);
@@ -503,7 +506,7 @@ const DynamicFilterBar = ({ serverFilterDefs, visibleFilterIds, onToggleFilter,
503
506
  }
504
507
  }
505
508
  onToggleFilter(id);
506
- };
509
+ }, [filterValues, serverFilterDefs, availableColumns, onFilterValuesChange, onToggleFilter, applyColFilter, applyColMultiFilter]);
507
510
  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) => {
508
511
  const def = serverFilterDefs.find((d) => d.id === id);
509
512
  if (def?.type === 'dateRange') {
@@ -91,10 +91,32 @@ export interface GenericPaginatedGridProps<TData extends object> {
91
91
  * Use `grid-no-grow` only when you need fixed column widths with horizontal scroll.
92
92
  */
93
93
  layoutMode?: 'grid' | 'semantic' | 'grid-no-grow';
94
- /** Compact toolbar, filter chips, and footer. Default: `default`. */
94
+ /**
95
+ * Compact toolbar, filter chips, and footer.
96
+ * When omitted, the grid auto-selects `compact` at the `tablet`/`mobile`
97
+ * responsive tiers and `default` at `desktop`. Pass explicitly to opt out
98
+ * of that auto-switch.
99
+ */
95
100
  uiDensity?: GridUiDensity;
96
101
  /** Override individual sizes from the `uiDensity` preset. */
97
102
  uiDensityOptions?: GridUiDensityOptions;
103
+ /**
104
+ * Viewport-width cutoffs (px) for the three responsive tiers:
105
+ * `desktop` (above `tablet`), `tablet` (between `mobile` and `tablet`),
106
+ * and `mobile` (at/below `mobile`). Each tier gets progressively tighter
107
+ * defaults — density, filter-chip cap, toolbar wrap, footer layout.
108
+ * Default: `{ tablet: 1024, mobile: 640 }`.
109
+ */
110
+ responsiveBreakpoints?: {
111
+ tablet?: number;
112
+ mobile?: number;
113
+ };
114
+ /**
115
+ * Opt out of all automatic responsive-tier adjustments (density, filter
116
+ * cap, toolbar wrap, footer layout) — the grid always behaves as `desktop`.
117
+ * Default: `false`.
118
+ */
119
+ disableResponsive?: boolean;
98
120
  /** Choices shown in the "Rows per page" footer dropdown. Default: [1 000, 5 000, 10 000]. */
99
121
  pageSizeOptions?: number[];
100
122
  /**
@@ -109,6 +131,18 @@ export interface GenericPaginatedGridProps<TData extends object> {
109
131
  * Accepts any valid CSS colour string.
110
132
  */
111
133
  headerBgColor?: string;
134
+ /**
135
+ * Background colour for the Excel export header row.
136
+ * Accepts a hex colour string (`#RRGGBB`).
137
+ * Falls back to `headerBgColor` when omitted, then to the default blue `#1565C0`.
138
+ */
139
+ exportHeaderBgColor?: string;
140
+ /**
141
+ * Text colour for the Excel export header row.
142
+ * Accepts a hex colour string (`#RRGGBB`).
143
+ * Defaults to white (`#FFFFFF`) when omitted.
144
+ */
145
+ exportHeaderTextColor?: string;
112
146
  /**
113
147
  * Minimum height of every header cell (CSS length or px number).
114
148
  * Default: `40px` (`34px` when `uiDensity="compact"`).
@@ -198,5 +232,5 @@ export interface GenericPaginatedGridProps<TData extends object> {
198
232
  */
199
233
  enableExpanding?: boolean;
200
234
  }
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;
235
+ declare function GenericPaginatedGrid<TData extends object>({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize, exportFilename, gridId, tableHeight, tableMinHeight, tableMaxHeight, autoFillViewport, layoutMode, uiDensity, uiDensityOptions, responsiveBreakpoints, disableResponsive, pageSizeOptions, showGlobalSearchByDefault, headerBgColor, exportHeaderBgColor, exportHeaderTextColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding, }: GenericPaginatedGridProps<TData>): import("react/jsx-runtime").JSX.Element;
202
236
  export default GenericPaginatedGrid;
@@ -1,9 +1,8 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
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';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useMemo, useState, useRef, useLayoutEffect, useCallback, useEffect } from 'react';
3
+ import { MRT_ToggleGlobalFilterButton, MRT_ShowHideColumnsButton, MRT_ToggleDensePaddingButton, MRT_ToggleFullScreenButton, useMaterialReactTable, MaterialReactTable } from 'material-react-table';
5
4
  import { useQuery, keepPreviousData } from '@tanstack/react-query';
6
- import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, CircularProgress } from '@mui/material';
5
+ import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, Skeleton, CircularProgress } from '@mui/material';
7
6
  import RefreshIcon from '@mui/icons-material/Refresh';
8
7
  import FileDownloadIcon from '@mui/icons-material/FileDownload';
9
8
  import FirstPageIcon from '@mui/icons-material/FirstPage';
@@ -11,6 +10,7 @@ import LastPageIcon from '@mui/icons-material/LastPage';
11
10
  import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
12
11
  import ChevronRightIcon from '@mui/icons-material/ChevronRight';
13
12
  import DynamicFilterBar from './DynamicFilterBar.esm.js';
13
+ import { getXLSX } from '../../utils/Constants/xlsx-js-style.esm.js';
14
14
 
15
15
  const UI_DENSITY_PRESETS = {
16
16
  default: {
@@ -194,27 +194,60 @@ const OverlayLoader = ({ message }) => (jsxs(Box, { sx: {
194
194
  bgcolor: 'rgba(255,255,255,0.80)', backdropFilter: 'blur(2px)',
195
195
  zIndex: 10, gap: 1.5,
196
196
  }, children: [jsx(CircularProgress, { size: 50, thickness: 4 }), jsx(Typography, { variant: "body2", color: "text.secondary", fontWeight: 500, children: message })] }));
197
- const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoading, canPrev, canNext, onFirst, onPrev, onNext, onLast, onPageSizeChange, pageSizeOptions, density, gridId, }) => {
197
+ // ─── Internal: skeleton loader (initial load — no data on screen yet) ────────
198
+ const SkeletonOverlay = ({ columnCount, rowCount = 10, }) => (jsx(Box, { sx: {
199
+ position: 'absolute', inset: 0,
200
+ display: 'flex', flexDirection: 'column',
201
+ bgcolor: 'background.paper',
202
+ zIndex: 10, p: 1.5, gap: 1,
203
+ overflow: 'hidden',
204
+ }, children: Array.from({ length: rowCount }).map((_, rowIdx) => (jsx(Box, { sx: { display: 'flex', gap: 1.5 }, children: Array.from({ length: Math.max(columnCount, 1) }).map((_, colIdx) => (jsx(Skeleton, { variant: "rounded", height: 28, sx: { flex: 1, minWidth: 0 }, animation: "wave" }, colIdx))) }, rowIdx))) }));
205
+ const useResponsiveTier = (tabletBreakpoint, mobileBreakpoint, enabled) => {
206
+ const [tier, setTier] = useState('desktop');
207
+ useEffect(() => {
208
+ if (!enabled || typeof window === 'undefined') {
209
+ setTier('desktop');
210
+ return;
211
+ }
212
+ const mqlTablet = window.matchMedia(`(max-width: ${tabletBreakpoint}px)`);
213
+ const mqlMobile = window.matchMedia(`(max-width: ${mobileBreakpoint}px)`);
214
+ const update = () => {
215
+ setTier(mqlMobile.matches ? 'mobile' : mqlTablet.matches ? 'tablet' : 'desktop');
216
+ };
217
+ update();
218
+ mqlTablet.addEventListener('change', update);
219
+ mqlMobile.addEventListener('change', update);
220
+ return () => {
221
+ mqlTablet.removeEventListener('change', update);
222
+ mqlMobile.removeEventListener('change', update);
223
+ };
224
+ }, [tabletBreakpoint, mobileBreakpoint, enabled]);
225
+ return enabled ? tier : 'desktop';
226
+ };
227
+ const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoading, canPrev, canNext, onFirst, onPrev, onNext, onLast, onPageSizeChange, pageSizeOptions, density, gridId, isStacked, }) => {
198
228
  const fs = density.footerFontSize;
199
229
  const selectH = density.footerMinHeight - 8;
200
230
  const startRow = totalRecords === 0 ? 0 : pageIndex * pageSize + 1;
201
231
  const endRow = Math.min((pageIndex + 1) * pageSize, totalRecords);
202
232
  return (jsxs(Box, { sx: {
203
- display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
233
+ display: 'flex',
234
+ flexDirection: isStacked ? 'column' : 'row',
235
+ alignItems: isStacked ? 'stretch' : 'center',
236
+ justifyContent: isStacked ? 'center' : 'flex-end',
204
237
  gap: { xs: 1, md: 2 }, px: 1.5, py: density.footerPaddingY,
205
238
  borderTop: '1px solid', borderColor: 'divider',
206
239
  bgcolor: 'background.paper', flexWrap: 'wrap',
207
240
  minHeight: density.footerMinHeight,
208
241
  flexShrink: 0,
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: {
242
+ }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: isStacked ? 'center' : 'flex-start', 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: {
210
243
  height: selectH, fontSize: fs - 1, minWidth: 72,
211
244
  '.MuiSelect-select': { py: '2px !important' },
212
245
  '.MuiOutlinedInput-notchedOutline': { borderColor: 'divider' },
213
- }, children: pageSizeOptions.map((opt) => (jsx(MenuItem, { value: opt, sx: { fontSize: fs - 1 }, children: opt.toLocaleString() }, opt))) })] }), jsx(Typography, { sx: { fontSize: fs }, color: "text.secondary", noWrap: true, children: isLoading || (isFetching && totalRecords === 0)
246
+ }, children: pageSizeOptions.map((opt) => (jsx(MenuItem, { value: opt, sx: { fontSize: fs - 1 }, children: opt.toLocaleString() }, opt))) })] }), jsx(Typography, { sx: { fontSize: fs, textAlign: isStacked ? 'center' : 'left' }, color: "text.secondary", noWrap: true, children: isLoading || (isFetching && totalRecords === 0)
214
247
  ? 'Loading…'
215
248
  : totalRecords === 0
216
249
  ? 'No records'
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: {
250
+ : `${startRow.toLocaleString()}–${endRow.toLocaleString()} of ${totalRecords.toLocaleString()}` }), jsxs(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: isStacked ? 'center' : 'flex-start', 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: {
218
251
  mx: 0.5,
219
252
  minWidth: density.footerMinHeight - 12,
220
253
  height: density.footerMinHeight - 12,
@@ -224,14 +257,48 @@ const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoa
224
257
  }, 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 } }) }) }) })] })] }));
225
258
  };
226
259
  // ─── GenericPaginatedGrid ─────────────────────────────────────────────────────
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, }) {
228
- const densityTokens = useMemo(() => ({ ...UI_DENSITY_PRESETS[uiDensity], ...uiDensityOptions }), [uiDensity, uiDensityOptions]);
260
+ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs, defaultFilters, defaultVisibleFilterIds, batchSize = 10000, exportFilename = 'export', gridId, tableHeight, tableMinHeight = '300px', tableMaxHeight, autoFillViewport = true, layoutMode = 'grid', uiDensity, uiDensityOptions, responsiveBreakpoints, disableResponsive = false, pageSizeOptions = [1000, 5000, 10000], showGlobalSearchByDefault = false, headerBgColor, exportHeaderBgColor, exportHeaderTextColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters = 4, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding = !!renderDetailPanel, }) {
261
+ // ── Responsive tier detection ────────────────────────────────────────────────
262
+ // Three tiers (desktop / tablet / mobile) drive auto density, a progressively
263
+ // lower filter-chip cap, toolbar wrapping, and a stacked pagination footer —
264
+ // unless the consumer opts out via `disableResponsive` or pins `uiDensity`
265
+ // explicitly (density only).
266
+ const tier = useResponsiveTier(responsiveBreakpoints?.tablet ?? 1024, responsiveBreakpoints?.mobile ?? 640, !disableResponsive);
267
+ const isNarrow = tier !== 'desktop'; // tablet or mobile — density, toolbar wrap
268
+ const isMobileTier = tier === 'mobile'; // mobile only — footer stacking, tightest chip cap
269
+ const resolvedUiDensity = uiDensity ?? (isNarrow ? 'compact' : 'default');
270
+ const effectiveMaxVisibleFilters = disableResponsive
271
+ ? maxVisibleFilters
272
+ : tier === 'mobile'
273
+ ? Math.min(maxVisibleFilters, 2)
274
+ : tier === 'tablet'
275
+ ? Math.min(maxVisibleFilters, 3)
276
+ : maxVisibleFilters;
277
+ const densityTokens = useMemo(() => ({ ...UI_DENSITY_PRESETS[resolvedUiDensity], ...uiDensityOptions }), [resolvedUiDensity, uiDensityOptions]);
229
278
  const resolvedGridId = gridId ?? exportFilename;
230
- const resolvedHeaderMinHeight = toCssSize(headerMinHeight, uiDensity === 'compact' ? '34px' : '40px');
231
- const resolvedHeaderMaxHeight = toCssSize(headerMaxHeight, uiDensity === 'compact' ? '56px' : '72px');
232
- const resolvedHeaderFontSize = toCssSize(headerFontSize ?? densityTokens.headerFontSize, uiDensity === 'compact' ? '11px' : '12px');
233
- const resolvedBodyFontSize = toCssSize(bodyFontSize ?? densityTokens.bodyFontSize, uiDensity === 'compact' ? '11px' : '12px');
279
+ const resolvedHeaderMinHeight = toCssSize(headerMinHeight, resolvedUiDensity === 'compact' ? '34px' : '40px');
280
+ const resolvedHeaderMaxHeight = toCssSize(headerMaxHeight, resolvedUiDensity === 'compact' ? '56px' : '72px');
281
+ const resolvedHeaderFontSize = toCssSize(headerFontSize ?? densityTokens.headerFontSize, resolvedUiDensity === 'compact' ? '11px' : '12px');
282
+ const resolvedBodyFontSize = toCssSize(bodyFontSize ?? densityTokens.bodyFontSize, resolvedUiDensity === 'compact' ? '11px' : '12px');
234
283
  const tableStyleOverrides = useMemo(() => ({
284
+ // Static rules folded in here so the whole object comes from one useMemo
285
+ // and GlobalStyles receives a stable reference unless these props change.
286
+ 'button[aria-label="Pin to right"]': { display: 'none' },
287
+ '.tp-gpg-root .MuiTableSortLabel-icon': { display: 'none !important' },
288
+ '.tp-gpg-root button[aria-label="Move"]': { display: 'none !important' },
289
+ '.tp-gpg-root [data-column-id="mrt-row-spacer"]': { display: 'none !important' },
290
+ '.tp-gpg-root .Mui-TableHeadCell-ColumnSpacer': { display: 'none !important' },
291
+ // Visible horizontal scrollbar — touch devices (iOS) hide scrollbars by
292
+ // default, leaving no affordance that a horizontally-overflowing table
293
+ // (common on narrow viewports) can be scrolled.
294
+ '.tp-gpg-root .MuiTableContainer-root::-webkit-scrollbar': {
295
+ height: 6,
296
+ width: 6,
297
+ },
298
+ '.tp-gpg-root .MuiTableContainer-root::-webkit-scrollbar-thumb': {
299
+ backgroundColor: 'rgba(0,0,0,0.25)',
300
+ borderRadius: 4,
301
+ },
235
302
  ...buildHeaderGlobalStyles(resolvedHeaderMinHeight, resolvedHeaderMaxHeight, resolvedHeaderFontSize, headerBgColor),
236
303
  ...buildBodyGlobalStyles(resolvedBodyFontSize),
237
304
  }), [
@@ -400,10 +467,15 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
400
467
  const handleToggleFilter = useCallback((id) => setVisibleFilterIds((prev) => {
401
468
  if (prev.includes(id))
402
469
  return prev.filter((v) => v !== id); // always allow remove
403
- if (prev.length >= maxVisibleFilters)
470
+ if (prev.length >= effectiveMaxVisibleFilters)
404
471
  return prev; // cap reached — ignore add
405
472
  return [...prev, id];
406
- }), [maxVisibleFilters]);
473
+ }), [effectiveMaxVisibleFilters]);
474
+ // Trim already-visible chips down to the new cap when the viewport narrows
475
+ // (e.g. resizing from desktop to mobile with 4 chips already shown).
476
+ useEffect(() => {
477
+ setVisibleFilterIds((prev) => prev.length > effectiveMaxVisibleFilters ? prev.slice(0, effectiveMaxVisibleFilters) : prev);
478
+ }, [effectiveMaxVisibleFilters]);
407
479
  const handleClearAll = useCallback(() => {
408
480
  setFilterValues(defaultFilters);
409
481
  setColumnFilters([]);
@@ -411,12 +483,15 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
411
483
  setGlobalFilter('');
412
484
  setPagination((p) => ({ ...p, pageIndex: 0 }));
413
485
  }, [defaultFilters, defaultVisible]);
414
- const handleExport = useCallback(() => {
486
+ const handleExport = useCallback(async () => {
487
+ // Loaded on demand (and cached) so the xlsx-js-style library is excluded
488
+ // from the initial bundle for consumers who never click Export.
489
+ const XLSX = await getXLSX();
415
490
  const dataCols = columns.filter((c) => 'accessorKey' in c && c.accessorKey);
416
491
  // 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';
492
+ const toArgb = (hex, fallback = 'FF1565C0') => hex?.startsWith('#') ? 'FF' + hex.slice(1).toUpperCase().padEnd(6, '0') : fallback;
493
+ const headerArgb = toArgb(exportHeaderBgColor ?? headerBgColor);
494
+ const textArgb = toArgb(exportHeaderTextColor, 'FFFFFFFF');
420
495
  const headerRow = dataCols.map((c) => c.header ?? '');
421
496
  const dataRows = rows.map((row) => dataCols.map((c) => String(row[c.accessorKey] ?? '')));
422
497
  const ws = XLSX.utils.aoa_to_sheet([headerRow, ...dataRows]);
@@ -426,7 +501,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
426
501
  if (ws[cellRef]) {
427
502
  ws[cellRef].s = {
428
503
  fill: { patternType: 'solid', fgColor: { rgb: headerArgb } },
429
- font: { bold: true, color: { rgb: 'FFFFFFFF' }, sz: 10 },
504
+ font: { bold: true, color: { rgb: textArgb }, sz: 10 },
430
505
  alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
431
506
  border: {
432
507
  bottom: { style: 'thin', color: { rgb: 'FFCCCCCC' } },
@@ -435,19 +510,94 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
435
510
  };
436
511
  }
437
512
  });
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
- });
513
+ // Auto-fit column widths — single O(n) pass accumulates max length per column
514
+ // instead of one reduce() per column (was O(n × cols)).
515
+ const colWidths = dataCols.map((c) => (c.header ?? '').length);
516
+ for (const row of rows) {
517
+ const r = row;
518
+ for (let ci = 0; ci < dataCols.length; ci++) {
519
+ const len = String(r[dataCols[ci].accessorKey] ?? '').length;
520
+ if (len > colWidths[ci])
521
+ colWidths[ci] = len;
522
+ }
523
+ }
524
+ ws['!cols'] = colWidths.map((w) => ({ wch: Math.max(w, 8) + 2 }));
447
525
  const wb = XLSX.utils.book_new();
448
526
  XLSX.utils.book_append_sheet(wb, ws, 'Data');
449
527
  XLSX.writeFile(wb, `${exportFilename}-page${pagination.pageIndex + 1}.xlsx`);
450
- }, [columns, rows, exportFilename, pagination.pageIndex, headerBgColor]);
528
+ }, [columns, rows, exportFilename, pagination.pageIndex, headerBgColor, exportHeaderBgColor, exportHeaderTextColor]);
529
+ // ── Memoized cell-prop builders ───────────────────────────────────────────
530
+ // These are functions passed to MRT per-cell. Without useCallback they get
531
+ // new references on every render, forcing MRT to rebuild every cell's sx.
532
+ const memoizedHeadCellProps = useCallback((params) => {
533
+ const { column, table } = params;
534
+ const colDef = column.columnDef.muiTableHeadCellProps;
535
+ const colResolved = typeof colDef === 'function' ? (colDef({ column, table }) ?? {}) : colDef ?? {};
536
+ const gridResolved = typeof gridHeadCellProps === 'function'
537
+ ? (gridHeadCellProps({ column, table }) ?? {})
538
+ : gridHeadCellProps ?? {};
539
+ const align = (colResolved.align ?? gridResolved.align ?? 'left');
540
+ const { sx: gridSx, align: _ga, ...gridRest } = gridResolved;
541
+ const { sx: colSx, align: _ca, ...colRest } = colResolved;
542
+ return {
543
+ ...gridRest,
544
+ ...colRest,
545
+ align,
546
+ sx: [
547
+ buildHeaderCellSx(align, resolvedHeaderMinHeight, resolvedHeaderMaxHeight, resolvedHeaderFontSize, headerBgColor, headerMaxLines),
548
+ ...(gridSx ? [gridSx] : []),
549
+ ...(colSx ? [colSx] : []),
550
+ ],
551
+ };
552
+ }, [
553
+ resolvedHeaderMinHeight,
554
+ resolvedHeaderMaxHeight,
555
+ resolvedHeaderFontSize,
556
+ headerBgColor,
557
+ headerMaxLines,
558
+ gridHeadCellProps,
559
+ ]);
560
+ const memoizedBodyCellProps = useCallback((params) => {
561
+ const { cell, column, row, table } = params;
562
+ const colDef = column.columnDef.muiTableBodyCellProps;
563
+ const colResolved = typeof colDef === 'function' ? (colDef({ cell, column, row, table }) ?? {}) : colDef ?? {};
564
+ const gridResolved = typeof gridBodyCellProps === 'function'
565
+ ? (gridBodyCellProps({ cell, column, row, table }) ?? {})
566
+ : gridBodyCellProps ?? {};
567
+ const { sx: gridSx, ...gridRest } = gridResolved;
568
+ const { sx: colSx, ...colRest } = colResolved;
569
+ return {
570
+ ...gridRest,
571
+ ...colRest,
572
+ sx: [
573
+ {
574
+ whiteSpace: 'nowrap',
575
+ fontSize: resolvedBodyFontSize,
576
+ lineHeight: 1.35,
577
+ borderRight: '1px solid rgba(224,224,224,1)',
578
+ },
579
+ ...(gridSx ? [gridSx] : []),
580
+ ...(colSx ? [colSx] : []),
581
+ ],
582
+ };
583
+ }, [resolvedBodyFontSize, gridBodyCellProps]);
584
+ // ── Memoized toolbar render functions ─────────────────────────────────────
585
+ // Defined with useCallback so MRT receives stable references and does not
586
+ // re-render the toolbar on every isFetching / data update cycle.
587
+ const renderTopToolbarCustomActions = useCallback(() => (jsxs(Box, { sx: {
588
+ display: 'flex',
589
+ alignItems: 'center',
590
+ flexWrap: isNarrow ? 'wrap' : 'nowrap',
591
+ flex: 1, minWidth: 0, pr: 1, gap: 0.5,
592
+ }, children: [jsxs(Box, { sx: { display: 'flex', alignItems: 'center', flexShrink: 0, gap: 0.25 }, children: [jsx(Tooltip, { title: "Clear filters & refresh", children: jsx("span", { children: jsx(IconButton, { id: `${resolvedGridId}_refresh_icon`, size: "small", onClick: () => { handleClearAll(); refetch(); }, disabled: isFetching, sx: { p: resolvedUiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(RefreshIcon, { sx: { fontSize: resolvedUiDensity === 'compact' ? 18 : 20 } }) }) }) }), jsx(Tooltip, { title: "Export batch as Excel (.xlsx)", children: jsx("span", { children: jsx(IconButton, { id: `${resolvedGridId}_export_icon`, size: "small", onClick: handleExport, disabled: rows.length === 0 || isFetching, sx: { p: resolvedUiDensity === 'compact' ? 0.35 : 0.5 }, children: jsx(FileDownloadIcon, { sx: { fontSize: resolvedUiDensity === 'compact' ? 18 : 20 } }) }) }) })] }), jsx(Box, { sx: { flex: 1, minWidth: isNarrow ? '100%' : 0 }, children: jsx(DynamicFilterBar, { serverFilterDefs: serverFilterDefs, visibleFilterIds: visibleFilterIds, onToggleFilter: handleToggleFilter, filterValues: filterValues, onFilterValuesChange: handleFilterValuesChange, columnFilters: columnFilters, onColumnFilterChange: setColumnFilters, availableColumns: availableColumns, maxVisibleFilters: effectiveMaxVisibleFilters, uiDensity: resolvedUiDensity, filterChipFontSize: densityTokens.filterChipFontSize, filterChipMinHeight: densityTokens.filterChipMinHeight, filterLabelFontSize: densityTokens.filterLabelFontSize, gridId: resolvedGridId }) })] })), [
593
+ resolvedGridId, isFetching, rows.length, resolvedUiDensity, isNarrow,
594
+ handleClearAll, refetch, handleExport,
595
+ serverFilterDefs, visibleFilterIds, handleToggleFilter,
596
+ filterValues, handleFilterValuesChange, columnFilters, setColumnFilters,
597
+ availableColumns, effectiveMaxVisibleFilters, densityTokens, handleToggleFilter,
598
+ ]);
599
+ const renderToolbarInternalActions = useCallback(({ 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` })] })), [resolvedGridId]);
600
+ const renderBottomToolbar = useCallback(({ 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, isStacked: isMobileTier })), [totalRecords, isFetching, isLoading, pageSizeOptions, densityTokens, resolvedGridId, isMobileTier]);
451
601
  // ── Table instance ─────────────────────────────────────────────────────────
452
602
  const table = useMaterialReactTable({
453
603
  columns: processedColumns,
@@ -544,6 +694,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
544
694
  minHeight: 0,
545
695
  maxHeight: '100%',
546
696
  overflow: 'auto',
697
+ WebkitOverflowScrolling: 'touch',
547
698
  },
548
699
  },
549
700
  muiTablePaperProps: {
@@ -559,10 +710,10 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
559
710
  muiTopToolbarProps: {
560
711
  sx: {
561
712
  alignItems: 'center',
562
- flexWrap: 'nowrap',
713
+ flexWrap: isNarrow ? 'wrap' : 'nowrap',
563
714
  minHeight: densityTokens.filterChipMinHeight + 12,
564
- py: uiDensity === 'compact' ? 0.25 : 0.5,
565
- '& .MuiIconButton-root': { p: uiDensity === 'compact' ? 0.35 : 0.5 },
715
+ py: resolvedUiDensity === 'compact' ? 0.25 : 0.5,
716
+ '& .MuiIconButton-root': { p: resolvedUiDensity === 'compact' ? 0.35 : 0.5 },
566
717
  '& .MuiIconButton-root svg': { fontSize: densityTokens.toolbarIconFontSize === 'small' ? 18 : 20 },
567
718
  },
568
719
  },
@@ -573,7 +724,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
573
724
  size: 'small',
574
725
  variant: 'outlined',
575
726
  sx: {
576
- minWidth: uiDensity === 'compact' ? 160 : 200,
727
+ minWidth: resolvedUiDensity === 'compact' ? 160 : 200,
577
728
  '& .MuiOutlinedInput-root': {
578
729
  height: densityTokens.filterChipMinHeight,
579
730
  fontSize: densityTokens.filterChipFontSize,
@@ -604,69 +755,16 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
604
755
  ...(headerBgColor ? { backgroundColor: headerBgColor } : {}),
605
756
  },
606
757
  },
607
- muiTableHeadCellProps: (params) => {
608
- const { column, table } = params;
609
- // ── Resolve column-level props ────────────────────────────────────────
610
- const colDef = column.columnDef.muiTableHeadCellProps;
611
- const colResolved = typeof colDef === 'function' ? (colDef({ column, table }) ?? {}) : colDef ?? {};
612
- // ── Resolve grid-level props ──────────────────────────────────────────
613
- const gridResolved = typeof gridHeadCellProps === 'function'
614
- ? (gridHeadCellProps({ column, table }) ?? {})
615
- : gridHeadCellProps ?? {};
616
- // Column align overrides grid align; default 'left'
617
- const align = (colResolved.align ?? gridResolved.align ?? 'left');
618
- const { sx: gridSx, align: _ga, ...gridRest } = gridResolved;
619
- const { sx: colSx, align: _ca, ...colRest } = colResolved;
620
- return {
621
- ...gridRest, // grid-level props (className, etc.)
622
- ...colRest, // column-level overrides
623
- align,
624
- sx: [
625
- buildHeaderCellSx(align, resolvedHeaderMinHeight, resolvedHeaderMaxHeight, resolvedHeaderFontSize, headerBgColor, headerMaxLines),
626
- ...(gridSx ? [gridSx] : []),
627
- ...(colSx ? [colSx] : []),
628
- ],
629
- };
630
- },
631
- muiTableBodyCellProps: (params) => {
632
- const { cell, column, row, table } = params;
633
- // ── Resolve column-level props ────────────────────────────────────────
634
- const colDef = column.columnDef.muiTableBodyCellProps;
635
- const colResolved = typeof colDef === 'function' ? (colDef({ cell, column, row, table }) ?? {}) : colDef ?? {};
636
- // ── Resolve grid-level props ──────────────────────────────────────────
637
- const gridResolved = typeof gridBodyCellProps === 'function'
638
- ? (gridBodyCellProps({ cell, column, row, table }) ?? {})
639
- : gridBodyCellProps ?? {};
640
- const { sx: gridSx, ...gridRest } = gridResolved;
641
- const { sx: colSx, ...colRest } = colResolved;
642
- return {
643
- ...gridRest, // grid-level props (title, className, etc.)
644
- ...colRest, // column-level overrides (align, etc.)
645
- sx: [
646
- // 1. base styles — always applied
647
- {
648
- whiteSpace: 'nowrap',
649
- fontSize: resolvedBodyFontSize,
650
- lineHeight: 1.35,
651
- borderRight: '1px solid rgba(224,224,224,1)',
652
- },
653
- // 2. grid-level sx additions
654
- ...(gridSx ? [gridSx] : []),
655
- // 3. column-level sx overrides (minWidth, maxWidth, textAlign, etc.)
656
- ...(colSx ? [colSx] : []),
657
- ],
658
- };
659
- },
758
+ muiTableHeadCellProps: memoizedHeadCellProps,
759
+ muiTableBodyCellProps: memoizedBodyCellProps,
660
760
  muiToolbarAlertBannerProps: isError
661
761
  ? { color: 'error', children: 'Failed to load data. Please try again.' }
662
762
  : undefined,
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 })),
763
+ // Memoized via useCallback above — stable references prevent MRT from
764
+ // re-rendering the toolbar on every isFetching / data update cycle.
765
+ renderTopToolbarCustomActions,
766
+ renderToolbarInternalActions,
767
+ renderBottomToolbar,
670
768
  });
671
769
  return (jsxs(Box, { ref: containerRef, id: resolvedGridId, className: "tp-gpg-root", sx: {
672
770
  position: 'relative',
@@ -676,14 +774,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
676
774
  display: 'flex',
677
775
  flexDirection: 'column',
678
776
  overflow: 'hidden',
679
- }, children: [jsx(GlobalStyles, { styles: {
680
- 'button[aria-label="Pin to right"]': { display: 'none' },
681
- '.tp-gpg-root .MuiTableSortLabel-icon': { display: 'none !important' },
682
- '.tp-gpg-root button[aria-label="Move"]': { display: 'none !important' },
683
- '.tp-gpg-root [data-column-id="mrt-row-spacer"]': { display: 'none !important' },
684
- '.tp-gpg-root .Mui-TableHeadCell-ColumnSpacer': { display: 'none !important' },
685
- ...tableStyleOverrides,
686
- } }), isLoading && jsx(OverlayLoader, { message: "Loading data\u2026" }), !isLoading && isFetching && jsx(OverlayLoader, { message: "Updating data\u2026" }), jsx(Box, { sx: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: jsx(MaterialReactTable, { table: table }) })] }));
777
+ }, children: [jsx(GlobalStyles, { styles: tableStyleOverrides }), isLoading && jsx(SkeletonOverlay, { columnCount: processedColumns.length }), !isLoading && isFetching && jsx(OverlayLoader, { message: "Updating data\u2026" }), jsx(Box, { sx: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: jsx(MaterialReactTable, { table: table }) })] }));
687
778
  }
688
779
 
689
780
  export { GenericPaginatedGrid as default };
@@ -6,7 +6,7 @@ import ListItemText from '@mui/material/ListItemText';
6
6
  import MenuItem from '@mui/material/MenuItem';
7
7
  import Select from '@mui/material/Select';
8
8
  import * as React from 'react';
9
- import { useState } from 'react';
9
+ import { useState, useCallback } from 'react';
10
10
 
11
11
  const ITEM_HEIGHT = 36;
12
12
  const ITEM_PADDING_TOP = 2;
@@ -52,11 +52,11 @@ function MultiSelectFieldComponent({ value, options, onChangeFn, disable = false
52
52
  setSelectAll(false);
53
53
  }
54
54
  }, [filterOptions, fieldValue]);
55
- const handleChange = (event) => {
55
+ const handleChange = useCallback((event) => {
56
56
  const { target: { value }, } = event;
57
57
  setPersonName(typeof value === 'string' ? value?.split(',') : value);
58
58
  onChangeFn((typeof value === 'string' ? value?.split(',') : value).join(','));
59
- };
59
+ }, [onChangeFn]);
60
60
  const selectedOptions = options.filter((item) => fieldValue?.split(',').some((value) => value === item.value));
61
61
  const selectedValues = selectedOptions.map((item) => item.label).join(' , ');
62
62
  return (jsx(Box, { children: jsx(FormControl, { fullWidth: true, ref: containerRef, sx: {