/** * Form layout resolution. * * Turns a collection into the shape the entity form renders: an ordered list of * sections holding grid-spanned fields, plus the metadata rail beside them. * * The reason this is a pure function rather than logic inside the form is that * the *defaults* are the interesting part. A collection that never writes an * `admin.form` block still has to get a two-column layout out of this — the flat * run of full-width fields it produced before was the single biggest cost in the * form, and no amount of config would have fixed it for collections nobody * hand-tunes. Deriving it here means it is testable in isolation, which matters * because "what span does a date get" is exactly the kind of rule that rots. */ import type { AdminCollection, PropertySpan } from "@rebasepro/cms-types"; import type { Property } from "@rebasepro/types"; /** A field placed on the grid. */ export interface ResolvedFormField { key: string; /** Columns occupied, out of `GRID_COLUMNS`. Ignored in the rail. */ span: PropertySpan; /** True for an `additionalFields` entry rather than a property. */ additional: boolean; /** * The span was written on the property rather than derived from its type. * Row filling leaves these alone: an author who wrote a width meant it. */ spanExplicit?: boolean; } export interface ResolvedFormSection { key: string; title?: string; collapsible: boolean; /** Initial state only; the form owns it after first interaction. */ collapsed: boolean; fields: ResolvedFormField[]; /** * Declared arrangement for the read-only view. Carried through untouched — * the resolver decides *which* fields a section holds, not how the surface * that renders it stacks them, and only the read view honours this. */ readVariant?: "grid" | "summary"; } export interface ResolvedFormLayout { sections: ResolvedFormSection[]; /** Fields shown in the rail. Empty means no rail fields. */ sidebar: ResolvedFormField[]; /** Show the read-only id/created/updated block at the foot of the rail. */ showRecordMeta: boolean; /** True when there is anything at all to put in the rail. */ hasRail: boolean; } export interface ResolveFormLayoutParams> { collection: AdminCollection; /** * Field keys in render order, already filtered by `propertiesOrder` — i.e. * the output of `getFormFieldKeys`. Passed in rather than recomputed so the * form and the layout can never disagree about which fields exist. */ fieldKeys: string[]; /** * Which fields the user may edit right now. A manual id is editable while * creating and frozen afterwards, and that decides whether it belongs in the * form at all or only in the record block. */ status: "new" | "existing" | "copy"; } /** * The span a property gets when nothing is declared. * * Deliberately coarse: three buckets, chosen by how much room the *editor* * needs, not by how important the field is. Importance is what `admin.form` * sections are for. */ export declare function deriveSpan(property: Property, isTitleProperty: boolean): PropertySpan; /** * Can the user still type this id? * * A `manual` id is a real field while creating and frozen once the row exists. * Every generated strategy (`uuid`, `cuid`, a raw SQL default) is never typed. * Getting this wrong in the "frozen" direction would make manual-id collections * uncreatable, so it fails towards showing the field. */ export declare function isIdPropertyEditable(property: Property, status: "new" | "existing" | "copy"): boolean; /** * An audit timestamp the database maintains — `created_at`, `updated_at`. * * Not a guess from the name: `autoValue` says the column is written by the * system on create or update, so the field is never typed. Rendering it as an * input in the middle of the form promised an edit that cannot happen, and the * record block already shows both values — the same two dates twice on one * screen. */ export declare function isAuditTimestamp(property: Property | undefined): boolean; /** * Close the trailing gap in each row. * * Without this a section of half-width fields with an odd count leaves a hole — * `name | sku` then `brand | ␣␣` — a field stranded beside half a row of * nothing, which reads as a mistake rather than a layout. The remainder is * spread across the row's derived fields rather than dumped on the last one, so * a lone half-width field becomes full width and a pair grows evenly. Fields * with an explicit span are never resized. */ export declare function fillRows(fields: ResolvedFormField[]): ResolvedFormField[]; /** * Resolve the layout for a collection's generated form. * * Never drops a field: anything not named by a section lands in a trailing * group, so adding a column to the database cannot make it silently invisible * in the panel. */ export declare function resolveFormLayout>({ collection, fieldKeys, status }: ResolveFormLayoutParams): ResolvedFormLayout;