tp-react-elements 2.0.4 → 2.0.5

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.esm14.js +2 -2
  4. package/dist/_virtual/index.esm4.js +2 -2
  5. package/dist/_virtual/index.esm6.js +2 -2
  6. package/dist/_virtual/index.esm7.js +2 -2
  7. package/dist/components/DateRangePicker/DateRangePicker.esm.js +49 -72
  8. package/dist/components/Form/FormRender.esm.js +1 -1
  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/FormComponents/index.esm.js +2 -2
  24. package/dist/components/ModalField/ConfirmationDialog.esm.js +3 -3
  25. package/dist/components/ModalField/ModalField.esm.js +5 -12
  26. package/dist/components/PaginatedGrid/DynamicFilterBar.d.ts +1 -1
  27. package/dist/components/PaginatedGrid/DynamicFilterBar.esm.js +27 -24
  28. package/dist/components/PaginatedGrid/GenericPaginatedGrid.d.ts +13 -1
  29. package/dist/components/PaginatedGrid/GenericPaginatedGrid.esm.js +102 -85
  30. package/dist/components/SelectField/MultiSelectFieldComponent.esm.js +3 -3
  31. package/dist/components/SessionTimeOut/SessionTimeOut.esm.js +33 -34
  32. package/dist/node_modules/react-date-range/dist/components/Calendar/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') {
@@ -109,6 +109,18 @@ export interface GenericPaginatedGridProps<TData extends object> {
109
109
  * Accepts any valid CSS colour string.
110
110
  */
111
111
  headerBgColor?: string;
112
+ /**
113
+ * Background colour for the Excel export header row.
114
+ * Accepts a hex colour string (`#RRGGBB`).
115
+ * Falls back to `headerBgColor` when omitted, then to the default blue `#1565C0`.
116
+ */
117
+ exportHeaderBgColor?: string;
118
+ /**
119
+ * Text colour for the Excel export header row.
120
+ * Accepts a hex colour string (`#RRGGBB`).
121
+ * Defaults to white (`#FFFFFF`) when omitted.
122
+ */
123
+ exportHeaderTextColor?: string;
112
124
  /**
113
125
  * Minimum height of every header cell (CSS length or px number).
114
126
  * Default: `40px` (`34px` when `uiDensity="compact"`).
@@ -198,5 +210,5 @@ export interface GenericPaginatedGridProps<TData extends object> {
198
210
  */
199
211
  enableExpanding?: boolean;
200
212
  }
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;
213
+ 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, exportHeaderBgColor, exportHeaderTextColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding, }: GenericPaginatedGridProps<TData>): import("react/jsx-runtime").JSX.Element;
202
214
  export default GenericPaginatedGrid;
@@ -1,7 +1,7 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
2
  import { useMemo, useState, useRef, useLayoutEffect, useCallback } from 'react';
3
3
  import * as XLSX from 'xlsx-js-style';
4
- import { useMaterialReactTable, MRT_ToggleGlobalFilterButton, MRT_ShowHideColumnsButton, MRT_ToggleDensePaddingButton, MRT_ToggleFullScreenButton, MaterialReactTable } from 'material-react-table';
4
+ import { MRT_ToggleGlobalFilterButton, MRT_ShowHideColumnsButton, MRT_ToggleDensePaddingButton, MRT_ToggleFullScreenButton, useMaterialReactTable, MaterialReactTable } from 'material-react-table';
5
5
  import { useQuery, keepPreviousData } from '@tanstack/react-query';
6
6
  import { Box, Tooltip, IconButton, GlobalStyles, Typography, Select, MenuItem, CircularProgress } from '@mui/material';
7
7
  import RefreshIcon from '@mui/icons-material/Refresh';
@@ -224,7 +224,7 @@ const PaginationFooter = ({ pageIndex, pageSize, totalRecords, isFetching, isLoa
224
224
  }, children: pageIndex + 1 }), jsx(Tooltip, { title: "Next page", children: jsx("span", { children: jsx(IconButton, { id: `${gridId}_next_page_icon`, size: "small", onClick: onNext, disabled: !canNext, sx: { p: 0.5 }, children: jsx(ChevronRightIcon, { sx: { fontSize: density.footerIconSize } }) }) }) }), jsx(Tooltip, { title: "Last page", children: jsx("span", { children: jsx(IconButton, { id: `${gridId}_last_page_icon`, size: "small", onClick: onLast, disabled: !canNext, sx: { p: 0.5 }, children: jsx(LastPageIcon, { sx: { fontSize: density.footerIconSize } }) }) }) })] })] }));
225
225
  };
226
226
  // ─── 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, }) {
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, exportHeaderBgColor, exportHeaderTextColor, headerMinHeight, headerMaxHeight, headerMaxLines, headerFontSize, bodyFontSize, maxVisibleFilters = 4, muiTableBodyCellProps: gridBodyCellProps, muiTableHeadCellProps: gridHeadCellProps, renderDetailPanel, enableExpanding = !!renderDetailPanel, }) {
228
228
  const densityTokens = useMemo(() => ({ ...UI_DENSITY_PRESETS[uiDensity], ...uiDensityOptions }), [uiDensity, uiDensityOptions]);
229
229
  const resolvedGridId = gridId ?? exportFilename;
230
230
  const resolvedHeaderMinHeight = toCssSize(headerMinHeight, uiDensity === 'compact' ? '34px' : '40px');
@@ -232,6 +232,13 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
232
232
  const resolvedHeaderFontSize = toCssSize(headerFontSize ?? densityTokens.headerFontSize, uiDensity === 'compact' ? '11px' : '12px');
233
233
  const resolvedBodyFontSize = toCssSize(bodyFontSize ?? densityTokens.bodyFontSize, uiDensity === 'compact' ? '11px' : '12px');
234
234
  const tableStyleOverrides = useMemo(() => ({
235
+ // Static rules folded in here so the whole object comes from one useMemo
236
+ // and GlobalStyles receives a stable reference unless these props change.
237
+ 'button[aria-label="Pin to right"]': { display: 'none' },
238
+ '.tp-gpg-root .MuiTableSortLabel-icon': { display: 'none !important' },
239
+ '.tp-gpg-root button[aria-label="Move"]': { display: 'none !important' },
240
+ '.tp-gpg-root [data-column-id="mrt-row-spacer"]': { display: 'none !important' },
241
+ '.tp-gpg-root .Mui-TableHeadCell-ColumnSpacer': { display: 'none !important' },
235
242
  ...buildHeaderGlobalStyles(resolvedHeaderMinHeight, resolvedHeaderMaxHeight, resolvedHeaderFontSize, headerBgColor),
236
243
  ...buildBodyGlobalStyles(resolvedBodyFontSize),
237
244
  }), [
@@ -414,9 +421,9 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
414
421
  const handleExport = useCallback(() => {
415
422
  const dataCols = columns.filter((c) => 'accessorKey' in c && c.accessorKey);
416
423
  // 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';
424
+ const toArgb = (hex, fallback = 'FF1565C0') => hex?.startsWith('#') ? 'FF' + hex.slice(1).toUpperCase().padEnd(6, '0') : fallback;
425
+ const headerArgb = toArgb(exportHeaderBgColor ?? headerBgColor);
426
+ const textArgb = toArgb(exportHeaderTextColor, 'FFFFFFFF');
420
427
  const headerRow = dataCols.map((c) => c.header ?? '');
421
428
  const dataRows = rows.map((row) => dataCols.map((c) => String(row[c.accessorKey] ?? '')));
422
429
  const ws = XLSX.utils.aoa_to_sheet([headerRow, ...dataRows]);
@@ -426,7 +433,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
426
433
  if (ws[cellRef]) {
427
434
  ws[cellRef].s = {
428
435
  fill: { patternType: 'solid', fgColor: { rgb: headerArgb } },
429
- font: { bold: true, color: { rgb: 'FFFFFFFF' }, sz: 10 },
436
+ font: { bold: true, color: { rgb: textArgb }, sz: 10 },
430
437
  alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
431
438
  border: {
432
439
  bottom: { style: 'thin', color: { rgb: 'FFCCCCCC' } },
@@ -435,19 +442,89 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
435
442
  };
436
443
  }
437
444
  });
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
- });
445
+ // Auto-fit column widths — single O(n) pass accumulates max length per column
446
+ // instead of one reduce() per column (was O(n × cols)).
447
+ const colWidths = dataCols.map((c) => (c.header ?? '').length);
448
+ for (const row of rows) {
449
+ const r = row;
450
+ for (let ci = 0; ci < dataCols.length; ci++) {
451
+ const len = String(r[dataCols[ci].accessorKey] ?? '').length;
452
+ if (len > colWidths[ci])
453
+ colWidths[ci] = len;
454
+ }
455
+ }
456
+ ws['!cols'] = colWidths.map((w) => ({ wch: Math.max(w, 8) + 2 }));
447
457
  const wb = XLSX.utils.book_new();
448
458
  XLSX.utils.book_append_sheet(wb, ws, 'Data');
449
459
  XLSX.writeFile(wb, `${exportFilename}-page${pagination.pageIndex + 1}.xlsx`);
450
- }, [columns, rows, exportFilename, pagination.pageIndex, headerBgColor]);
460
+ }, [columns, rows, exportFilename, pagination.pageIndex, headerBgColor, exportHeaderBgColor, exportHeaderTextColor]);
461
+ // ── Memoized cell-prop builders ───────────────────────────────────────────
462
+ // These are functions passed to MRT per-cell. Without useCallback they get
463
+ // new references on every render, forcing MRT to rebuild every cell's sx.
464
+ const memoizedHeadCellProps = useCallback((params) => {
465
+ const { column, table } = params;
466
+ const colDef = column.columnDef.muiTableHeadCellProps;
467
+ const colResolved = typeof colDef === 'function' ? (colDef({ column, table }) ?? {}) : colDef ?? {};
468
+ const gridResolved = typeof gridHeadCellProps === 'function'
469
+ ? (gridHeadCellProps({ column, table }) ?? {})
470
+ : gridHeadCellProps ?? {};
471
+ const align = (colResolved.align ?? gridResolved.align ?? 'left');
472
+ const { sx: gridSx, align: _ga, ...gridRest } = gridResolved;
473
+ const { sx: colSx, align: _ca, ...colRest } = colResolved;
474
+ return {
475
+ ...gridRest,
476
+ ...colRest,
477
+ align,
478
+ sx: [
479
+ buildHeaderCellSx(align, resolvedHeaderMinHeight, resolvedHeaderMaxHeight, resolvedHeaderFontSize, headerBgColor, headerMaxLines),
480
+ ...(gridSx ? [gridSx] : []),
481
+ ...(colSx ? [colSx] : []),
482
+ ],
483
+ };
484
+ }, [
485
+ resolvedHeaderMinHeight,
486
+ resolvedHeaderMaxHeight,
487
+ resolvedHeaderFontSize,
488
+ headerBgColor,
489
+ headerMaxLines,
490
+ gridHeadCellProps,
491
+ ]);
492
+ const memoizedBodyCellProps = useCallback((params) => {
493
+ const { cell, column, row, table } = params;
494
+ const colDef = column.columnDef.muiTableBodyCellProps;
495
+ const colResolved = typeof colDef === 'function' ? (colDef({ cell, column, row, table }) ?? {}) : colDef ?? {};
496
+ const gridResolved = typeof gridBodyCellProps === 'function'
497
+ ? (gridBodyCellProps({ cell, column, row, table }) ?? {})
498
+ : gridBodyCellProps ?? {};
499
+ const { sx: gridSx, ...gridRest } = gridResolved;
500
+ const { sx: colSx, ...colRest } = colResolved;
501
+ return {
502
+ ...gridRest,
503
+ ...colRest,
504
+ sx: [
505
+ {
506
+ whiteSpace: 'nowrap',
507
+ fontSize: resolvedBodyFontSize,
508
+ lineHeight: 1.35,
509
+ borderRight: '1px solid rgba(224,224,224,1)',
510
+ },
511
+ ...(gridSx ? [gridSx] : []),
512
+ ...(colSx ? [colSx] : []),
513
+ ],
514
+ };
515
+ }, [resolvedBodyFontSize, gridBodyCellProps]);
516
+ // ── Memoized toolbar render functions ─────────────────────────────────────
517
+ // Defined with useCallback so MRT receives stable references and does not
518
+ // re-render the toolbar on every isFetching / data update cycle.
519
+ const renderTopToolbarCustomActions = useCallback(() => (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 }) })] })), [
520
+ resolvedGridId, isFetching, rows.length, uiDensity,
521
+ handleClearAll, refetch, handleExport,
522
+ serverFilterDefs, visibleFilterIds, handleToggleFilter,
523
+ filterValues, handleFilterValuesChange, columnFilters, setColumnFilters,
524
+ availableColumns, maxVisibleFilters, densityTokens, handleToggleFilter,
525
+ ]);
526
+ 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]);
527
+ 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 })), [totalRecords, isFetching, isLoading, pageSizeOptions, densityTokens, resolvedGridId]);
451
528
  // ── Table instance ─────────────────────────────────────────────────────────
452
529
  const table = useMaterialReactTable({
453
530
  columns: processedColumns,
@@ -604,69 +681,16 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
604
681
  ...(headerBgColor ? { backgroundColor: headerBgColor } : {}),
605
682
  },
606
683
  },
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
- },
684
+ muiTableHeadCellProps: memoizedHeadCellProps,
685
+ muiTableBodyCellProps: memoizedBodyCellProps,
660
686
  muiToolbarAlertBannerProps: isError
661
687
  ? { color: 'error', children: 'Failed to load data. Please try again.' }
662
688
  : 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 })),
689
+ // Memoized via useCallback above — stable references prevent MRT from
690
+ // re-rendering the toolbar on every isFetching / data update cycle.
691
+ renderTopToolbarCustomActions,
692
+ renderToolbarInternalActions,
693
+ renderBottomToolbar,
670
694
  });
671
695
  return (jsxs(Box, { ref: containerRef, id: resolvedGridId, className: "tp-gpg-root", sx: {
672
696
  position: 'relative',
@@ -676,14 +700,7 @@ function GenericPaginatedGrid({ columns, fetchPage, queryKey, serverFilterDefs,
676
700
  display: 'flex',
677
701
  flexDirection: 'column',
678
702
  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 }) })] }));
703
+ }, children: [jsx(GlobalStyles, { styles: tableStyleOverrides }), 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 }) })] }));
687
704
  }
688
705
 
689
706
  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: {
@@ -1,62 +1,61 @@
1
- import { jsx, Fragment } from 'react/jsx-runtime';
2
- import { useState, useEffect } from 'react';
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
3
  import { useIdleTimer } from 'react-idle-timer';
4
4
  import ConfirmationDialog from '../ModalField/ConfirmationDialog.esm.js';
5
5
 
6
6
  const secondsToMMSS = (totalSeconds) => {
7
7
  const minutes = Math.floor(totalSeconds / 60);
8
8
  const seconds = Math.floor(totalSeconds % 60);
9
- const formattedMinutes = String(minutes).padStart(2, '0');
10
- const formattedSeconds = String(seconds).padStart(2, '0');
11
- return `${formattedMinutes}:${formattedSeconds}`;
9
+ return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
12
10
  };
13
11
  const promptBeforeValue = 120000;
14
12
  const SessionTimeOut = ({ sessionTime, handleSubmitSession, onSessionExpire, handleSessionCancel, SubmitButtonName = 'Submit', }) => {
15
13
  const [openConfirmModal, setOpenConfirmModal] = useState(false);
16
- const expiryDetails = sessionTime;
17
- const providedTimestamp = new Date(expiryDetails).getTime();
14
+ const providedTimestamp = new Date(sessionTime).getTime();
18
15
  const currentTimestamp = new Date().getTime();
19
- const remainingTime = providedTimestamp - currentTimestamp;
20
- const timeout = remainingTime;
16
+ const timeout = providedTimestamp - currentTimestamp;
21
17
  const promptBeforeIdle = Math.min(promptBeforeValue, timeout - 1000);
22
18
  const [, setRemainingTimeInSeconds] = useState(Math.ceil(timeout / 1000));
23
19
  const [formattedRemainingTime, setFormattedRemainingTime] = useState(secondsToMMSS(Math.ceil(timeout / 1000)));
24
- const onIdle = () => {
25
- if (sessionTime) {
20
+ const onIdle = useCallback(() => {
21
+ if (sessionTime)
26
22
  onSessionExpire();
27
- }
28
- };
29
- const handleSubmit = () => {
30
- handleSubmitSession();
31
- setOpenConfirmModal(false);
32
- };
33
- const handleCancel = () => {
34
- handleSessionCancel();
35
- setOpenConfirmModal(false);
36
- };
37
- const onPrompt = () => {
23
+ }, [sessionTime, onSessionExpire]);
24
+ const onPrompt = useCallback(() => {
38
25
  setOpenConfirmModal(true);
39
- };
26
+ }, []);
40
27
  const { getRemainingTime } = useIdleTimer({
41
28
  onIdle,
42
29
  onPrompt,
43
30
  promptBeforeIdle,
44
31
  timeout,
45
- throttle: 1000, // Adjust throttle to reduce delays
32
+ throttle: 1000,
46
33
  });
34
+ // Keep a ref to getRemainingTime so the interval effect never needs it
35
+ // as a reactive dependency — react-idle-timer guarantees it is stable,
36
+ // but ESLint cannot verify that, which previously caused a spurious
37
+ // re-subscription on every render.
38
+ const getRemainingTimeRef = useRef(getRemainingTime);
39
+ getRemainingTimeRef.current = getRemainingTime;
47
40
  useEffect(() => {
48
41
  const interval = setInterval(() => {
49
- const remainingSeconds = Math.ceil(getRemainingTime() / 1000);
50
- setRemainingTimeInSeconds(promptBeforeIdle !== promptBeforeValue ? remainingSeconds : 0);
51
- const formattedTime = secondsToMMSS(remainingSeconds);
52
- setFormattedRemainingTime(promptBeforeIdle !== promptBeforeValue ? formattedTime : '00:00');
53
- }, 1000); // Adjust interval to 1000 ms
54
- return () => {
55
- clearInterval(interval);
56
- };
57
- }, [promptBeforeIdle, getRemainingTime]);
42
+ const remainingSeconds = Math.ceil(getRemainingTimeRef.current() / 1000);
43
+ const show = promptBeforeIdle !== promptBeforeValue;
44
+ setRemainingTimeInSeconds(show ? remainingSeconds : 0);
45
+ setFormattedRemainingTime(show ? secondsToMMSS(remainingSeconds) : '00:00');
46
+ }, 1000);
47
+ return () => clearInterval(interval);
48
+ }, [promptBeforeIdle]); // getRemainingTime intentionally omitted — stable ref used instead
49
+ const handleSubmit = useCallback(() => {
50
+ handleSubmitSession();
51
+ setOpenConfirmModal(false);
52
+ }, [handleSubmitSession]);
53
+ const handleCancel = useCallback(() => {
54
+ handleSessionCancel();
55
+ setOpenConfirmModal(false);
56
+ }, [handleSessionCancel]);
58
57
  const sessionMessage = `Your session will expire in ${formattedRemainingTime} seconds. Do you want to extend the session?`;
59
- return (jsx(Fragment, { children: jsx(ConfirmationDialog, { openConfirmDialog: openConfirmModal, handleCancel: handleCancel, onClickSubmit: handleSubmit, text: sessionMessage, Submit: SubmitButtonName }) }));
58
+ return (jsx(ConfirmationDialog, { openConfirmDialog: openConfirmModal, handleCancel: handleCancel, onClickSubmit: handleSubmit, text: sessionMessage, Submit: SubmitButtonName }));
60
59
  };
61
60
 
62
61
  export { SessionTimeOut as default };
@@ -1,4 +1,4 @@
1
- import { __exports as Calendar } from '../../../../../_virtual/index.esm6.js';
1
+ import { __exports as Calendar } from '../../../../../_virtual/index.esm7.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRange } from '../../../../../_virtual/index.esm4.js';
1
+ import { __exports as DateRange } from '../../../../../_virtual/index.esm6.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireCalendar } from '../Calendar/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm7.js';
1
+ import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm4.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDateRange } from '../DateRange/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DayCell } from '../../../../../_virtual/index.esm11.js';
1
+ import { __exports as DayCell } from '../../../../../_virtual/index.esm12.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireClassnames } from '../../../../classnames/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as InputRangeField } from '../../../../../_virtual/index.esm14.js';
1
+ import { __exports as InputRangeField } from '../../../../../_virtual/index.esm11.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { __exports as Month } from '../../../../../_virtual/index.esm12.js';
1
+ import { __exports as Month } from '../../../../../_virtual/index.esm14.js';
2
2
  import React__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tp-react-elements",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "React form components library built with React Hook Form and Yup",