Document register
The organization's controlled documents in a data grid with their number, status, effective version and next review date, flagged when the review is due. 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 empty skeleton @reui/data-grid @reui/frameInstall the glue
Install the block with
npx better-supabase add drive. 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 component
React and shadcn
View in the exampledocument-register.tsx "use client"; import type { CellContext, ColumnDef } from "@tanstack/react-table"; import type { DriveDocumentStatus } from "better-supabase/blocks/drive"; import { useTable } from "@tanstack/react-table"; import { FileCheckIcon, TriangleAlertIcon } from "lucide-react"; import { useExtracted, useFormatter } from "next-intl"; import { useRouter } from "next/navigation"; import { useTransition } from "react"; import type { DataGridFeatures } from "@/components/reui/data-grid/data-grid"; import { Alert, AlertAction, AlertDescription, AlertTitle, } from "@/components/reui/alert"; import { Badge } from "@/components/reui/badge"; import { DataGrid, DataGridContainer, dataGridFeatures, } from "@/components/reui/data-grid/data-grid"; import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"; import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"; import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle, } from "@/components/reui/frame"; import { Button } from "@/components/ui/button"; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@/components/ui/empty"; import { Skeleton } from "@/components/ui/skeleton"; import type { DocumentRegisterView, DocumentRow } from "../drive-queries"; type DocumentCell = CellContext<DataGridFeatures, DocumentRow>; function NumberCell({ row }: DocumentCell) { return ( <span className="font-mono text-xs text-muted-foreground"> {row.original.number} </span> ); } function NameCell({ row }: DocumentCell) { return <span className="font-medium">{row.original.name}</span>; } function StatusCell({ row }: DocumentCell) { const t = useExtracted("drive"); const status: DriveDocumentStatus = row.original.status; switch (status) { case "draft": { return <Badge variant="secondary">{t("Draft")}</Badge>; } case "in_review": { return <Badge variant="info-light">{t("In review")}</Badge>; } case "effective": { return <Badge variant="success-light">{t("Effective")}</Badge>; } case "obsolete": { return <Badge variant="secondary">{t("Obsolete")}</Badge>; } default: { const unknown: never = status; return unknown; } } } function VersionCell({ row }: DocumentCell) { const t = useExtracted("drive"); const { effectiveVersion } = row.original; return effectiveVersion === null ? ( <span className="text-muted-foreground">–</span> ) : ( <span className="tabular-nums"> {t("v{version, number}", { version: effectiveVersion })} </span> ); } function ReviewCell({ row }: DocumentCell) { const t = useExtracted("drive"); const format = useFormatter(); const { nextReviewOn, reviewDue } = row.original; if (nextReviewOn === null) { return <span className="text-muted-foreground">–</span>; } return ( <span className="flex items-center gap-2"> <span className="tabular-nums"> {format.dateTime(new Date(nextReviewOn), { dateStyle: "medium", timeZone: "UTC", })} </span> {reviewDue ? ( <Badge variant="warning-light">{t("Review due")}</Badge> ) : null} </span> ); } function DocumentGrid({ documents }: { documents: readonly DocumentRow[] }) { const t = useExtracted("drive"); const columns: ColumnDef<DataGridFeatures, DocumentRow>[] = [ { accessorKey: "number", header: t("Number"), cell: NumberCell, size: 110 }, { accessorKey: "name", header: t("Document"), cell: NameCell, size: 260 }, { accessorKey: "type", header: t("Type"), size: 110 }, { accessorKey: "status", header: t("Status"), cell: StatusCell, size: 120 }, { accessorKey: "effectiveVersion", header: t("Effective"), cell: VersionCell, size: 100, }, { accessorKey: "nextReviewOn", header: t("Next review"), cell: ReviewCell, size: 200, }, ]; const table = useTable({ features: dataGridFeatures, columns, data: [...documents], getRowId: (row) => row.id, }); return ( <DataGrid table={table} recordCount={documents.length} tableLayout={{ dense: true }} > <DataGridContainer> <DataGridScrollArea> <DataGridTable /> </DataGridScrollArea> </DataGridContainer> </DataGrid> ); } /** * The organization's controlled documents: each with its number, status, the * released version readers get and the date of its next review. */ export function DocumentRegister({ state }: { state: DocumentRegisterView }) { const t = useExtracted("drive"); const router = useRouter(); const [retrying, startRetry] = useTransition(); if (state.kind === "no-organization") { return null; } return ( <Frame spacing="sm" data-recipe="document-register"> <FrameHeader className="gap-0.5"> <FrameTitle>{t("Document register")}</FrameTitle> <FrameDescription> {t( "Files under document control. Readers get the effective version; a revision replaces it once its release is approved.", )} </FrameDescription> </FrameHeader> <FramePanel> {state.kind === "error" ? ( <Alert variant="destructive"> <TriangleAlertIcon /> <AlertTitle>{t("The document register didn't load")}</AlertTitle> <AlertDescription>{t("Try again in a moment.")}</AlertDescription> <AlertAction> <Button size="sm" variant="outline" type="button" disabled={retrying} onClick={() => { startRetry(() => { router.refresh(); }); }} > {t("Retry")} </Button> </AlertAction> </Alert> ) : state.documents.length === 0 ? ( <Empty> <EmptyHeader> <EmptyMedia variant="icon"> <FileCheckIcon /> </EmptyMedia> <EmptyTitle>{t("No controlled documents yet")}</EmptyTitle> <EmptyDescription> {t( "Put a file under document control to give it a number, an owner and a review date.", )} </EmptyDescription> </EmptyHeader> <EmptyContent> <a className="text-sm underline underline-offset-4" href="https://bettersupabase.com/docs/blocks/drive-documents" > {t("Read the controlled documents guide")} </a> </EmptyContent> </Empty> ) : ( <DocumentGrid documents={state.documents} /> )} </FramePanel> </Frame> ); } export function DocumentRegisterSkeleton() { return ( <Frame spacing="sm" aria-busy="true"> <FrameHeader className="gap-1"> <Skeleton className="h-4 w-36" /> <Skeleton className="h-4 w-80" /> </FrameHeader> <FramePanel> <Skeleton className="h-48" /> </FramePanel> </Frame> ); }