{{#if framework == "nextjs"}} "use client"; {{/if}} import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { IconPlus } from "@tabler/icons-react"; import { type ColumnDef, type ColumnFiltersState, type ColumnSort, flexRender, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type OnChangeFn, type PaginationState, type Row, type SortingState, type Table as TableInstance, useReactTable, type VisibilityState, } from "@tanstack/react-table"; {{#if framework == "nextjs"}} import Link from "next/link"; import { useRouter } from "next/navigation"; {{else}} import { Link } from "react-router"; {{/if}} import * as React from "react"; import { toast } from "sonner"; import DataTableColumnSelector from "./data-table-column-selector"; import DataTableFooter from "./data-table-footer"; export interface BaseRecord { _id?: string | number; id?: string | number; [key: string]: unknown; } interface DataTableProps { data?: T[]; columns?: ColumnDef[]; pageSize?: number; pageIndex?: number; total?: number; uniqueIdProperty?: string; defaultSort?: ColumnSort[]; enableRowSelection?: boolean; onDeleteMany?: (ids: string[]) => Promise; actionLink?: { href: string; label: string }; actionModal?: { form: React.ReactNode; label: string; title?: string }; rightSite?: React.ReactNode; hasHeaderFooter?: boolean; } /** Unified way to read a record's unique id */ function getRowIdValue(row: T, uniqueIdKey: string) { const raw = row[uniqueIdKey] ?? row.id ?? row._id; return typeof raw === "string" || typeof raw === "number" ? String(raw) : ""; } export function DataTable({ data: initialData = [], columns = [], pageSize = 20, pageIndex = 1, total = 0, uniqueIdProperty = "id", defaultSort = [], enableRowSelection = true, onDeleteMany, actionLink, actionModal, rightSite, hasHeaderFooter = true, }: DataTableProps) { {{#if framework == "nextjs"}} const router = useRouter(); {{/if}} const [data, setData] = React.useState(initialData); const [isInitialLoad, setIsInitialLoad] = React.useState(true); const [isDialogOpen, setIsDialogOpen] = React.useState(false); const [rowSelection, setRowSelection] = React.useState({}); const [columnVisibility, setColumnVisibility] = React.useState({}); const [columnFilters, setColumnFilters] = React.useState( [], ); const [sorting, setSorting] = React.useState(defaultSort); const [pagination, setPagination] = React.useState({ pageIndex: Math.max(0, pageIndex - 1), pageSize, }); // skeleton only during page changes after initial load const [isTableLoading, setIsTableLoading] = React.useState(false); const prevPageIndex = React.useRef(pagination.pageIndex); const uniqueIdRef = React.useRef(uniqueIdProperty); uniqueIdRef.current = uniqueIdProperty; // eslint-disable-next-line react-hooks/incompatible-library const table = useReactTable({ data, columns, state: { sorting, columnVisibility, rowSelection, columnFilters, pagination, }, getRowId: (row) => getRowIdValue(row, uniqueIdRef.current), enableRowSelection, onRowSelectionChange: setRowSelection, onSortingChange: setSorting as OnChangeFn, onColumnFiltersChange: setColumnFilters as OnChangeFn, onColumnVisibilityChange: setColumnVisibility, onPaginationChange: setPagination, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), }); // Selected IDs derived from table state (lint-friendly deps) const selection = table.getState().rowSelection as Record; const selectedIds = React.useMemo( () => Object.keys(selection).filter((id) => selection[id]), [selection], ); // Refresh local data when server data changes React.useEffect(() => { setData(initialData); setIsInitialLoad(false); }, [initialData]); // Show skeleton when paginating (not on first paint) React.useEffect(() => { const changed = pagination.pageIndex !== prevPageIndex.current; if (!isInitialLoad && changed) { setIsTableLoading(true); prevPageIndex.current = pagination.pageIndex; } }, [pagination.pageIndex, isInitialLoad]); React.useEffect(() => { if (!isTableLoading) return; const t = setTimeout(() => setIsTableLoading(false), 400); return () => clearTimeout(t); }, [isTableLoading]); // Bulk delete const handleDeleteMany = React.useCallback(async () => { if (!onDeleteMany || selectedIds.length === 0) return; toast.promise( onDeleteMany(selectedIds).then(() => { table.resetRowSelection(); setIsDialogOpen(false); }), { loading: "Deleting selected items...", success: () => { {{#if framework == "nextjs"}} router.refresh(); {{/if}} return `Successfully deleted ${selectedIds.length} item${ selectedIds.length > 1 ? "s" : "" }`; }, error: (err) => (err?.message as string) || "Failed to delete items", }, ); {{#if framework != "nextjs"}} }, [onDeleteMany, selectedIds, table]); {{else}} }, [onDeleteMany, selectedIds, table, router]); {{/if}} return ( {/* Toolbar */}
{/* LEFT */}
{actionModal && ( {actionModal.title && ( {actionModal.title} )} {actionModal.form} )} {/* Bulk Delete */} {onDeleteMany && selectedIds.length > 0 && ( Delete Selected Items Are you sure you want to delete {selectedIds.length}{" "} selected item{selectedIds.length > 1 ? "s" : ""}? This action cannot be undone. )}
{/* RIGHT */}
{actionLink && ( )} {rightSite && rightSite}
{/* Table */}
{table.getHeaderGroups().map((hg) => ( {hg.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {isTableLoading ? ( Array.from({ length: pagination.pageSize }).map((_, i) => ( )) ) : data.length > 0 ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))} )) ) : ( No results. )}
); } /** Selection checkbox column */ // eslint-disable-next-line react-refresh/only-export-components export function createSelectionColumn() { return { id: "select", header: ({ table }: { table: TableInstance }) => (
table.toggleAllPageRowsSelected(!!v)} aria-label="Select all" className="border-border" />
), cell: ({ row }: { row: Row }) => (
row.toggleSelected(!!v)} aria-label="Select row" />
), enableSorting: false, enableHiding: false, } as ColumnDef; }