UI recipesUI recipes
Case detail
One case with its severity, root cause, what still blocks closing it and its actions, and a Close button once nothing blocks it.
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-detail.
Web, with shadcn components:
npx shadcn add alert badge button empty @reui/frame/**
* One case with what still stops it from closing and its actions. Close is
* enabled once nothing blocks it; the database checks again when it runs.
*/
export function CaseDetail({
detail,
canClose,
}: {
detail: CaseDetailView;
canClose: boolean;
}) {
const t = useExtracted("cases");
const format = useFormatter();
const errorMessage = useErrorMessage();
const blocker = useBlockerLabel();
const actionType = useActionTypeLabel();
const severity = useSeverityLabel();
const close = useAction(closeCase, {
onSuccess: ({ number }) => {
toast.success(t("Closed {number}", { number }));
},
onError: (error) => {
toast.error(errorMessage(error));
},
});
const open = detail.status === "open";
return (
<Frame spacing="sm" data-recipe="case-detail">
<FrameHeader className="gap-0.5">
<FrameTitle>
{t("{number}: {title}", {
number: detail.number,
title: detail.title,
})}
</FrameTitle>
<FrameDescription>
{detail.stage === null
? detail.kind
: t("{kind} in {stage}", {
kind: detail.kind,
stage: detail.stage,
})}
</FrameDescription>
</FrameHeader>
<FramePanel className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-2">
{detail.severity === null ? null : (
<Badge variant={severityVariant(detail.severity)}>
{severity(detail.severity)}
</Badge>
)}
{detail.effectivenessDueOn === null ? null : (
<Badge variant="info-light">
{t("Effectiveness check due {date}", {
date: format.dateTime(new Date(detail.effectivenessDueOn), {
dateStyle: "medium",
timeZone: "UTC",
}),
})}
</Badge>
)}
</div>
<p className="text-sm">
{detail.rootCause ?? (
<span className="text-muted-foreground">
{t("No root cause recorded yet.")}
</span>
)}
</p>
{open && detail.blockers.length > 0 ? (
<Alert variant="warning">
<CircleDotIcon />
<AlertTitle>{t("Before this case can close")}</AlertTitle>
<AlertDescription>
<ul className="flex list-disc flex-col gap-1 pl-4">
{detail.blockers.map((code) => (
<li key={code}>{blocker(code)}</li>
))}
</ul>
</AlertDescription>
</Alert>
) : open ? (
<Alert variant="success">
<CircleCheckIcon />
<AlertTitle>{t("Nothing blocks closing this case")}</AlertTitle>
</Alert>
) : null}
{detail.actions.length === 0 ? (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<ListChecksIcon />
</EmptyMedia>
<EmptyTitle>{t("No actions yet")}</EmptyTitle>
<EmptyDescription>
{t(
"Containment and corrective actions are tasks; add them from the case's work items.",
)}
</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
<ul className="flex flex-col gap-2">
{detail.actions.map((action) => (
<li
key={action.id}
className="flex items-center justify-between gap-2 text-sm"
>
<span className="flex items-center gap-2">
<Badge variant="outline">{actionType(action.type)}</Badge>
{action.title}
</span>
{action.done ? (
<Badge variant="success-light">{t("Done")}</Badge>
) : (
<Badge variant="secondary">
{action.required ? t("Open, required") : t("Open")}
</Badge>
)}
</li>
))}
</ul>
)}
</FramePanel>
{open && canClose ? (
<FrameFooter>
<Button
type="button"
size="sm"
disabled={detail.blockers.length > 0 || close.pending}
onClick={() => {
void close.run({ caseId: detail.id });
}}
>
{t("Close case")}
</Button>
</FrameFooter>
) : null}
</Frame>
);
}Last updated on