diff --git a/.changeset/field-inline-edit.md b/.changeset/field-inline-edit.md new file mode 100644 index 000000000..f29521a21 --- /dev/null +++ b/.changeset/field-inline-edit.md @@ -0,0 +1,13 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec): `inlineEdit` on relationship fields for declarative master-detail + +A `master_detail`/`lookup` field can now declare `inlineEdit: true` (plus +optional `inlineTitle` / `inlineColumns` / `inlineAmountField`) to mean "these +child records are entered/edited inline within the parent's form". The intent +lives in the data model: the parent's standard create/edit form then renders an +atomic master-detail form (object fields + an editable child grid) with no form +view config and no bespoke page. Use for line-item/composition children; leave +off for associations (comments, attachments). Renderer support is in objectui. diff --git a/examples/app-showcase/src/objects/task.object.ts b/examples/app-showcase/src/objects/task.object.ts index 927bc5b43..8e0a2d356 100644 --- a/examples/app-showcase/src/objects/task.object.ts +++ b/examples/app-showcase/src/objects/task.object.ts @@ -24,7 +24,11 @@ export const Task = ObjectSchema.create({ fields: { title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }), - project: Field.masterDetail('showcase_project', { label: 'Project', required: true }), + // `inlineEdit` declares (in the data model) that tasks are entered inline + // within their project's form — so the standard New/Edit Project form + // auto-renders an atomic Tasks subtable, with no form view config and no + // bespoke page. + project: Field.masterDetail('showcase_project', { label: 'Project', required: true, inlineEdit: true, inlineTitle: 'Tasks' }), assignee: Field.text({ label: 'Assignee', maxLength: 200 }), status: Field.select({ label: 'Status', diff --git a/examples/app-showcase/src/views/project.view.ts b/examples/app-showcase/src/views/project.view.ts index 4cb1d4963..e79b735e3 100644 --- a/examples/app-showcase/src/views/project.view.ts +++ b/examples/app-showcase/src/views/project.view.ts @@ -44,10 +44,10 @@ export const ProjectViews = defineView({ { label: 'Project', columns: 2, fields: ['name', 'account', 'status', 'health', 'owner'] }, { label: 'Budget & Schedule', columns: 2, fields: ['budget', 'spent', 'start_date', 'end_date'] }, ], - // Config-driven master-detail (Tier 0): the standard New/Edit Project form - // renders its Tasks inline (FK + columns derived from showcase_task), - // saved as one atomic transaction — no bespoke page. - subforms: [{ childObject: 'showcase_task', title: 'Tasks', addLabel: 'Add task' }], + // No subforms here: the Tasks subtable is derived from the data model — + // showcase_task.project declares `inlineEdit: true`, so every standard + // Project form auto-renders it. (A view could still add `subforms` to + // override the derived columns/order.) }, }, }); diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 151785554..a52307636 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -403,6 +403,23 @@ export const FieldSchema = lazySchema(() => z.object({ referenceFilters: z.array(z.string()).optional().describe('Filters applied to lookup dialogs (e.g. "active = true")'), writeRequiresMasterRead: z.boolean().optional().describe('If true, user needs read access to master record to edit this field'), deleteBehavior: z.enum(['set_null', 'cascade', 'restrict']).optional().default('set_null').describe('What happens if referenced record is deleted'), + /** + * Master-detail INLINE EDITING. On a child's `master_detail`/`lookup` field + * (whose `reference` is the parent object), set `inlineEdit: true` to declare + * "this child is entered/edited inline within the parent's form". The + * parent's standard create/edit form then renders an editable grid for these + * children and saves parent + children in ONE atomic transaction — no form + * view config and no bespoke page. The intent lives here in the data model; + * forms derive the UI. Use for true line-item/composition children (invoice + * lines, order items); leave off for associations (comments, attachments). + */ + inlineEdit: z.boolean().optional().describe('Edit these child records inline within the parent object\'s form (atomic master-detail).'), + /** Optional section title for the inline grid (defaults to the child object label). */ + inlineTitle: z.string().optional().describe('Title for the inline master-detail grid'), + /** Optional explicit grid columns for the inline editor (derived from the child object when omitted). */ + inlineColumns: z.array(z.any()).optional().describe('Explicit columns for the inline grid (derived from the child object when omitted)'), + /** Optional numeric child field summed for the inline grid running total. */ + inlineAmountField: z.string().optional().describe('Numeric child field summed for the inline grid total'), /** Calculation — CEL formula. Plain string accepted for back-compat; build emits canonical envelope. */ expression: ExpressionInputSchema.optional().describe('Formula expression (CEL). e.g. F`record.amount * 0.1`'),