Skip to content

Generated record screens and forms

View as Markdown

This page traces table metadata into generated list, create, detail, and edit screens. You will create a project and related task without adding React code, then identify which schema choices control each field. The same surfaces can provide the first usable record workflow for asset registers, team directories, cases, and approval queues.

Set up generated project and task screens. I want named project lookups, status and priority selects, search, and a Tasks section on each project.

Registered table metadata drives the ordinary CRUD experience:

import { sqliteTable } from "drizzle-orm/sqlite-core";
import { sapportaTable, select } from "@sapporta/server/table";
export const tasksTable = sqliteTable("tasks", {
// Other columns omitted for focus.
status: select("status", ["open", "in_progress", "completed"] as const)
.notNull()
.default("open"),
priority: select("priority", ["low", "medium", "high"] as const)
.notNull()
.default("medium"),
});
export const tasks = sapportaTable({
drizzle: tasksTable,
meta: {
label: "Tasks",
rowScope: "workspaceGlobal",
rowLabelColumns: ["title"],
search: { columns: ["title", "description"] },
columns: {
project_id: { label: "Project" },
description: { textDisplay: "multiLine" },
},
},
});

Column kinds choose default inputs and formatting. Each select() option tuple constrains the stored string and supplies a searchable, clearable combobox. The text typed into that combobox filters its declared options; it is not stored as the field value. A Drizzle foreign key plus the project rowLabelColumns turns project_id into a scoped lookup that stores an ID and displays a name. System-managed scope fields, generated primary keys, and columns with apiWritable: false stay out of ordinary forms.

The project definition supplies the reverse navigation:

children: [
{
table: "tasks",
foreignKey: "project_id",
label: "Tasks",
columns: ["title", "status", "due_date"],
defaultSort: "due_date",
},
];
  1. Start the app with pnpm dev and open /tables/projects.
  2. Create a project named Website Relaunch.
  3. Open /tables/tasks/new and create Publish launch checklist.
  4. Choose Website Relaunch in the Project lookup. Set status to open, priority to high, and add a due date.
  5. Edit the saved task, then open its project record and find the task in the Tasks child section.

Lookup results contain only rows visible to the current user. Generated form validation and generated HTTP validation use the same table definition. A hidden field or a fixed client filter is presentation, not an authorization rule.

Numeric, money, percentage, date, and timestamp inputs keep raw strings while a record is being edited. Intermediate numeric text such as - or 12. therefore stays visible. The form decodes the full draft once on submit. Finite numeric text becomes a JSON number, dates and timestamps become canonical strings, and invalid input produces an issue beside its field without rewriting the draft.

Create presence rules are applied during that decode. Empty optional non-text controls are omitted so database defaults and nullable insert rules still apply. Empty text remains "", which is distinct from null. Required empty controls produce local issues. The server then applies API write policy, adds trusted scope values, checks reference visibility, and performs authoritative structural and application validation immediately before the Drizzle write.

The project and task schemas now provide a complete ordinary record workflow: create, edit, search, lookup, detail, child navigation, copy, and export. Start with metadata when the behavior is still table-shaped. Add a custom React screen only when the workflow needs a different composition, temporary state, or domain interaction model. The next guide compares those Grid layers.