Approvals queue
Requests waiting for the caller's decision, with approve and reject buttons. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add alert badge button card empty skeleton tableInstall the glue
Install the block with
npx better-supabase add approvals. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the screen
React and shadcn
View in the exampleapprovals-queue.tsx /** * Render inside `<Suspense>`: the organization's pending approval requests, * newest first, with Approve and Reject on each. The block checks that the * caller is an approver of the current step. */ export async function ApprovalsQueue() { const [session, t, format] = await Promise.all([ getSession(), getExtracted("approvals"), getFormatter(), ]); if (!can(session, "approvals.read")) { forbidden(); } const queue = await getApprovalsQueue(); if (queue.kind === "no-organization") { return null; } const canRequest = can(session, "approvals.request"); const canDecide = can(session, "approvals.manage"); const customers = queue.kind === "ok" ? queue.data.customers : []; const actionLabel = (action: string) => action === "discount" ? t("Discount") : action; return ( <Card data-recipe="approvals-queue"> <CardHeader> <CardTitle>{t("Pending requests")}</CardTitle> <CardDescription> {t("Each request waits for its approvers, one step at a time.")} </CardDescription> {canRequest && queue.kind === "ok" && queue.data.requests.length > 0 ? ( <CardAction> <RequestApprovalDialog customers={customers} /> </CardAction> ) : null} </CardHeader> <CardContent> {queue.kind === "error" ? ( <Alert variant="destructive"> <TriangleAlertIcon /> <AlertTitle>{t("The requests didn't load")}</AlertTitle> <AlertDescription>{t("Try again in a moment.")}</AlertDescription> <AlertAction> <form action={retryApprovals}> <Button size="sm" variant="outline" type="submit"> {t("Retry")} </Button> </form> </AlertAction> </Alert> ) : queue.data.requests.length === 0 ? ( <Empty className="border"> <EmptyHeader> <EmptyMedia variant="icon"> <ShieldCheckIcon /> </EmptyMedia> <EmptyTitle>{t("Nothing waiting")}</EmptyTitle> <EmptyDescription> {canRequest ? t( "Ask for an approval before giving a customer a discount.", ) : t("New requests show up here.")} </EmptyDescription> </EmptyHeader> {canRequest ? ( <EmptyContent> <RequestApprovalDialog customers={customers} /> </EmptyContent> ) : null} </Empty> ) : ( <Table> <TableHeader> <TableRow> <TableHead>{t("Request")}</TableHead> <TableHead>{t("Requested by")}</TableHead> <TableHead>{t("Step")}</TableHead> <TableHead>{t("Requested")}</TableHead> {canDecide ? ( <TableHead className="text-right">{t("Decision")}</TableHead> ) : null} </TableRow> </TableHeader> <TableBody> {queue.data.requests.map((request) => { const label = `${actionLabel(request.action)}: ${ request.subjectName ?? request.subjectId }`; return ( <TableRow key={request.id}> <TableCell> <div className="flex flex-col gap-1"> <span className="font-medium">{label}</span> {request.reason ? ( <span className="text-xs text-muted-foreground"> {request.reason} </span> ) : null} </div> </TableCell> <TableCell className="text-muted-foreground"> {request.requestedBy ?? t("Unknown")} </TableCell> <TableCell> <Badge variant="warning-light"> {t("{step, number} of {steps, number}", { step: request.step + 1, steps: request.steps, })} </Badge> </TableCell> <TableCell className="text-muted-foreground"> {format.dateTime(new Date(request.createdAt), { dateStyle: "medium", timeStyle: "short", })} </TableCell> {canDecide ? ( <TableCell> <ApprovalDecision id={request.id} label={label} /> </TableCell> ) : null} </TableRow> ); })} </TableBody> </Table> )} </CardContent> </Card> ); }