diff --git a/.changeset/formview-subforms.md b/.changeset/formview-subforms.md new file mode 100644 index 000000000..b6347e1c9 --- /dev/null +++ b/.changeset/formview-subforms.md @@ -0,0 +1,13 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec): `FormViewSchema.subforms` for config-driven master-detail + +A form view can now declare inline child collections via `subforms`, so the +standard create/edit form for an object can render as a master-detail form +(object fields on top, an editable child grid below, persisted atomically) +without a bespoke page. Each entry needs only `childObject`; the relationship +FK and grid columns are derived from the child object's metadata (override via +`relationshipField` / `columns`). Renderer support: ObjectForm already renders +`subforms` (objectui), and the ObjectView form path passes them through. diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index d99e20c58..003ee3ad3 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -679,6 +679,27 @@ export const FormViewSchema = lazySchema(() => z.object({ sections: z.array(FormSectionSchema).optional(), // For simple layout groups: z.array(FormSectionSchema).optional(), // Legacy support -> alias to sections + /** + * Inline child collections (master-detail). When present, the standard + * create/edit form for this object renders as a master-detail form — the + * object's own fields on top, an editable grid per child collection below, + * persisted together in ONE atomic transaction — with no bespoke page. Each + * entry needs only `childObject`; the relationship FK and grid columns are + * derived from the child object's metadata (override via + * `relationshipField` / `columns`). + */ + subforms: z.array(z.object({ + childObject: z.string().describe('Child object whose records are entered inline'), + relationshipField: z.string().optional().describe('FK on the child pointing back to the parent (auto-detected when omitted)'), + columns: z.array(z.any()).optional().describe('Editable grid columns (derived from the child object when omitted)'), + amountField: z.string().optional().describe('Numeric child column summed for the running total'), + totalField: z.string().optional().describe('Parent field to receive the rolled-up sum'), + title: z.string().optional().describe('Section title'), + addLabel: z.string().optional().describe('Add-row button label'), + minRows: z.number().optional(), + maxRows: z.number().optional(), + })).optional().describe('Inline master-detail child collections'), + /** Default Sort for Related Lists (e.g., sort child records by date) */ defaultSort: z.array(z.object({ field: z.string().describe('Field name to sort by'),