Files
app/src/admin/ui/DataTable.tsx
BOHA 1398d4b4dc fix(mui): review fixes — Select string contract, DataTable aria-sort, sx/page-clamp/dev-warn
Select value/option restricted to string (MUI sets the literal child value; string|number + onChange:string was a type lie). DataTable sortDirection uses ?? 'asc' so aria-sort is never undefined on the active column. StatusChip sx-merge drops undefined; Pagination clamps page into range; DataTable dev-warns on sortKey without onSort; Alert onClose JSDoc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:34:15 +02:00

116 lines
3.5 KiB
TypeScript

import type { ReactNode } from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import TableSortLabel from "@mui/material/TableSortLabel";
export interface DataColumn<T> {
key: string;
header: string;
align?: "left" | "right";
/** Render the cell with the DM Mono numeric face. */
mono?: boolean;
/** Emphasize the cell (font-weight 500). */
bold?: boolean;
/** If set, the header becomes a clickable sort control (requires onSort). */
sortKey?: string;
render: (row: T) => ReactNode;
}
export interface DataTableProps<T> {
columns: DataColumn<T>[];
rows: T[];
rowKey: (row: T) => string | number;
rowInactive?: (row: T) => boolean;
empty?: ReactNode;
/** Server-sort wiring: active key (matches a column's sortKey), direction, handler. */
sortBy?: string;
sortDir?: "asc" | "desc";
onSort?: (key: string) => void;
}
/** Dense, Soft-SaaS-styled data table. Styling only — never re-formats values. */
export default function DataTable<T>({
columns,
rows,
rowKey,
rowInactive,
empty,
sortBy,
sortDir,
onSort,
}: DataTableProps<T>) {
if (import.meta.env.DEV && columns.some((c) => c.sortKey) && !onSort) {
console.warn("DataTable: columns with sortKey require an onSort prop.");
}
if (rows.length === 0 && empty) return <>{empty}</>;
return (
<TableContainer sx={{ borderRadius: 2 }}>
<Table size="small" sx={{ "& td, & th": { borderColor: "divider" } }}>
<TableHead>
<TableRow>
{columns.map((c) => (
<TableCell
key={c.key}
align={c.align}
sortDirection={
c.sortKey && sortBy === c.sortKey ? (sortDir ?? "asc") : false
}
sx={{
textTransform: "uppercase",
letterSpacing: ".05em",
fontSize: ".64rem",
fontWeight: 700,
color: "text.secondary",
}}
>
{c.sortKey && onSort ? (
<TableSortLabel
active={sortBy === c.sortKey}
direction={
sortBy === c.sortKey ? (sortDir ?? "asc") : "asc"
}
onClick={() => onSort(c.sortKey!)}
>
{c.header}
</TableSortLabel>
) : (
c.header
)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
key={rowKey(row)}
hover
sx={rowInactive?.(row) ? { opacity: 0.55 } : undefined}
>
{columns.map((c) => (
<TableCell
key={c.key}
align={c.align}
sx={{
fontSize: ".8rem",
...(c.bold ? { fontWeight: 500 } : {}),
...(c.mono
? { fontFamily: "'DM Mono', Menlo, monospace" }
: {}),
}}
>
{c.render(row)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}