Customizes DataTable columns, badges, date formatting, and table display for existing entities (project)
From schema0-devnpx claudepluginhub schema0/ai-agent-plugins --plugin schema0-devThis skill is limited to using the following tools:
FILTER_EXAMPLE.mdPrerequisite: This skill requires a schema0 template project. Before using, ensure
CLAUDE.mdexists in the project root and read it for project rules and conventions.
Web only. This skill generates files into
apps/web/. Do NOT use ifapps/web/does not exist.
Create and customize DataTable columns. This skill generates the {Entity}Column.tsx file.
| File | Location |
|---|---|
| Columns | apps/web/src/components/ui/data-table/custom/{entity}/{Entity}Column.tsx |
| Index | apps/web/src/components/ui/data-table/custom/{entity}/index.ts |
flowchart TD
A[schema-gen] --> D[table-customization]
Prerequisite: Run schema-gen skill first to create the database schema.
import { createColumnHelper } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Edit, Trash2 } from "lucide-react";
import { useNavigate } from "react-router";
const columnHelper = createColumnHelper<any>();
export const {Entity}Column = [
columnHelper.accessor("name", {
header: "Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("status", {
header: "Status",
cell: (info) => (
<Badge variant={info.getValue() === "active" ? "default" : "secondary"}>
{info.getValue()}
</Badge>
),
}),
columnHelper.accessor("createdAt", {
header: "Created",
cell: (info) => new Date(info.getValue()).toLocaleDateString(),
}),
// ⚠️ CRITICAL: Pass row.original (full object), NOT row.original.id
// The list route's handleEdit/handleDeleteClick expect the full item object.
// ⚠️ CRITICAL: Do NOT use confirm() — use table.options.meta?.onDelete which opens an AlertDialog.
columnHelper.display({
id: "actions",
header: "Actions",
cell: ({ row, table }) => {
const onEdit = table.options.meta?.onUpdate as ((item: any) => void) | undefined;
const onDelete = table.options.meta?.onDelete as ((item: any) => void) | undefined;
return (
<div className="flex gap-2">
<Button variant="ghost" size="icon" onClick={() => onEdit?.(row.original)} aria-label="Edit">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => onDelete?.(row.original)} aria-label="Delete">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
);
},
}),
];
Create at: apps/web/src/components/ui/data-table/custom/{entity}/index.ts
export { {Entity}Column } from "./{Entity}Column";
export const {Entity}Column = [
// ... existing columns
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => {
const priority = row.getValue("priority") as string;
return (
<Badge variant={priority === "high" ? "destructive" : "secondary"}>
{priority}
</Badge>
);
},
},
];
{
accessorKey: "createdAt",
header: "Created",
cell: ({ row }) => {
const date = new Date(row.getValue("createdAt"));
return (
<div className="text-sm">
<div>{date.toLocaleDateString()}</div>
<div className="text-muted-foreground text-xs">{date.toLocaleTimeString()}</div>
</div>
);
},
}
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string;
const variants: Record<string, string> = {
active: "bg-green-100 text-green-800",
pending: "bg-yellow-100 text-yellow-800",
inactive: "bg-gray-100 text-gray-800",
};
return (
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
variants[status] || variants.inactive
}`}>
{status}
</span>
);
},
}
{
accessorKey: "dueDate",
header: "Due Date",
cell: ({ row }) => {
const dueDate = new Date(row.getValue("dueDate"));
const now = new Date();
const isOverdue = dueDate < now;
return (
<div className={isOverdue ? "text-red-600 font-semibold" : ""}>
{dueDate.toLocaleDateString()}
{isOverdue && " (Overdue)"}
</div>
);
},
}
{
accessorKey: "price",
header: "Price",
cell: ({ row }) => {
const price = row.getValue("price") as number;
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
},
}
{
accessorKey: "completed",
header: "Completed",
cell: ({ row }) => {
const completed = row.getValue("completed") as boolean;
return (
<Badge variant={completed ? "default" : "secondary"}>
{completed ? "Yes" : "No"}
</Badge>
);
},
}
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => {
const email = row.getValue("email") as string;
return (
<a href={`mailto:${email}`} className="text-blue-600 hover:underline">
{email}
</a>
);
},
}
{
accessorKey: "website",
header: "Website",
cell: ({ row }) => {
const url = row.getValue("website") as string;
return (
<a href={url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
{url}
</a>
);
},
}
{
accessorKey: "avatar",
header: "Avatar",
cell: ({ row }) => {
const avatar = row.getValue("avatar") as string;
return avatar ? (
<img src={avatar} alt="Avatar" className="w-8 h-8 rounded-full" />
) : (
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
{row.original.name?.charAt(0).toUpperCase()}
</div>
);
},
}
See the actions column in the Base Columns Pattern above. Key rules:
row.original (full object), NOT row.original.idtable.options.meta?.onDelete (opens AlertDialog) — NEVER confirm()aria-label="Edit" / aria-label="Delete" for test accessibilityany type in generated code — use proper types, generics, or unknown with type narrowing// @ts-ignore, // @ts-expect-error, // @ts-nocheck, or // eslint-disable — fix the type error insteadbunx oxlint --type-check --type-aware --quiet <your-column-file> to verify (only your files, not project-wide)