"use client"; import { ColumnDef, ColumnFiltersState, flexRender, getCoreRowModel, PaginationState, useReactTable } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { DataTablePagination } from "@app/components/DataTablePagination"; import { Button } from "@app/components/ui/button"; import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Input } from "@app/components/ui/input"; import { useStoredColumnVisibility } from "@app/hooks/useStoredColumnVisibility"; import { Columns, Filter, Plus, RefreshCw, Search } from "lucide-react"; import { useTranslations } from "next-intl"; import { useMemo, useState } from "react"; // Extended ColumnDef type that includes optional friendlyName for column visibility dropdown export type ExtendedColumnDef = ColumnDef< TData, TValue > & { friendlyName?: string; }; type FilterOption = { id: string; label: string; value: string; }; type DataTableFilter = { id: string; label: string; options: FilterOption[]; multiSelect?: boolean; onValueChange: (selectedValues: string[]) => void; values?: string[]; displayMode?: "label" | "calculated"; // How to display the filter button text }; export type DataTablePaginationUpdateFn = (newPage: PaginationState) => void; type ControlledDataTableProps = { columns: ExtendedColumnDef[]; rows: TData[]; tableId: string; addButtonText?: string; onAdd?: () => void; onRefresh?: () => void; isRefreshing?: boolean; isNavigatingToAddPage?: boolean; searchPlaceholder?: string; filters?: DataTableFilter[]; filterDisplayMode?: "label" | "calculated"; // Global filter display mode (can be overridden per filter) columnVisibility?: Record; enableColumnVisibility?: boolean; onSearch?: (input: string) => void; searchQuery?: string; onPaginationChange: DataTablePaginationUpdateFn; stickyLeftColumn?: string; // Column ID or accessorKey for left sticky column stickyRightColumn?: string; // Column ID or accessorKey for right sticky column (typically "actions") rowCount: number; pagination: PaginationState; }; export function ControlledDataTable({ columns, rows, addButtonText, onAdd, onRefresh, isRefreshing, searchPlaceholder = "Search...", filters, filterDisplayMode = "label", columnVisibility: defaultColumnVisibility, enableColumnVisibility = false, tableId, pagination, stickyLeftColumn, onSearch, searchQuery, onPaginationChange, stickyRightColumn, rowCount, isNavigatingToAddPage }: ControlledDataTableProps) { const t = useTranslations(); const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useStoredColumnVisibility( tableId, defaultColumnVisibility ); // TODO: filters const activeFilters = useMemo(() => { const initial: Record = {}; filters?.forEach((filter) => { initial[filter.id] = filter.values || []; }); return initial; }, [filters]); const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowModel(), // getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onPaginationChange: (state) => { const newState = typeof state === "function" ? state(pagination) : state; onPaginationChange(newState); }, manualFiltering: true, manualPagination: true, rowCount, state: { columnFilters, columnVisibility, pagination } }); // Calculate display text for a filter based on selected values const getFilterDisplayText = (filter: DataTableFilter): string => { const selectedValues = activeFilters[filter.id] || []; if (selectedValues.length === 0) { return filter.label; } const selectedOptions = filter.options.filter((option) => selectedValues.includes(option.value) ); if (selectedOptions.length === 0) { return filter.label; } if (selectedOptions.length === 1) { return selectedOptions[0].label; } // Multiple selections: always join with "and" return selectedOptions.map((opt) => opt.label).join(" or "); }; const handleFilterChange = ( filterId: string, optionValue: string, checked: boolean ) => { const currentValues = activeFilters[filterId] || []; const filter = filters?.find((f) => f.id === filterId); if (!filter) return; let newValues: string[]; if (filter.multiSelect) { // Multi-select: add or remove the value if (checked) { newValues = [...currentValues, optionValue]; } else { newValues = currentValues.filter((v) => v !== optionValue); } } else { // Single-select: replace the value newValues = checked ? [optionValue] : []; } filter.onValueChange(newValues); }; // 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 [mask-image:linear-gradient(to_left,transparent_0%,black_20px)]"; } if (isStickyColumn(columnId, accessorKey, "right")) { return "sticky right-0 z-10 w-auto min-w-fit bg-card [mask-image:linear-gradient(to_right,transparent_0%,black_20px)]"; } return ""; }; return (
{onSearch && (
onSearch(e.currentTarget.value) } className="w-full pl-8" />
)} {filters && filters.length > 0 && (
{filters.map((filter) => { const selectedValues = activeFilters[filter.id] || []; const hasActiveFilters = selectedValues.length > 0; const displayMode = filter.displayMode || filterDisplayMode; const displayText = displayMode === "calculated" ? getFilterDisplayText(filter) : filter.label; return ( {filter.label} {filter.options.map( (option) => { const isChecked = selectedValues.includes( option.value ); return ( { handleFilterChange( filter.id, option.value, checked ); }} onSelect={(e) => e.preventDefault() } > {option.label} ); } )} ); })}
)}
{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 ) } onSelect={( e ) => e.preventDefault() } > { 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. )}
{rowCount > 0 && ( onPaginationChange({ ...pagination, pageSize }) } onPageChange={(pageIndex) => { onPaginationChange({ ...pagination, pageIndex }); }} isServerPagination pageSize={pagination.pageSize} pageIndex={pagination.pageIndex} /> )}
); }