| name | finito-forms |
| description | Implement and refactor forms in the Finito codebase using the established AutoForm architecture. Use when working on `*-form.tsx` files in `app/**` or `components/**`, adding or changing Zod form schemas, default values, conditional form sections, array table fields, custom field components, and save actions backed by Evolu (`upsert`/`update`/`insert`). |
Finito Forms
Overview
Implement forms in the same style as existing project forms.
Follow one repeatable pattern: Zod schema + defaults + AutoForm layout + useActionForm + Evolu persistence.
Read references/form-patterns.md before editing.
Workflow
- Read the target form and identify these blocks:
...Schema (Zod input/output conversion).
create...DefaultValues (UI defaults; mostly strings for text inputs).
createComponents with createAutoFormLayout.
useActionForm(..., { defaultValues, saveAction }).
<AutoForm form={form} components={components} />.
- Define schema in UI-input style, then transform to domain values:
- Prefer
StringToNullableStringSchema, StringToUndefinedStringSchema, StringToNumberSchema, StringToNullableNumberSchema.
- Apply business constraints with
.pipe(...), .refine(...), .superRefine(...), and discriminated unions when needed.
- Keep
z.input<typeof schema> for defaults and z.output<typeof schema> for save logic.
- Build defaults deterministically:
- Use
satisfies z.input<typeof schema>.
- For edit/create forms, merge defaults with incoming partial defaults via
useState(() => merge(createDefaults(), params.defaultValues ?? {})).
- Use
createId(createRandomBytes()) for new entities and createIdFromString("") for singleton settings rows.
- Compose UI with AutoForm builder:
- Use
builder.magicInput(...).text/textarea/select/amount/checkbox/....
- Group inputs with
builder.line, builder.card, builder.collapsibleSeparator, builder.accordion.
- Use
builder.nestedField, builder.arrayField, builder.arrayTableField for structured data.
- Use
builder.when/builder.whenNot for conditional sections.
- Use
builder.createComponent for custom inputs (comboboxes, computed fields, derived setters via useWatch + useFormContext).
- Wire form execution with
useActionForm:
- Pass
defaultValues and saveAction.
- Treat
saveAction(values, originalValues) as already validated/transformed output.
- Keep side effects in
saveAction and let useActionForm handle submit state + success/error toasts.
- Persist with Evolu using project rules:
- Use
evolu.upsert for coherent row state writes.
- Use
evolu.update for partial changes (for example, toggling isDeleted).
- Use
evolu.insert when ID should be generated by Evolu.
- For editable arrays, diff existing IDs vs submitted IDs and soft-delete removed rows with
isDeleted: sqliteTrue.
- Keep project conventions:
- Translate labels/descriptions with
t(...).
- Memoize components with
useMemo(() => createComponents(t), [t]).
- Use
form.form.setFocus(...) in useEffect when UX expects initial focus.
Key Files
- Core primitives:
components/auto-form.tsx
hooks/use-action-form.ts
lib/shared/types.ts
- Representative forms:
app/admin/(private)/ai-assistant/settings/ai-assistant-settings-form.tsx
app/admin/(private)/settings/credentials/credentials-form.tsx
app/admin/(private)/settings/billing-settings/billing-settings-form.tsx
app/admin/(private)/contacts/contact-form.tsx
app/admin/(private)/reservations/reservation-form.tsx
app/admin/(private)/payments/new/payment-form.tsx
Output Contract
When asked to create or refactor a Finito form, produce:
- Updated schema + defaults + component layout + save action in project style.
- Correct Evolu write method (
upsert/update/insert) per data mutation intent.
- No ad-hoc form architecture unless explicitly requested.