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;
}