Custom fields editor
A settings page that adds, orders and removes the custom fields of a record type. 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 custom-fields. 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 examplecustom-fields-editor.tsx /** * Render inside `<Suspense>`: the custom fields of the record type in * `?entity=`, in display order. Managers add, relabel, reorder and delete * them. */ export async function CustomFieldsEditor({ searchParams, }: { searchParams: Promise<Record<string, string | string[] | undefined>>; }) { const [session, t, params] = await Promise.all([ getSession(), getExtracted("custom-fields"), searchParams, ]); if (!can(session, "custom_fields.read")) { forbidden(); } const parsed = v.safeParse(FieldEntitySchema, params["entity"]); const entity = parsed.success ? parsed.output : "customer"; const fields = await getFields(entity); if (fields.kind === "no-organization") { return null; } const canManage = can(session, "custom_fields.manage"); return ( <Card data-recipe="custom-fields-editor"> <CardHeader> <CardTitle>{t("Custom fields")}</CardTitle> <CardDescription> {t("Extra fields on your records, shown in this order.")} </CardDescription> <CardAction> <div className="flex items-center gap-2"> <EntityPicker entity={entity} /> {canManage && fields.kind === "ok" && fields.data.length > 0 ? ( <FieldDialog key={entity} entity={entity} /> ) : null} </div> </CardAction> </CardHeader> <CardContent> {fields.kind === "error" ? ( <Alert variant="destructive"> <TriangleAlertIcon /> <AlertTitle>{t("The fields didn't load")}</AlertTitle> <AlertDescription>{t("Try again in a moment.")}</AlertDescription> <AlertAction> <form action={retryCustomFields}> <Button size="sm" variant="outline" type="submit"> {t("Retry")} </Button> </form> </AlertAction> </Alert> ) : fields.data.length === 0 ? ( <Empty className="border"> <EmptyHeader> <EmptyMedia variant="icon"> <ListPlusIcon /> </EmptyMedia> <EmptyTitle>{t("No custom fields yet")}</EmptyTitle> <EmptyDescription> {canManage ? t( "Add a field to record what your team tracks on top of the built-in ones.", ) : t("An admin can add fields for this record type.")} </EmptyDescription> </EmptyHeader> {canManage ? ( <EmptyContent> <FieldDialog key={entity} entity={entity} /> </EmptyContent> ) : null} </Empty> ) : ( <Table> <TableHeader> <TableRow> <TableHead>{t("Label")}</TableHead> <TableHead>{t("Key")}</TableHead> <TableHead>{t("Type")}</TableHead> {canManage ? ( <TableHead className="text-right">{t("Actions")}</TableHead> ) : null} </TableRow> </TableHeader> <TableBody> {fields.data.map((field, index) => ( <TableRow key={field.id}> <TableCell> <div className="flex items-center gap-2"> <span className="font-medium">{field.label}</span> {field.sensitive ? ( <Badge variant="warning-light">{t("Sensitive")}</Badge> ) : null} </div> </TableCell> <TableCell className="font-mono text-xs text-muted-foreground"> {field.key} </TableCell> <TableCell> <FieldTypeBadge type={field.type} /> </TableCell> {canManage ? ( <TableCell> <FieldRowControls entity={entity} field={field} first={index === 0} last={index === fields.data.length - 1} /> </TableCell> ) : null} </TableRow> ))} </TableBody> </Table> )} </CardContent> </Card> ); }