diff --git a/.changeset/field-form-summary-subfields.md b/.changeset/field-form-summary-subfields.md new file mode 100644 index 0000000000..ac65df8db6 --- /dev/null +++ b/.changeset/field-form-summary-subfields.md @@ -0,0 +1,16 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): declare `summaryOperations` sub-fields in the Field metadata form (#3257) + +`fieldForm` (the registered metadata form for editing a Field) previously +declared `summaryOperations` as a bare `composite` with no sub-fields, so a +protocol-driven renderer had to fall back to a raw JSON editor. It now declares +the inner shape explicitly — `object` (`ref:object`), `function` (select), +`field`, `relationshipField`, and `filter` (bound to `widget: 'filter-condition'`) +— mirroring the `summaryOperations` Zod schema and surfacing the roll-up `filter` +added in #1868. Also gates the block to `data.type == 'summary'`. + +Small step toward #3257 (making the Studio field designer metadata-driven rather +than hand-coded); the live objectui inspector already edits these fields. diff --git a/packages/spec/src/data/field.form.ts b/packages/spec/src/data/field.form.ts index 2ea92df552..076a9b195e 100644 --- a/packages/spec/src/data/field.form.ts +++ b/packages/spec/src/data/field.form.ts @@ -56,7 +56,35 @@ export const fieldForm = defineForm({ collapsed: true, fields: [ { field: 'expression', widget: 'textarea', helpText: 'CEL expression to calculate this field (makes it read-only)' }, - { field: 'summaryOperations', type: 'composite', helpText: 'Roll-up summary configuration (for parent-child relationships)' }, + { + field: 'summaryOperations', + type: 'composite', + visibleWhen: "data.type == 'summary'", + helpText: 'Roll-up summary configuration (for parent-child relationships)', + // Declare the composite's inner shape so the protocol-driven form + // renders structured sub-fields (not a raw JSON blob). Mirrors the + // `summaryOperations` Zod schema in field.zod.ts; `filter` is bound to + // the FilterCondition widget so only matching child rows aggregate. + fields: [ + { field: 'object', widget: 'ref:object', required: true, helpText: 'Child object to aggregate' }, + { + field: 'function', + type: 'select', + required: true, + options: [ + { label: 'Count', value: 'count' }, + { label: 'Sum', value: 'sum' }, + { label: 'Min', value: 'min' }, + { label: 'Max', value: 'max' }, + { label: 'Average', value: 'avg' }, + ], + helpText: 'Aggregation function', + }, + { field: 'field', required: true, helpText: 'Child field to aggregate (ignored for count)' }, + { field: 'relationshipField', helpText: 'Child FK back to this parent (auto-detected when omitted)' }, + { field: 'filter', widget: 'filter-condition', helpText: 'Only child rows matching this predicate are aggregated (e.g. status == received)' }, + ], + }, ], }, {