Skip to content

Commit ee84fa2

Browse files
committed
fix(spec): the form-view conversion strips a master-detail block's section pane
object-master-detail-form shares the object-form section shape, so its sections[].pane parsed through 18.2.0 and is a tombstone now. A stored master-detail page would have failed at parse after the upgrade. convertBlock's page walk now strips sections[].pane on that block too (fixture row, expectedNotices 17), the D3 entry names it, and the pane prescription is worded for both blocks. Claude-Session: https://claude.ai/code/session_01JQwxbWHWPRD5rZXeY69YvL Co-authored-by: Claude <noreply@anthropic.com>
1 parent f282e8a commit ee84fa2

6 files changed

Lines changed: 101 additions & 29 deletions

File tree

‎content/docs/references/ui/component.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ Sort field and direction pair
640640
| **collapsed** | `boolean` | optional | Start the section closed (implies `collapsible`) |
641641
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the whole section, heading and fields, is drawn only when TRUE |
642642
| **columns** | `integer` | optional | Field-grid columns for this section (1-4), a number |
643-
| **pane** | `never` | optional | [REMOVED] `object-form` property `sections[].pane` was removed in @objectstack/spec 18.3.0 (ADR-0050) — it placed a section in one pane of a `formType: 'split'` form, and `split` left the block in the same release, so no form draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
643+
| **pane** | `never` | optional | [REMOVED] Section property `pane` (on `object-form` and `object-master-detail-form` `sections[]`) was removed in @objectstack/spec 18.3.0 (ADR-0050) — it placed a section in one pane of an `object-form` `formType: 'split'` form, and `split` left that block in the same release, so no form draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
644644
| **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's `fieldGroups`. Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `description`, `visibleWhen`, and a `true` `collapsible` / `collapsed`) |
645645
| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … } \| { name: string; label?: string; description?: string; type?: string; … })[]` | optional | The section's fields, in order — each a field name, the form view's `{ field, … }` entry overriding that object field, or an inline form field `{ name, type, … }`. Omit only when `group` supplies the members |
646646

@@ -1151,7 +1151,7 @@ Sort field and direction pair
11511151
| **recordId** | `string \| number` | optional | Parent record to load (edit mode) |
11521152
| **mode** | `Enum<'create' \| 'edit'>` | optional | Form mode |
11531153
| **formType** | `Enum<'simple' \| 'tabbed'>` | optional | Parent form presentation — the two variants the renderer honours for the parent half |
1154-
| **sections** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | Parent form sections — the same section shape `object-form` takes: `{ name?, label?, description?, collapsible?, collapsed?, visibleWhen?, columns?, pane?, fields }` or `{ group, columns?, pane? }` |
1154+
| **sections** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | Parent form sections — the same section shape `object-form` takes: `{ name?, label?, description?, collapsible?, collapsed?, visibleWhen?, columns?, fields }` or `{ group, columns? }` |
11551155
| **fields** | `string[]` | optional | Parent field names to draw, in order — bare names, as on `object-form`; a `{ name }` or `{ field }` object entry is refused |
11561156
| **details** | `{ childObject: string; relationshipField?: string; columns?: object[]; formFields?: string[]; … }[]` | optional | Detail collections — each a strict entry (`{ childObject, title?, addLabel?, columns?, relationshipField?, … }`) whose `columns` are the inline grid columns a relationship field's `inlineColumns` takes; the FK and columns auto-derive from child metadata when omitted |
11571157
| **title** | `string \| Record<string, string>` | optional | Form title |
@@ -1173,7 +1173,7 @@ Sort field and direction pair
11731173
| **collapsed** | `boolean` | optional | Start the section closed (implies `collapsible`) |
11741174
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the whole section, heading and fields, is drawn only when TRUE |
11751175
| **columns** | `integer` | optional | Field-grid columns for this section (1-4), a number |
1176-
| **pane** | `never` | optional | [REMOVED] `object-form` property `sections[].pane` was removed in @objectstack/spec 18.3.0 (ADR-0050) — it placed a section in one pane of a `formType: 'split'` form, and `split` left the block in the same release, so no form draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
1176+
| **pane** | `never` | optional | [REMOVED] Section property `pane` (on `object-form` and `object-master-detail-form` `sections[]`) was removed in @objectstack/spec 18.3.0 (ADR-0050) — it placed a section in one pane of an `object-form` `formType: 'split'` form, and `split` left that block in the same release, so no form draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
11771177
| **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's `fieldGroups`. Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `description`, `visibleWhen`, and a `true` `collapsible` / `collapsed`) |
11781178
| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … } \| { name: string; label?: string; description?: string; type?: string; … })[]` | optional | The section's fields, in order — each a field name, the form view's `{ field, … }` entry overriding that object field, or an inline form field `{ name, type, … }`. Omit only when `group` supplies the members |
11791179

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8739,7 +8739,10 @@ const connectorConnectionTimeoutMsRemoved: MetadataConversion = {
87398739
* and container nesting — scoped to `type === 'object-form'`, as
87408740
* {@link formLayoutInlineGridToVertical} is. The block accepted `split` and
87418741
* the three keys through 18.2.0, so a stored page or an assembled artifact
8742-
* can carry them.
8742+
* can carry them. The `object-master-detail-form` block's parent `sections`
8743+
* are the same section shape (`objectFormSection()`), so its `pane` parsed
8744+
* through 18.2.0 and is refused now: the walk strips `sections[].pane` there
8745+
* too, and touches nothing else on that block.
87438746
*/
87448747
const formViewPresentationTypesRemoved: MetadataConversion = {
87458748
id: 'form-view-presentation-types-removed',
@@ -8751,13 +8754,15 @@ const formViewPresentationTypesRemoved: MetadataConversion = {
87518754
+ 'view.form.sections[].pane / view.form.splitSize / view.form.splitResizable / '
87528755
+ "view.form.splitDirection / page.component.object-form.formType ('split') / "
87538756
+ 'page.component.object-form.sections[].pane / page.component.object-form.splitDirection / '
8754-
+ 'page.component.object-form.splitSize / page.component.object-form.splitResizable',
8757+
+ 'page.component.object-form.splitSize / page.component.object-form.splitResizable / '
8758+
+ 'page.component.object-master-detail-form.sections[].pane',
87558759
summary:
87568760
"form view `type` members 'split' / 'drawer' / 'modal' removed (`type` is the form's layout — "
87578761
+ "simple, tabbed or wizard; where a form opens is the caller's open mode, and a record beside its "
87588762
+ 'list is a reader\'s preference), with the split-only keys `sections[].pane`, `splitDirection`, '
87598763
+ '`splitSize` and `splitResizable`; the `object-form` page block sheds its `split` member and the same '
8760-
+ 'four keys (its `drawer` / `modal` containers stay). The retired member is deleted, which lands the '
8764+
+ 'four keys (its `drawer` / `modal` containers stay), and the `object-master-detail-form` block its '
8765+
+ 'sections\' `pane`. The retired member is deleted, which lands the '
87618766
+ 'form on the `simple` default',
87628767
apply(stack, emit) {
87638768
const RETIRED_TYPES: ReadonlySet<unknown> = new Set(['split', 'drawer', 'modal']);
@@ -8796,10 +8801,14 @@ const formViewPresentationTypesRemoved: MetadataConversion = {
87968801
return stripSectionPanes(next, 'sections', path);
87978802
};
87988803
const withPages = mapPageComponents(stack, (component, path) => {
8799-
if (component.type !== 'object-form') return component;
8804+
if (component.type !== 'object-form' && component.type !== 'object-master-detail-form') return component;
88008805
const properties = component.properties;
88018806
if (!isDict(properties)) return component;
8802-
const next = convertBlock(properties, `${path}.properties`);
8807+
// The master-detail block's parent `sections` share the `object-form`
8808+
// section shape, `pane` included; it had no `split` member or split keys.
8809+
const next = component.type === 'object-form'
8810+
? convertBlock(properties, `${path}.properties`)
8811+
: stripSectionPanes(properties, 'sections', `${path}.properties`);
88038812
return next === properties ? component : { ...component, properties: next };
88048813
});
88058814
const withViews = mapViewPayloads(withPages, (payload, kind, path) =>
@@ -8841,6 +8850,11 @@ const formViewPresentationTypesRemoved: MetadataConversion = {
88418850
},
88428851
// A drawer block is a live container on the block: untouched.
88438852
{ type: 'object-form', properties: { objectName: 'fv_task', formType: 'drawer', drawerSide: 'right' } },
8853+
// A master-detail block's parent sections share the shape: the pane goes.
8854+
{
8855+
type: 'object-master-detail-form',
8856+
properties: { objectName: 'fv_task', sections: [{ label: 'Task', pane: 'primary', fields: ['title'] }] },
8857+
},
88448858
// The same keys on a component that is NOT an `object-form` survive.
88458859
{ type: 'element:custom', properties: { formType: 'split', splitSize: 40 } },
88468860
],
@@ -8900,6 +8914,10 @@ const formViewPresentationTypesRemoved: MetadataConversion = {
89008914
},
89018915
},
89028916
{ type: 'object-form', properties: { objectName: 'fv_task', formType: 'drawer', drawerSide: 'right' } },
8917+
{
8918+
type: 'object-master-detail-form',
8919+
properties: { objectName: 'fv_task', sections: [{ label: 'Task', fields: ['title'] }] },
8920+
},
89038921
{ type: 'element:custom', properties: { formType: 'split', splitSize: 40 } },
89048922
],
89058923
},
@@ -8928,12 +8946,13 @@ const formViewPresentationTypesRemoved: MetadataConversion = {
89288946
{ name: 'fv_task.side', object: 'fv_task', viewKind: 'form' },
89298947
],
89308948
},
8931-
// The split block: the member, three keys and two panes (6). `beside`: the
8932-
// member, `splitDirection`, `splitSize`, `splitResizable` and two panes
8933-
// (6); `quick`, `dialog`, the item record and the assembled overlay: one
8934-
// member each (4). The drawer block, the non-form component, the tabbed
8935-
// form and the list emit none.
8936-
expectedNotices: 16,
8949+
// The split block: the member, three keys and two panes (6). The
8950+
// master-detail block: its one pane (1). `beside`: the member,
8951+
// `splitDirection`, `splitSize`, `splitResizable` and two panes (6);
8952+
// `quick`, `dialog`, the item record and the assembled overlay: one member
8953+
// each (4). The drawer block, the non-form component, the tabbed form and
8954+
// the list emit none.
8955+
expectedNotices: 17,
89378956
},
89388957
};
89398958

‎packages/spec/src/migrations/entries/semantic/18.ui-form-view-type-layout-only.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const entry: SemanticMigration = {
2323
+ 'view.form.sections[].pane, view.form.splitDirection, view.form.splitSize and view.form.splitResizable '
2424
+ 'were removed with them; on an object-form page block the value \'split\' and the props '
2525
+ 'splitDirection, splitSize, splitResizable and sections[].pane left too (its \'drawer\' and '
26-
+ '\'modal\' stay)',
26+
+ '\'modal\' stay), and so did sections[].pane on an object-master-detail-form block',
2727
replacement:
2828
'the form\'s layout: delete the retired value for the `simple` default, or write `tabbed` (one tab '
2929
+ 'per section) or `wizard`. Where the form opens is the caller\'s: a list opens a clicked record in '
@@ -40,13 +40,13 @@ export const entry: SemanticMigration = {
4040
+ '`form-view-presentation-types-removed` makes each stored or existing source parse: it DELETES a '
4141
+ 'retired member, which lands the form on the `simple` default, and strips `pane`, `splitDirection`, '
4242
+ '`splitSize` and `splitResizable` (on an `object-form` page block it deletes `formType: \'split\'` '
43-
+ 'and strips the same keys). What it cannot decide is the layout a former `split` form should take (`tabbed` '
43+
+ 'and strips the same keys, and on an `object-master-detail-form` block it strips a section `pane`). What it cannot decide is the layout a former `split` form should take (`tabbed` '
4444
+ 'keeps its sections apart, `simple` stacks them), and how the caller of a former `drawer` or `modal` '
4545
+ 'form should now open it',
4646
acceptanceCriteria:
4747
'No form view declares `type` `split`, `drawer` or `modal`, and no form view section declares `pane`, '
4848
+ 'nor a form view `splitDirection`, `splitSize` or `splitResizable`; no `object-form` page block '
49-
+ 'declares `formType` `split`, `splitDirection`, `splitSize`, `splitResizable` or a section `pane`; each is refused at parse with its prescription, and '
49+
+ 'declares `formType` `split`, `splitDirection`, `splitSize`, `splitResizable` or a section `pane`, and no `object-master-detail-form` block a section `pane`; each is refused at parse with its prescription, and '
5050
+ 'TypeScript rejects each at a `FormView` position. Every form the conversion changed renders in the '
5151
+ 'layout its author chose, and every entry point that opened it as a drawer or dialog still opens it '
5252
+ 'that way through its own open mode.',

‎packages/spec/src/shared/retired-key-removal-version.pin.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ import {
110110
ElementRepeaterPropsSchema,
111111
ObjectFormPropsSchema,
112112
ObjectGridPropsSchema,
113+
ObjectMasterDetailFormPropsSchema,
113114
ObjectKanbanPropsSchema,
114115
PageHeaderProps,
115116
} from '../ui/component.zod';
@@ -431,6 +432,11 @@ const JOIN: Readonly<Record<string, readonly Renderer[]>> = {
431432
() => ObjectFormPropsSchema.safeParse({ objectName: 'crm_contact', sections: [{ pane: 'primary', fields: ['name'] }] }),
432433
'sections',
433434
),
435+
refusal(
436+
'object-master-detail-form sections[].pane',
437+
() => ObjectMasterDetailFormPropsSchema.safeParse({ objectName: 'crm_contact', sections: [{ pane: 'primary', fields: ['name'] }] }),
438+
'sections',
439+
),
434440
],
435441
// #25050: retired after 18.0.0, so the v18 train's major. Partial (unset
436442
// values and remote urls only), so the prescription's migrate sentence is

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6270,10 +6270,15 @@ const objectFormSplitKeyRetired = (key: string, what: string): string =>
62706270
const OBJECT_FORM_SPLIT_DIRECTION_RETIRED = objectFormSplitKeyRetired('splitDirection', 'set the pane orientation');
62716271
const OBJECT_FORM_SPLIT_SIZE_RETIRED = objectFormSplitKeyRetired('splitSize', 'sized the primary pane');
62726272
const OBJECT_FORM_SPLIT_RESIZABLE_RETIRED = objectFormSplitKeyRetired('splitResizable', 'made the panes resizable');
6273+
/**
6274+
* One section shape serves the `object-form` block and the parent half of the
6275+
* `object-master-detail-form` block (`objectFormSection()`), so this
6276+
* prescription names both.
6277+
*/
62736278
const OBJECT_FORM_SECTION_PANE_RETIRED =
6274-
'`object-form` property `sections[].pane` was removed in @objectstack/spec 18.3.0 (ADR-0050) — it '
6275-
+ "placed a section in one pane of a `formType: 'split'` form, and `split` left the block in the same "
6276-
+ 'release, so no form draws panes. Delete the key. '
6279+
'Section property `pane` (on `object-form` and `object-master-detail-form` `sections[]`) was removed in '
6280+
+ "@objectstack/spec 18.3.0 (ADR-0050) — it placed a section in one pane of an `object-form` `formType: 'split'` "
6281+
+ 'form, and `split` left that block in the same release, so no form draws panes. Delete the key. '
62776282
+ OBJECT_FORM_SPLIT_FIX;
62786283

62796284
/**
@@ -6823,9 +6828,9 @@ function objectFormSectionColumnsRefusal(input: unknown): string | undefined {
68236828
*
68246829
* `name` and `label` (the heading, and the per-`formType` maps),
68256830
* `description`, `collapsible` / `collapsed` (`resolveSectionCollapse`),
6826-
* `visibleWhen` (the divider row's predicate), `columns`, `pane` (the split
6827-
* form), `group` (resolved into the field group's own section above the
6828-
* routing) and `fields` (read points recorded in `liveness/page.json`, row
6831+
* `visibleWhen` (the divider row's predicate), `columns`, `pane` (a
6832+
* tombstone since 18.3.0: its reader, the split form, retired), `group`
6833+
* (resolved into the field group's own section above the routing) and `fields` (read points recorded in `liveness/page.json`, row
68296834
* `regions`). That is exactly the form view's section's key set, and objectui
68306835
* declares the same (`ObjectFormSection`).
68316836
*
@@ -7255,7 +7260,7 @@ export const ObjectMasterDetailFormPropsSchema = lazySchema(() => strictObject({
72557260
* one instance {@link buildObjectFormSection} builds (S-forms stage).
72567261
*/
72577262
sections: z.array(objectFormSection()).optional()
7258-
.describe('Parent form sections — the same section shape `object-form` takes: `{ name?, label?, description?, collapsible?, collapsed?, visibleWhen?, columns?, pane?, fields }` or `{ group, columns?, pane? }`'),
7263+
.describe('Parent form sections — the same section shape `object-form` takes: `{ name?, label?, description?, collapsible?, collapsed?, visibleWhen?, columns?, fields }` or `{ group, columns? }`'),
72597264
fields: formFieldNameList().optional()
72607265
.describe('Parent field names to draw, in order — bare names, as on `object-form`; a `{ name }` or `{ field }` object entry is refused'),
72617266
details: z.array(masterDetailDetailEntry()).optional()

0 commit comments

Comments
 (0)