UI recipesUI recipes
Case board
The open cases in stage columns, reported to verifying; dragging a card moves the case to that stage of its kind's pipeline.
It loads its data on the server through the block's client; the cases block must be installed (npx better-supabase add cases). Its page on bettersupabase.com is /recipes/case-board.
Web, with shadcn components:
npx shadcn add badge card @reui/kanban/**
* The organization's open cases in a column for their stage. Dragging a card to
* another column moves the case there; a failed move puts it back. The parent
* keys it by the cards' stages, so a refresh starts it from the saved board.
*/
export function CaseBoard({
data,
canMove,
}: {
data: CaseBoardData;
canMove: boolean;
}) {
const errorMessage = useErrorMessage();
const label = useStageLabel();
const [columns, setColumns] = useState(() => columnsOf(data.cards));
const move = useAction(moveCase);
const byId = new Map(data.cards.map((card) => [card.id, card]));
return (
<div data-recipe="case-board">
<Kanban
value={columns}
onValueChange={setColumns}
getItemValue={(card) => card.id}
onValueCommit={(_value, meta) => {
if (meta.kind !== "item") {
return;
}
const from = v.parse(StageSchema, meta.activeContainer);
const to = v.parse(StageSchema, meta.overContainer);
const card = byId.get(String(meta.event.active.id));
const stageId =
card === undefined ? undefined : data.stages[card.pipelineId]?.[to];
if (from === to || card === undefined || stageId === undefined) {
setColumns(meta.previousValue);
return;
}
void move.run({ caseId: card.id, stageId }).then((result) => {
if (!result.ok) {
setColumns(meta.previousValue);
toast.error(errorMessage(result.error));
}
return result;
});
}}
>
<KanbanBoard className="flex overflow-x-auto">
{OPEN_STAGES.map((stage) => {
const column = columns[stage] ?? [];
return (
<KanbanColumn
key={stage}
value={stage}
className="w-72 shrink-0 gap-3 rounded-lg bg-muted/50 p-3"
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium">{label(stage)}</span>
<Badge variant="outline">{column.length}</Badge>
</div>
<KanbanColumnContent value={stage} className="min-h-16">
{column.map((card) => (
<KanbanItem
key={card.id}
value={card.id}
disabled={!canMove || move.pending}
>
<KanbanItemHandle cursor={canMove}>
<CaseItem card={card} />
</KanbanItemHandle>
</KanbanItem>
))}
</KanbanColumnContent>
</KanbanColumn>
);
})}
</KanbanBoard>
<KanbanOverlay>
{({ value, variant }) => {
const card =
variant === "item" ? byId.get(String(value)) : undefined;
return card ? <CaseItem card={card} /> : null;
}}
</KanbanOverlay>
</Kanban>
</div>
);
}Last updated on