{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "data-table", "title": "Data Table", "description": "A data table built on TanStack Table with sorting, filtering, pagination, and skeleton loading.", "dependencies": [ "@tanstack/react-table", "lucide-react" ], "registryDependencies": [ "https://ds.formance.com/r/badge.json", "https://ds.formance.com/r/button.json", "https://ds.formance.com/r/command.json", "https://ds.formance.com/r/dropdown-menu.json", "https://ds.formance.com/r/popover.json", "https://ds.formance.com/r/select.json", "https://ds.formance.com/r/skeleton.json", "https://ds.formance.com/r/table.json" ], "files": [ { "path": "registry/default/ui-fragments/data-table.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { useEffect, useState } from 'react';\n\nimport {\n Column,\n ColumnDef,\n ColumnFiltersState,\n flexRender,\n getCoreRowModel,\n getFacetedRowModel,\n getFacetedUniqueValues,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n InitialTableState,\n Row,\n SortingState,\n Table as TanstackTable,\n useReactTable,\n VisibilityState,\n} from '@tanstack/react-table';\nimport {\n ArrowDown,\n ArrowUp,\n Check,\n ChevronDown,\n ChevronLeft,\n ChevronRight,\n CirclePlus,\n Ellipsis,\n EyeOff,\n Settings2,\n X,\n} from 'lucide-react';\n\nimport { CSSProperties } from 'react';\n\nimport { cn } from '@/lib/utils';\nimport { Badge, type TBadgeProps } from '@/registry/default/ui/badge';\nimport { Button } from '@/registry/default/ui/button';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from '@/registry/default/ui/command';\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/registry/default/ui/dropdown-menu';\nimport { Input } from '@/registry/default/ui/input';\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@/registry/default/ui/popover';\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@/registry/default/ui/select';\nimport { Separator } from '@/registry/default/ui/separator';\nimport { Skeleton } from '@/registry/default/ui/skeleton';\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@/registry/default/ui/table';\n\n// ---------------------------------------------------------------------------\n// Cell border styles (matches platform-ui column/row separators)\n// ---------------------------------------------------------------------------\n\nfunction getCellBorderStyles(\n column: Column,\n isLastRow: boolean\n): CSSProperties {\n const isFirstColumn = column.getIsFirstColumn();\n\n return {\n borderLeft: !isFirstColumn ? '1px solid var(--border)' : undefined,\n borderBottom: !isLastRow ? '1px solid var(--border)' : undefined,\n };\n}\n\n// ---------------------------------------------------------------------------\n// DataTableColumnHeader\n// ---------------------------------------------------------------------------\n\ntype TDataTableColumnHeaderProps =\n React.HTMLAttributes & {\n column: Column;\n title: string;\n };\n\nfunction DataTableColumnHeader({\n column,\n title,\n className,\n}: TDataTableColumnHeaderProps) {\n if (!column.getCanSort()) {\n return
{title}
;\n }\n\n return (\n
\n \n \n \n {title}\n {column.getIsSorted() === 'desc' ? (\n \n ) : column.getIsSorted() === 'asc' ? (\n \n ) : (\n \n )}\n \n \n \n column.toggleSorting(false)}>\n \n Asc\n \n column.toggleSorting(true)}>\n \n Desc\n \n \n column.toggleVisibility(false)}>\n \n Hide\n \n \n \n
\n );\n}\n\n// ---------------------------------------------------------------------------\n// DataTableFacetedFilter\n// ---------------------------------------------------------------------------\n\ntype TFacetedFilterOption = {\n label: string;\n value: string;\n icon?: React.ComponentType<{ className?: string }>;\n variant?: TBadgeProps['variant'];\n};\n\ntype TDataTableFacetedFilterProps = {\n column?: Column;\n title?: string;\n options: TFacetedFilterOption[];\n};\n\nfunction DataTableFacetedFilter({\n column,\n title,\n options,\n}: TDataTableFacetedFilterProps) {\n const facets = column?.getFacetedUniqueValues();\n const selectedValues = new Set(column?.getFilterValue() as string[]);\n\n return (\n \n \n \n \n {title}\n {selectedValues?.size > 0 && (\n <>\n \n
\n {selectedValues.size > 2 ? (\n \n {selectedValues.size} selected\n \n ) : (\n options\n .filter((option) => selectedValues.has(option.value))\n .map((option) => (\n \n {option.label}\n \n ))\n )}\n
\n \n )}\n \n
\n \n \n \n \n No results found.\n \n {options.map((option) => {\n const isSelected = selectedValues.has(option.value);\n\n return (\n {\n if (isSelected) {\n selectedValues.delete(option.value);\n } else {\n selectedValues.add(option.value);\n }\n const filterValues = Array.from(selectedValues);\n column?.setFilterValue(\n filterValues.length ? filterValues : undefined\n );\n }}\n >\n \n {isSelected && }\n \n {option.icon && (\n \n )}\n {option.label}\n {facets?.get(option.value) && (\n \n {facets.get(option.value)}\n \n )}\n \n );\n })}\n \n {selectedValues.size > 0 && (\n <>\n \n \n column?.setFilterValue(undefined)}\n className=\"justify-center text-center\"\n >\n Clear filters\n \n \n \n )}\n \n \n \n
\n );\n}\n\n// ---------------------------------------------------------------------------\n// DataTableViewOptions\n// ---------------------------------------------------------------------------\n\ntype TDataTableViewOptionsProps = {\n table: TanstackTable;\n};\n\nfunction DataTableViewOptions({\n table,\n}: TDataTableViewOptionsProps) {\n return (\n \n \n \n \n \n Toggle columns\n \n {table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== 'undefined' && column.getCanHide()\n )\n .map((column) => (\n column.toggleVisibility(!!value)}\n >\n {column.id}\n \n ))}\n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// DataTableToolbar\n// ---------------------------------------------------------------------------\n\ntype TDataTableToolbarProps = {\n table: TanstackTable;\n searchConfig?: {\n placeholder: string;\n columnKey: string;\n size?: 'sm' | 'md' | 'lg';\n }[];\n filtersConfig?: {\n title: string;\n column: string;\n options: TFacetedFilterOption[];\n }[];\n toolbarLeft?: React.ReactNode;\n toolbarRight?: React.ReactNode;\n};\n\nfunction DataTableToolbar({\n table,\n searchConfig,\n filtersConfig,\n toolbarLeft,\n toolbarRight,\n}: TDataTableToolbarProps) {\n const isFiltered = table.getState().columnFilters.length > 0;\n\n return (\n
\n
\n {toolbarLeft}\n {searchConfig?.map((search, index) => (\n \n table\n .getColumn(search.columnKey)\n ?.setFilterValue(event.target.value)\n }\n className={cn('', {\n 'w-28': search.size === 'sm',\n 'w-40 lg:w-52': search.size === 'md' || !search.size,\n 'w-56': search.size === 'lg',\n })}\n />\n ))}\n {filtersConfig?.map((filter, index) => {\n const col = table.getColumn(filter.column);\n if (!col) return null;\n\n return (\n \n );\n })}\n {isFiltered && (\n table.setColumnFilters([])}\n className=\"h-8 px-2 lg:px-3 ml-3\"\n >\n Reset\n \n \n )}\n
\n
\n {toolbarRight}\n \n
\n
\n );\n}\n\n// ---------------------------------------------------------------------------\n// DataTablePagination\n// ---------------------------------------------------------------------------\n\ntype TDataTablePaginationProps = {\n table: TanstackTable;\n};\n\nfunction DataTablePagination({\n table,\n}: TDataTablePaginationProps) {\n return (\n
\n
\n
\n

Rows per page

\n table.setPageSize(Number(value))}\n >\n \n \n \n \n {[5, 10, 15, 20, 30, 50].map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n
\n\n
\n Page {table.getState().pagination.pageIndex + 1} of{' '}\n {table.getPageCount()}\n
\n\n
\n table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n type=\"button\"\n >\n \n \n table.nextPage()}\n disabled={!table.getCanNextPage()}\n type=\"button\"\n >\n \n \n
\n
\n
\n );\n}\n\n// ---------------------------------------------------------------------------\n// DataTableRowActions\n// ---------------------------------------------------------------------------\n\ntype TDataTableRowActionsProps = {\n children: React.ReactNode;\n};\n\nfunction DataTableRowActions({ children }: TDataTableRowActionsProps) {\n return (\n \n \n \n \n Open menu\n \n \n {children}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// DataTableSkeleton\n// ---------------------------------------------------------------------------\n\ntype TDataTableSkeletonProps = React.HTMLAttributes & {\n columnCount: number;\n rowCount?: number;\n searchableColumnCount?: number;\n filterableColumnCount?: number;\n showViewOptions?: boolean;\n cellWidths?: string[];\n withHeader?: boolean;\n withPagination?: boolean;\n shrinkZero?: boolean;\n};\n\nfunction DataTableSkeleton({\n columnCount,\n rowCount = 10,\n searchableColumnCount = 0,\n filterableColumnCount = 0,\n showViewOptions = true,\n cellWidths = ['auto'],\n withHeader = true,\n withPagination = true,\n shrinkZero = false,\n className,\n ...props\n}: TDataTableSkeletonProps) {\n return (\n \n
\n
\n {searchableColumnCount > 0\n ? Array.from({ length: searchableColumnCount }).map((_, i) => (\n \n ))\n : null}\n {filterableColumnCount > 0\n ? Array.from({ length: filterableColumnCount }).map((_, i) => (\n \n ))\n : null}\n
\n {showViewOptions ? (\n
\n
\n \n {withHeader && (\n \n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n \n )}\n \n {Array.from({\n length: withHeader ? Math.max(0, rowCount - 1) : rowCount,\n }).map((_, i) => (\n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n ))}\n \n
\n
\n {withPagination ? (\n
\n \n
\n
\n \n \n
\n
\n \n
\n
\n \n \n
\n
\n
\n ) : null}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// DataTable\n// ---------------------------------------------------------------------------\n\ntype TDataTableProps = {\n columns: ColumnDef[];\n data: TData[];\n initialState?: InitialTableState;\n displayPagination?: boolean;\n emptyState?: React.ReactNode;\n renderExpandedRow?: (row: Row) => React.ReactNode;\n getRowCanExpand?: (row: Row) => boolean;\n // Passthrough to TanStack. Left `undefined` (its defaults) unless a consumer\n // opts out — e.g. to silence the StrictMode \"update on a not-yet-mounted\n // component\" dev warning caused by TanStack's microtask-queued auto-resets.\n autoResetPageIndex?: boolean;\n autoResetExpanded?: boolean;\n searchConfig?: TDataTableToolbarProps['searchConfig'];\n filtersConfig?: TDataTableToolbarProps['filtersConfig'];\n toolbarLeft?: React.ReactNode;\n toolbarRight?: React.ReactNode;\n hideToolbar?: boolean;\n onTableReady?: (table: TanstackTable) => void;\n};\n\nfunction DataTable({\n columns,\n data,\n searchConfig,\n filtersConfig,\n initialState,\n displayPagination = true,\n toolbarRight,\n toolbarLeft,\n emptyState,\n autoResetPageIndex,\n autoResetExpanded,\n hideToolbar = false,\n onTableReady,\n}: TDataTableProps) {\n const [columnVisibility, setColumnVisibility] = useState(\n initialState?.columnVisibility ?? {}\n );\n const [columnFilters, setColumnFilters] = useState(\n initialState?.columnFilters ?? []\n );\n const [sorting, setSorting] = useState([]);\n\n const table = useReactTable({\n data,\n columns,\n initialState,\n state: {\n sorting,\n columnVisibility,\n columnFilters,\n },\n enableRowSelection: true,\n autoResetPageIndex,\n autoResetExpanded,\n onSortingChange: setSorting,\n onColumnFiltersChange: setColumnFilters,\n onColumnVisibilityChange: setColumnVisibility,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFacetedRowModel: getFacetedRowModel(),\n getFacetedUniqueValues: getFacetedUniqueValues(),\n });\n\n const hasData = data.length > 0;\n\n useEffect(() => {\n onTableReady?.(table);\n }, [onTableReady, table]);\n\n return (\n
\n {!hideToolbar && (\n \n )}\n\n {hasData ? (\n
\n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n \n ))}\n \n ))}\n \n \n {table.getRowModel().rows?.length ? (\n table.getRowModel().rows.map((row, rowIndex) => {\n const isLastRow =\n rowIndex === table.getRowModel().rows.length - 1;\n\n return (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext()\n )}\n \n ))}\n \n );\n })\n ) : (\n \n \n No results.\n \n \n )}\n \n
\n
\n ) : emptyState ? (\n emptyState\n ) : (\n
\n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n \n ))}\n \n ))}\n \n \n \n \n No results.\n \n \n \n
\n
\n )}\n\n {displayPagination && }\n
\n );\n}\n\nexport {\n DataTable,\n DataTableColumnHeader,\n DataTableFacetedFilter,\n DataTablePagination,\n DataTableRowActions,\n DataTableSkeleton,\n DataTableToolbar,\n DataTableViewOptions,\n};\n\nexport type {\n TDataTableColumnHeaderProps,\n TDataTablePaginationProps,\n TDataTableProps,\n TDataTableRowActionsProps,\n TDataTableSkeletonProps,\n TDataTableToolbarProps,\n TDataTableViewOptionsProps,\n TFacetedFilterOption,\n};\n", "type": "registry:component", "target": "components/ui-fragments/data-table.tsx" } ], "type": "registry:component" }