From 57be7d0e0ceb7d30dc492a35f6d5f4632b0bd166 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:20:43 +0200 Subject: [PATCH] refactor(DataTable): streamline column visibility logic and enhance type definitions; improve cleanup timers and optimize rendering --- .../src/components/DataTable/DataTable.tsx | 145 ++++++++---------- 1 file changed, 65 insertions(+), 80 deletions(-) diff --git a/packages/client/src/components/DataTable/DataTable.tsx b/packages/client/src/components/DataTable/DataTable.tsx index 8ae4ba808a..2cb332b448 100644 --- a/packages/client/src/components/DataTable/DataTable.tsx +++ b/packages/client/src/components/DataTable/DataTable.tsx @@ -8,29 +8,25 @@ import { type SortingState, type VisibilityState, type ColumnDef, - type CellContext, type Row, + type Table as TTable, } from '@tanstack/react-table'; import type { DataTableProps } from './DataTable.types'; -import { - Table, - TableBody, - TableHead, - TableHeader, - TableCell, - TableRow, - Button, - Label, -} from '~/components'; import { SelectionCheckbox, MemoizedTableRow, SkeletonRows } from './DataTableComponents'; +import { Table, TableBody, TableHead, TableHeader, TableCell, TableRow } from '../Table'; import { useDebounced, useOptimizedRowSelection } from './DataTable.hooks'; import { DataTableErrorBoundary } from './DataTableErrorBoundary'; -import { DataTableSearch } from './DataTableSearch'; import { useMediaQuery, useLocalize } from '~/hooks'; -import { useToastContext } from '~/Providers'; +import { DataTableSearch } from './DataTableSearch'; import { cn, logger } from '~/utils'; +import { Button } from '../Button'; +import { Label } from '../Label'; import { Spinner } from '~/svgs'; +type ProcessedDataRow = TData & { _id: string; _index: number }; + +type TableColumnDef = ColumnDef, TValue>; + function DataTable, TValue>({ columns, data, @@ -50,9 +46,8 @@ function DataTable, TValue>({ customActionsRenderer, }: DataTableProps) { const localize = useLocalize(); - const { showToast } = useToastContext(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); - const isDesktop = useMediaQuery('(min-width: 1024px)'); + const tableContainerRef = useRef(null); const scrollTimeoutRef = useRef(null); const scrollRAFRef = useRef(null); @@ -71,29 +66,49 @@ function DataTable, TValue>({ const [internalSorting, setInternalSorting] = useState(defaultSort); const [isScrollingFetching, setIsScrollingFetching] = useState(false); + const cleanupTimers = useCallback(() => { + if (scrollRAFRef.current) { + cancelAnimationFrame(scrollRAFRef.current); + scrollRAFRef.current = null; + } + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + scrollTimeoutRef.current = null; + } + }, []); + const debouncedTerm = useDebounced(searchTerm, debounceDelay); const finalSorting = sorting ?? internalSorting; - // Memoize column visibility calculations const calculatedVisibility = useMemo(() => { const newVisibility: VisibilityState = {}; - if (isSmallScreen) { - columns.forEach((col: ColumnDef & { meta?: { hideOnMobile?: boolean } }) => { - if (col.id && col.meta?.hideOnMobile) { - newVisibility[col.id] = false; - } - }); - } + columns.forEach((col) => { + const meta = (col as { meta?: { desktopOnly?: boolean } }).meta; + if (!meta?.desktopOnly) return; + + const rawId = + (col as { id?: string | number; accessorKey?: string | number }).id ?? + (col as { accessorKey?: string | number }).accessorKey; + + if ((typeof rawId === 'string' || typeof rawId === 'number') && String(rawId).length > 0) { + newVisibility[String(rawId)] = !isSmallScreen; + } else { + logger.warn( + 'DataTable: A desktopOnly column is missing id/accessorKey; cannot control header visibility automatically.', + col, + ); + } + }); return newVisibility; }, [isSmallScreen, columns]); useEffect(() => { - setColumnVisibility(calculatedVisibility); + setColumnVisibility((prev) => ({ ...prev, ...calculatedVisibility })); }, [calculatedVisibility]); const processedData = useMemo( () => - data.map((item, index) => { + data.map((item, index): ProcessedDataRow => { if (item.id === null || item.id === undefined) { logger.warn( 'DataTable Warning: A data row is missing a unique "id" property. Using index as a fallback. This can lead to unexpected behavior with selection and sorting.', @@ -110,46 +125,19 @@ function DataTable, TValue>({ [data], ); - // Enhanced columns with desktop-only cell rendering - const enhancedColumns = useMemo(() => { - return columns.map((col) => { - const originalCol = col as ColumnDef & { - meta?: { - hideOnMobile?: boolean; - desktopOnly?: boolean; - className?: string; - }; - }; - - if (originalCol.meta?.desktopOnly && originalCol.cell) { - const originalCell = originalCol.cell; - return { - ...originalCol, - cell: (props: CellContext) => { - if (!isDesktop) { - return null; - } - return typeof originalCell === 'function' ? originalCell(props) : originalCell; - }, - }; - } - return originalCol; - }); - }, [columns, isDesktop]); - - const tableColumns = useMemo(() => { + const tableColumns = useMemo((): TableColumnDef[] => { if (!enableRowSelection || !showCheckboxes) { - return enhancedColumns as ColumnDef[]; + return columns as TableColumnDef[]; } - const selectColumn: ColumnDef = { + const selectColumn: ColumnDef, TValue> = { id: 'select', header: ({ table }) => (
table.toggleAllRowsSelected(value)} - ariaLabel={localize('com_ui_select_all' as string)} + ariaLabel={localize('com_ui_select_all')} />
), @@ -165,15 +153,12 @@ function DataTable, TValue>({ meta: { className: 'w-12', }, - }; + } as TableColumnDef; - return [ - selectColumn, - ...(enhancedColumns as ColumnDef[]), - ] as ColumnDef[]; - }, [enhancedColumns, enableRowSelection, showCheckboxes, localize]); + return [selectColumn, ...(columns as TableColumnDef[])]; + }, [columns, enableRowSelection, showCheckboxes, localize]); - const table = useReactTable({ + const table = useReactTable>({ data: processedData, columns: tableColumns, getRowId: (row) => row._id, @@ -227,7 +212,6 @@ function DataTable, TValue>({ } }, [debouncedTerm, filterValue, onFilterChange, setOptimizedRowSelection]); - // Optimized scroll handler with RAF const handleScroll = useCallback(() => { if (scrollRAFRef.current !== null) { cancelAnimationFrame(scrollRAFRef.current); @@ -261,7 +245,7 @@ function DataTable, TValue>({ } scrollTimeoutRef.current = null; - }, 150); // Slightly increased debounce for better performance + }, 150); scrollRAFRef.current = null; }); @@ -274,14 +258,9 @@ function DataTable, TValue>({ scrollElement.addEventListener('scroll', handleScroll, { passive: true }); return () => { scrollElement.removeEventListener('scroll', handleScroll); - if (scrollTimeoutRef.current) { - clearTimeout(scrollTimeoutRef.current); - } - if (scrollRAFRef.current) { - cancelAnimationFrame(scrollRAFRef.current); - } + cleanupTimers(); }; - }, [handleScroll]); + }, [handleScroll, cleanupTimers]); const handleReset = useCallback(() => { setError(null); @@ -295,7 +274,7 @@ function DataTable, TValue>({

{error.message}

- +
); @@ -315,8 +294,7 @@ function DataTable, TValue>({ customActionsRenderer({ selectedCount, selectedRows: table.getSelectedRowModel().rows.map((r) => r.original), - table, - showToast, + table: table as unknown as TTable, })} @@ -330,11 +308,19 @@ function DataTable, TValue>({ } as React.CSSProperties } > - +
{headerGroups.map((headerGroup) => ( {headerGroup.headers.map((header) => { + const isDesktopOnly = + (header.column.columnDef.meta as { desktopOnly?: boolean } | undefined) + ?.desktopOnly ?? false; + + if (!header.column.getIsVisible() || (isSmallScreen && isDesktopOnly)) { + return null; + } + const isSelectHeader = header.id === 'select'; const meta = header.column.columnDef.meta as { className?: string } | undefined; return ( @@ -376,8 +362,7 @@ function DataTable, TValue>({ {showSkeletons ? ( >[]} /> ) : ( <> @@ -395,8 +380,8 @@ function DataTable, TValue>({ return ( } - columns={tableColumns} + row={row as Row>} + columns={tableColumns as ColumnDef>[]} index={virtualRow.index} virtualIndex={virtualRow.index} />