metis-desk/packages/rdx-ui/src/components/entity-table-content.tsx
2026-10-10 13:32:40 +02:00

129 lines
4.0 KiB
TypeScript

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@repo/shadcn-ui/components/table";
import type { Table as TableModel } from "@tanstack/react-table";
import type { EntityColumn } from "./entity-list";
export function EntityTableContent<T extends { id: string }>({
table,
columns,
title,
loading,
unavailable,
message,
rowClassName,
onSort,
}: {
table: TableModel<T>;
columns: readonly EntityColumn<T>[];
title: string;
loading: boolean;
unavailable: boolean;
message: string;
rowClassName: ((item: T) => string) | undefined;
onSort: () => void;
}) {
const visibleColumns = [
...table.getCenterVisibleLeafColumns(),
...table.getRightVisibleLeafColumns(),
];
const total = table.getFilteredRowModel().rows.length;
return (
<Table aria-label={title}>
<TableHeader>
<TableRow>
{visibleColumns.map((column) => {
const direction = column.getIsSorted();
return (
<TableHead
aria-sort={
direction === "asc"
? "ascending"
: direction === "desc"
? "descending"
: undefined
}
className={entityColumnClass(column.getIsPinned() === "right", true)}
data-pinned={column.getIsPinned() || undefined}
key={column.id}
scope="col"
>
{column.getCanSort() ? (
<button
className="inline-flex items-center gap-2 rounded py-1 text-left focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => {
column.toggleSorting();
onSort();
}}
type="button"
>
{String(column.columnDef.header)}
<span aria-hidden="true">
{direction === "asc" ? "↑" : direction === "desc" ? "↓" : "↕"}
</span>
</button>
) : (
String(column.columnDef.header)
)}
</TableHead>
);
})}
</TableRow>
</TableHeader>
<TableBody>
{loading || unavailable ? (
<TableRow>
<TableCell colSpan={visibleColumns.length}>
<span className="text-muted-foreground">{message}</span>
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow
className={`group/entity-row ${rowClassName?.(row.original) ?? ""}`}
key={row.id}
>
{visibleColumns.map((column) => (
<TableCell
className={entityColumnClass(column.getIsPinned() === "right", false)}
data-pinned={column.getIsPinned() || undefined}
key={column.id}
>
{columns.find((item) => item.id === column.id)?.render(row.original)}
</TableCell>
))}
</TableRow>
))
)}
{!(loading || unavailable || total) && (
<TableRow>
<TableCell className="p-6 text-muted-foreground" colSpan={visibleColumns.length}>
{message}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
);
}
function entityColumnClass(pinned: boolean, header: boolean) {
if (pinned) {
return header
? "sticky right-0 z-20 w-20 min-w-20 bg-card pr-4 text-right"
: "sticky right-0 z-10 h-10 w-20 min-w-20 bg-card px-3 py-3 pr-4 text-right whitespace-nowrap group-hover/entity-row:bg-[color-mix(in_oklch,var(--muted)_50%,var(--card))] group-has-aria-expanded/entity-row:bg-[color-mix(in_oklch,var(--muted)_50%,var(--card))]";
}
return header
? "first:min-w-40 bg-background font-medium"
: "h-14 first:min-w-40 max-w-lg whitespace-normal break-words px-3 py-3";
}