import type { ReactNode } from "react"; import { Children, createElement, isValidElement, useCallback } from "react"; import type { DataTableBaseProps, ExtractRecordPaths, HintedString, Identifier, RaRecord, SortPayload, } from "ra-core"; import { DataTableBase, DataTableRenderContext, FieldTitle, RecordContextProvider, useDataTableCallbacksContext, useDataTableConfigContext, useDataTableDataContext, useDataTableRenderContext, useDataTableSelectedIdsContext, useDataTableSortContext, useDataTableStoreContext, useGetPathForRecordCallback, useRecordContext, useResourceContext, useStore, useTranslate, useTranslateLabel, } from "ra-core"; import { useNavigate } from "react-router"; import { ArrowDownAZ, ArrowUpZA } from "lucide-react"; import get from "lodash/get"; import { cn } from "@/lib/utils"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { ColumnsSelector, ColumnsSelectorItem, } from "@/components/admin/columns-button"; import { NumberField } from "@/components/admin/number-field"; import { BulkActionsToolbar, BulkActionsToolbarChildren, } from "@/components/admin/bulk-actions-toolbar"; const defaultBulkActionButtons = ; /** * A powerful data table with sorting, selection, and column customization. * * Displays records in a table with built-in support for column sorting, bulk selection, row clicks, * and column visibility controls. Use DataTable.Col to define columns. * * @see {@link https://marmelab.com/shadcn-admin-kit/docs/datatable/ DataTable documentation} * * @example * import { List, DataTable, ReferenceField, EditButton } from '@/components/admin'; * * export const PostList = () => ( * * * * * * * * * * * * * ); */ export function DataTable( props: DataTableProps, ) { const { children, className, rowClassName, bulkActionButtons = defaultBulkActionButtons, bulkActionsToolbar, ...rest } = props; const hasBulkActions = !!bulkActionsToolbar || bulkActionButtons !== false; const resourceFromContext = useResourceContext(props); const storeKey = props.storeKey || `${resourceFromContext}.datatable`; const [columnRanks] = useStore(`${storeKey}_columnRanks`); const columns = columnRanks ? reorderChildren(children, columnRanks) : children; return ( hasBulkActions={hasBulkActions} loading={null} empty={} {...rest} >
{columns} rowClassName={rowClassName}> {columns}
{bulkActionsToolbar ?? (bulkActionButtons !== false && ( {isValidElement(bulkActionButtons) ? bulkActionButtons : defaultBulkActionButtons} ))} {children} ); } DataTable.Col = DataTableColumn; DataTable.NumberCol = DataTableNumberColumn; const DataTableHead = ({ children }: { children: ReactNode }) => { const data = useDataTableDataContext(); const { hasBulkActions = false } = useDataTableConfigContext(); const { onSelect } = useDataTableCallbacksContext(); const selectedIds = useDataTableSelectedIdsContext(); const handleToggleSelectAll = (checked: boolean) => { if (!onSelect || !data || !selectedIds) return; onSelect( checked ? selectedIds.concat( data .filter((record) => !selectedIds.includes(record.id)) .map((record) => record.id), ) : [], ); }; const selectableIds = Array.isArray(data) ? data.map((record) => record.id) : []; return ( {hasBulkActions ? ( 0 && selectableIds.length > 0 && selectableIds.every((id) => selectedIds.includes(id)) } className="mb-2" /> ) : null} {children} ); }; const DataTableBody = ({ children, rowClassName, }: { children: ReactNode; rowClassName?: (record: RecordType) => string | undefined; }) => { const data = useDataTableDataContext(); return ( {data?.map((record, rowIndex) => ( {children} ))} ); }; const DataTableRow = ({ children, className, }: { children: ReactNode; className?: string; }) => { const { rowClick, handleToggleItem } = useDataTableCallbacksContext(); const selectedIds = useDataTableSelectedIdsContext(); const { hasBulkActions = false } = useDataTableConfigContext(); const record = useRecordContext(); if (!record) { throw new Error("DataTableRow can only be used within a RecordContext"); } const resource = useResourceContext(); if (!resource) { throw new Error("DataTableRow can only be used within a ResourceContext"); } const navigate = useNavigate(); const getPathForRecord = useGetPathForRecordCallback(); const handleToggle = useCallback( (event: React.MouseEvent) => { event.stopPropagation(); if (!handleToggleItem) return; handleToggleItem(record.id, event); }, [handleToggleItem, record.id], ); const handleClick = useCallback(async () => { const temporaryLink = typeof rowClick === "function" ? rowClick(record.id, resource, record) : rowClick; const link = isPromise(temporaryLink) ? await temporaryLink : temporaryLink; const path = await getPathForRecord({ record, resource, link, }); if (path === false || path == null) { return; } navigate(path, { state: { _scrollToTop: true }, }); }, [record, resource, rowClick, navigate, getPathForRecord]); return ( {hasBulkActions ? (
) : null} {children}
); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const isPromise = (value: any): value is Promise => value && typeof value.then === "function"; const DataTableEmpty = () => { const translate = useTranslate(); return ( {translate("ra.navigation.no_results", { _: "No results found." })} ); }; export interface DataTableProps extends Partial> { children: ReactNode; className?: string; rowClassName?: (record: RecordType) => string | undefined; bulkActionButtons?: ReactNode; bulkActionsToolbar?: ReactNode; } export function DataTableColumn< RecordType extends RaRecord = RaRecord, >(props: DataTableColumnProps) { const renderContext = useDataTableRenderContext(); switch (renderContext) { case "columnsSelector": return {...props} />; case "header": return ; case "data": return ; } } /** * Reorder children based on columnRanks * * Note that columnRanks may be shorter than the number of children */ const reorderChildren = (children: ReactNode, columnRanks: number[]) => Children.toArray(children).reduce((acc: ReactNode[], child, index) => { const rank = columnRanks.indexOf(index); if (rank === -1) { // if the column is not in columnRanks, keep it at the same index acc[index] = child; } else { // if the column is in columnRanks, move it to the rank index acc[rank] = child; } return acc; }, []); function DataTableHeadCell< RecordType extends RaRecord = RaRecord, >(props: DataTableColumnProps) { const { disableSort, source, label, sortByOrder, className, headerClassName, } = props; const sort = useDataTableSortContext(); const { handleSort } = useDataTableCallbacksContext(); const resource = useResourceContext(); const translate = useTranslate(); const translateLabel = useTranslateLabel(); const { storeKey, defaultHiddenColumns } = useDataTableStoreContext(); const [hiddenColumns] = useStore(storeKey, defaultHiddenColumns); const isColumnHidden = hiddenColumns.includes(source!); if (isColumnHidden) return null; const nextSortOrder = sort && sort.field === source ? oppositeOrder[sort.order] : (sortByOrder ?? "ASC"); const fieldLabel = translateLabel({ label: typeof label === "string" ? label : undefined, resource, source, }); const sortLabel = translate("ra.sort.sort_by", { field: fieldLabel, field_lower_first: typeof fieldLabel === "string" ? fieldLabel.charAt(0).toLowerCase() + fieldLabel.slice(1) : undefined, order: translate(`ra.sort.${nextSortOrder}`), _: translate("ra.action.sort"), }); return ( {handleSort && sort && !disableSort && source ? (

{sortLabel}

) : ( )}
); } const oppositeOrder: Record = { ASC: "DESC", DESC: "ASC", }; function DataTableCell< RecordType extends RaRecord = RaRecord, >(props: DataTableColumnProps) { const { children, render, field, source, className, cellClassName, conditionalClassName, } = props; const { storeKey, defaultHiddenColumns } = useDataTableStoreContext(); const [hiddenColumns] = useStore(storeKey, defaultHiddenColumns); const record = useRecordContext(); const isColumnHidden = hiddenColumns.includes(source!); if (isColumnHidden) return null; if (!render && !field && !children && !source) { throw new Error( "DataTableColumn: Missing at least one of the following props: render, field, children, or source", ); } return ( {children ?? (render ? record && render(record) : field ? createElement(field, { source }) : get(record, source!))} ); } export interface DataTableColumnProps< RecordType extends RaRecord = RaRecord, > { className?: string; cellClassName?: string; headerClassName?: string; conditionalClassName?: (record: RecordType) => string | false | undefined; children?: ReactNode; render?: (record: RecordType) => React.ReactNode; field?: React.ElementType; source?: NoInfer>>; label?: React.ReactNode; disableSort?: boolean; sortByOrder?: SortPayload["order"]; } export function DataTableNumberColumn< RecordType extends RaRecord = RaRecord, >(props: DataTableNumberColumnProps) { const { source, options, locales, className, headerClassName, cellClassName, ...rest } = props; return ( ); } export interface DataTableNumberColumnProps< RecordType extends RaRecord = RaRecord, > extends DataTableColumnProps { source: NoInfer>>; locales?: string | string[]; options?: Intl.NumberFormatOptions; }