Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/formview-subforms.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions packages/spec/src/ui/view.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Loading