"use client"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable, getPaginationRowModel, SortingState, getSortedRowModel, ColumnFiltersState, getFilteredRowModel, VisibilityState } from "@tanstack/react-table"; // Extended ColumnDef type that includes optional friendlyName for column visibility dropdown export type ExtendedColumnDef = ColumnDef & { friendlyName?: string; }; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Button } from "@app/components/ui/button"; import { useEffect, useMemo, useState } from "react"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "@app/components/DataTablePagination"; import { Plus, Search, RefreshCw, Columns } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@app/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs"; import { useTranslations } from "next-intl"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; const STORAGE_KEYS = { PAGE_SIZE: "datatable-page-size", COLUMN_VISIBILITY: "datatable-column-visibility", getTablePageSize: (tableId?: string) => tableId ? `${tableId}-size` : STORAGE_KEYS.PAGE_SIZE, getTableColumnVisibility: (tableId?: string) => tableId ? `${tableId}-column-visibility` : STORAGE_KEYS.COLUMN_VISIBILITY }; const getStoredPageSize = (tableId?: string, defaultSize = 20): number => { if (typeof window === "undefined") return defaultSize; try { const key = STORAGE_KEYS.getTablePageSize(tableId); const stored = localStorage.getItem(key); if (stored) { const parsed = parseInt(stored, 10); // Validate that it's a reasonable page size if (parsed > 0 && parsed <= 1000) { return parsed; } } } catch (error) { console.warn("Failed to read page size from localStorage:", error); } return defaultSize; }; const setStoredPageSize = (pageSize: number, tableId?: string): void => { if (typeof window === "undefined") return; try { const key = STORAGE_KEYS.getTablePageSize(tableId); localStorage.setItem(key, pageSize.toString()); } catch (error) { console.warn("Failed to save page size to localStorage:", error); } }; const getStoredColumnVisibility = ( tableId?: string, defaultVisibility?: Record ): Record => { if (typeof window === "undefined") return defaultVisibility || {}; try { const key = STORAGE_KEYS.getTableColumnVisibility(tableId); const stored = localStorage.getItem(key); if (stored) { const parsed = JSON.parse(stored); // Validate that it's an object if (typeof parsed === "object" && parsed !== null) { return parsed; } } } catch (error) { console.warn( "Failed to read column visibility from localStorage:", error ); } return defaultVisibility || {}; }; const setStoredColumnVisibility = ( visibility: Record, tableId?: string ): void => { if (typeof window === "undefined") return; try { const key = STORAGE_KEYS.getTableColumnVisibility(tableId); localStorage.setItem(key, JSON.stringify(visibility)); } catch (error) { console.warn( "Failed to save column visibility to localStorage:", error ); } }; type TabFilter = { id: string; label: string; filterFn: (row: any) => boolean; }; type DataTableProps = { columns: ExtendedColumnDef[]; data: TData[]; title?: string; addButtonText?: string; onAdd?: () => void; onRefresh?: () => void; isRefreshing?: boolean; searchPlaceholder?: string; searchColumn?: string; defaultSort?: { id: string; desc: boolean; }; tabs?: TabFilter[]; defaultTab?: string; persistPageSize?: boolean | string; defaultPageSize?: number; columnVisibility?: Record; enableColumnVisibility?: boolean; persistColumnVisibility?: boolean | string; stickyLeftColumn?: string; // Column ID or accessorKey for left sticky column stickyRightColumn?: string; // Column ID or accessorKey for right sticky column (typically "actions") }; export function DataTable({ columns, data, title, addButtonText, onAdd, onRefresh, isRefreshing, searchPlaceholder = "Search...", searchColumn = "name", defaultSort, tabs, defaultTab, persistPageSize = false, defaultPageSize = 20, columnVisibility: defaultColumnVisibility, enableColumnVisibility = false, persistColumnVisibility = false, stickyLeftColumn, stickyRightColumn }: DataTableProps) { const t = useTranslations(); // Determine table identifier for storage // Use persistPageSize string if provided, otherwise use persistColumnVisibility string, otherwise undefined const tableId = typeof persistPageSize === "string" ? persistPageSize : typeof persistColumnVisibility === "string" ? persistColumnVisibility : undefined; // Compute initial column visibility (from localStorage if enabled, otherwise from prop/default) const initialColumnVisibility = (() => { if (persistColumnVisibility) { return getStoredColumnVisibility( tableId, defaultColumnVisibility ); } return defaultColumnVisibility || {}; })(); // Initialize page size from storage or default const [pageSize, setPageSize] = useState(() => { if (persistPageSize) { return getStoredPageSize(tableId, defaultPageSize); } return defaultPageSize; }); const [sorting, setSorting] = useState( defaultSort ? [defaultSort] : [] ); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState([]); const [columnVisibility, setColumnVisibility] = useState(initialColumnVisibility); const [activeTab, setActiveTab] = useState( defaultTab || tabs?.[0]?.id || "" ); // Apply tab filter to data const filteredData = useMemo(() => { if (!tabs || activeTab === "") { return data; } const activeTabFilter = tabs.find((tab) => tab.id === activeTab); if (!activeTabFilter) { return data; } return data.filter(activeTabFilter.filterFn); }, [data, tabs, activeTab]); const table = useReactTable({ data: filteredData, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), onGlobalFilterChange: setGlobalFilter, onColumnVisibilityChange: setColumnVisibility, initialState: { pagination: { pageSize: pageSize, pageIndex: 0 }, columnVisibility: initialColumnVisibility }, state: { sorting, columnFilters, globalFilter, columnVisibility } }); useEffect(() => { const currentPageSize = table.getState().pagination.pageSize; if (currentPageSize !== pageSize) { table.setPageSize(pageSize); // Persist to localStorage if enabled if (persistPageSize) { setStoredPageSize(pageSize, tableId); } } }, [pageSize, table, persistPageSize, tableId]); useEffect(() => { // Persist column visibility to localStorage when it changes if (persistColumnVisibility) { setStoredColumnVisibility(columnVisibility, tableId); } }, [columnVisibility, persistColumnVisibility, tableId]); const handleTabChange = (value: string) => { setActiveTab(value); // Reset to first page when changing tabs table.setPageIndex(0); }; // Enhanced pagination component that updates our local state const handlePageSizeChange = (newPageSize: number) => { setPageSize(newPageSize); table.setPageSize(newPageSize); // Persist immediately when changed if (persistPageSize) { setStoredPageSize(newPageSize, tableId); } }; // Helper function to check if a column should be sticky const isStickyColumn = (columnId: string | undefined, accessorKey: string | undefined, position: "left" | "right"): boolean => { if (position === "left" && stickyLeftColumn) { return columnId === stickyLeftColumn || accessorKey === stickyLeftColumn; } if (position === "right" && stickyRightColumn) { return columnId === stickyRightColumn || accessorKey === stickyRightColumn; } return false; }; // Get sticky column classes const getStickyClasses = (columnId: string | undefined, accessorKey: string | undefined): string => { if (isStickyColumn(columnId, accessorKey, "left")) { return "md:sticky md:left-0 z-10 bg-card"; } if (isStickyColumn(columnId, accessorKey, "right")) { return "sticky right-0 z-10 w-auto min-w-fit bg-card"; } return ""; }; return (
table.setGlobalFilter( String(e.target.value) ) } className="w-full pl-8" />
{tabs && tabs.length > 0 && ( {tabs.map((tab) => ( {tab.label} ( {data.filter(tab.filterFn).length}) ))} )}
{onRefresh && (
)} {onAdd && addButtonText && (
)}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const columnId = header.column.id; const accessorKey = (header.column.columnDef as any).accessorKey as string | undefined; const stickyClasses = getStickyClasses(columnId, accessorKey); const isRightSticky = isStickyColumn(columnId, accessorKey, "right"); const hasHideableColumns = enableColumnVisibility && table.getAllColumns().some((col) => col.getCanHide()); return ( {header.isPlaceholder ? null : ( isRightSticky && hasHideableColumns ? (
{t("toggleColumns") || "Toggle columns"} {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { const columnDef = column.columnDef as any; const friendlyName = columnDef.friendlyName; const displayName = friendlyName || (typeof columnDef.header === "string" ? columnDef.header : column.id); return ( column.toggleVisibility(!!value) } > {displayName} ); })}
{flexRender( header.column.columnDef.header, header.getContext() )}
) : ( flexRender( header.column.columnDef.header, header.getContext() ) ) )}
); })}
))}
{table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => { const columnId = cell.column.id; const accessorKey = (cell.column.columnDef as any).accessorKey as string | undefined; const stickyClasses = getStickyClasses(columnId, accessorKey); const isRightSticky = isStickyColumn(columnId, accessorKey, "right"); return ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ); })} )) ) : ( No results found. )}
); }