Skip to content

Commit 999aecb

Browse files
committed
fix(plugin-form,types): the details entry rows hold on objectstack main too; the sortField fork is stated as retired upstream (objectui#11396, patch round 1)
The `Spec Main Shape Gate` compiles this repository against `@objectstack/spec` built from objectstack `main`, where the detail entry's `sortField` is a `retiredKey()` tombstone (objectstack-ai/objectstack#21589, PR #21632, `6ec54f00`, unreleased after 17.6.0). The compile-time row `specStillDeclaresSortField` pinned the 17.6.0 shape and was red there. - Drop `specStillDeclaresSortField`. `derivedFromSpec`, `columnsAreTheGridColumn` and `memberList` hold on both shapes: the `Omit` subtracts the key whether the entry declares it as a string or as a tombstone. The forward tripwire for the retirement stays the runtime pin in `@object-ui/types`, which records `sortField` ACCEPTED on 17.6.0 and flips at the bump. - Say what is true now in the `MasterDetailForm.tsx` docblock, the members test header, the types pin header and row name, the registration description, the console parity row and the changeset body: the retirement has landed on objectstack `main`, is unreleased after 17.6.0, and the fork closes at objectui's bump to the first release carrying it. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a4ac1f3 commit 999aecb

6 files changed

Lines changed: 52 additions & 36 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
---
22
---
33

4-
No release. `@object-ui/plugin-form`'s exported `MasterDetailDetailConfig` is now derived from `@objectstack/spec`'s `details` entry by reference (`ComponentPropsMap['object-master-detail-form'].details[]`, a closed shape since 17.6.0) instead of restating it by hand, minus the one member the renderer does not read (`sortField`, retired here by objectui#11070). Member for member the type is the same as before — the same keys, the same member types, `columns` the same inline grid column — so what a caller may write does not move. The `object-master-detail-form` registration declares `of: 'object'` and a description for `details`; tests and the console's parity prose follow (objectui#11396).
4+
No release. `@object-ui/plugin-form`'s exported `MasterDetailDetailConfig` is now derived from `@objectstack/spec`'s `details` entry by reference (`ComponentPropsMap['object-master-detail-form'].details[]`, a closed shape since 17.6.0) instead of restating it by hand, minus the one member the renderer does not read (`sortField`: retired here by objectui#11070, and on objectstack `main` by objectstack-ai/objectstack#21589, unreleased after 17.6.0; the subtraction holds on both spec shapes). Member for member the type is the same as before — the same keys, the same member types, `columns` the same inline grid column — so what a caller may write does not move. The `object-master-detail-form` registration declares `of: 'object'` and a description for `details`; tests and the console's parity prose follow (objectui#11396).

‎apps/console/src/__tests__/registry-inputs-spec-parity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3034,7 +3034,7 @@ const MEMBER_PINS: Record<string, MemberPin> = {
30343034
},
30353035
'object-master-detail-form.details': {
30363036
file: 'packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx',
3037-
pins: 'Members are detail-collection OBJECTS (`MasterDetailDetailConfig`), and unlike this block\'s parent keys the read site is THIS block: `MasterDetailForm` is the key\'s only reader. Each member renders as its own section in AUTHORED order, headed by `title` (fallback `Line Items`), with `columns` reaching the line grid in authored order. ⭐ The sharp row: four members reach the grid through ONE hand-written object under the grid\'s camelCase keys (objectui#11610): `minRows`, `maxRows` and `addLabel` by the same name, and `amountField` RENAMED to the grid\'s `totalField` (the CHILD column summed; the detail\'s own `totalField`, the PARENT field, is not forwarded), beside `columns` and the DERIVED `sortField` (objectui#11070 round 9 retired the detail\'s `sortField` member; row 2c pins that a written one is read by nothing). That object is pinned as a SORTED KEY SET of exactly six, with two off-list members (one in the grid\'s own spelling, `allowAdd`) asserted not forwarded and an all-empty control beside it. `inlineMode: \'form\'` makes a list whose Add opens the full form (labelled `Add` when unset), and in grid mode `formFields` wider than `columns` offers the row form; each arm is the other\'s control. `childObject` and `relationshipField` address the atomic batch (each line a create on the child, linked by `{ $ref: 0 }`) and the edit-mode read (`{ $filter: { FK: recordId }, $top: 500 }` per collection). `totalField` receives the sum of the lines\' `amountField` on the PARENT leg, and a collection without one puts nothing there. With `{ childObject }` alone the FK and the columns are DERIVED from the child object, the one member behaviour the spec\'s description promises, and the batch writes the derived FK. `details: []` is the non-vacuity control: no section, no grid, a one-leg batch. The line grid is the REAL `LineItemsField`, wrapped only to record the props it is handed. Since `@objectstack/spec` 17.6.0 (objectstack-ai/objectstack#21215) the spec row is an array of CLOSED entries — `childObject` required, `columns` the spec\'s inline grid column, an undeclared member key refused by name — and `@object-ui/types` takes that row by reference, so both published faces give the spec\'s verdict on an entry (`packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts`). `MasterDetailDetailConfig` is DERIVED from that entry (objectui#11396) and held `Equal` to it at compile time in the pinned file, with ONE stated fork: the spec declares `sortField`, which this block does not read (row 2c), and the spec-side retirement is objectstack\'s. The registration declares `of: \'object\'`, derived from the now single-kind member contract. A declaration fixes an entry\'s SHAPE, not what this block DOES with a member, so the read site is still the whole member contract. New file (objectui#8071 slice 18).',
3037+
pins: 'Members are detail-collection OBJECTS (`MasterDetailDetailConfig`), and unlike this block\'s parent keys the read site is THIS block: `MasterDetailForm` is the key\'s only reader. Each member renders as its own section in AUTHORED order, headed by `title` (fallback `Line Items`), with `columns` reaching the line grid in authored order. ⭐ The sharp row: four members reach the grid through ONE hand-written object under the grid\'s camelCase keys (objectui#11610): `minRows`, `maxRows` and `addLabel` by the same name, and `amountField` RENAMED to the grid\'s `totalField` (the CHILD column summed; the detail\'s own `totalField`, the PARENT field, is not forwarded), beside `columns` and the DERIVED `sortField` (objectui#11070 round 9 retired the detail\'s `sortField` member; row 2c pins that a written one is read by nothing). That object is pinned as a SORTED KEY SET of exactly six, with two off-list members (one in the grid\'s own spelling, `allowAdd`) asserted not forwarded and an all-empty control beside it. `inlineMode: \'form\'` makes a list whose Add opens the full form (labelled `Add` when unset), and in grid mode `formFields` wider than `columns` offers the row form; each arm is the other\'s control. `childObject` and `relationshipField` address the atomic batch (each line a create on the child, linked by `{ $ref: 0 }`) and the edit-mode read (`{ $filter: { FK: recordId }, $top: 500 }` per collection). `totalField` receives the sum of the lines\' `amountField` on the PARENT leg, and a collection without one puts nothing there. With `{ childObject }` alone the FK and the columns are DERIVED from the child object, the one member behaviour the spec\'s description promises, and the batch writes the derived FK. `details: []` is the non-vacuity control: no section, no grid, a one-leg batch. The line grid is the REAL `LineItemsField`, wrapped only to record the props it is handed. Since `@objectstack/spec` 17.6.0 (objectstack-ai/objectstack#21215) the spec row is an array of CLOSED entries — `childObject` required, `columns` the spec\'s inline grid column, an undeclared member key refused by name — and `@object-ui/types` takes that row by reference, so both published faces give the spec\'s verdict on an entry (`packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts`). `MasterDetailDetailConfig` is DERIVED from that entry (objectui#11396) and held `Equal` to it at compile time in the pinned file, with ONE stated fork: 17.6.0 declares `sortField`, which this block does not read (row 2c); objectstack `main` has since retired it (objectstack-ai/objectstack#21589, unreleased after 17.6.0), and the fork closes at the bump to the first release carrying that. The registration declares `of: \'object\'`, derived from the now single-kind member contract. A declaration fixes an entry\'s SHAPE, not what this block DOES with a member, so the read site is still the whole member contract. New file (objectui#8071 slice 18).',
30383038
},
30393039
'object-master-detail-form.fields': {
30403040
file: 'packages/plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx',

‎packages/plugin-form/src/MasterDetailForm.tsx‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,21 @@ import type { ObjectMasterDetailFormProps } from '@objectstack/spec/ui';
7878
* `__tests__/masterDetailDetailsMembers-8071.test.tsx` holds the two equal at
7979
* compile time.
8080
*
81-
* ⛔ ONE fork, stated rather than absorbed: the spec entry declares `sortField`,
82-
* and this form reads no such member. objectui#11070 round 9 retired the
83-
* authored override: the child field the grid stamps with each line's position
84-
* is DERIVED from the child object (`deriveDetail` picks its `position` /
85-
* `sort_order` / … field) and carried on the resolved entry, and row 2c of that
86-
* test pins that a written `sortField` is read by nothing. The spec-side
87-
* retirement is objectstack's half (recorded on
88-
* objectstack-ai/objectstack#6017); until it lands, the member is left OFF this
89-
* face, so the authoring face declares no key the renderer does not honour.
90-
* That omission is the whole of what is hand-written about the shape.
81+
* ⛔ ONE fork, stated rather than absorbed: at 17.6.0 the spec entry declares
82+
* `sortField`, and this form reads no such member. objectui#11070 round 9
83+
* retired the authored override: the child field the grid stamps with each
84+
* line's position is DERIVED from the child object (`deriveDetail` picks its
85+
* `position` / `sort_order` / … field) and carried on the resolved entry, and
86+
* row 2c of that test pins that a written `sortField` is read by nothing.
87+
* objectstack has since retired the member on its `main` — a `retiredKey()`
88+
* tombstone on the entry (objectstack-ai/objectstack#21589, landed by PR
89+
* objectstack-ai/objectstack#21632 as `6ec54f00`), unreleased after 17.6.0 —
90+
* so the fork closes at objectui's bump to the first `@objectstack/spec`
91+
* release that carries it; the omission below then subtracts a key the entry
92+
* no longer offers, and holds on both shapes. Until that bump the member is
93+
* left OFF this face, so the authoring face declares no key the renderer does
94+
* not honour. That omission is the whole of what is hand-written about the
95+
* shape.
9196
*
9297
* Where each member lands — the renderer's reading, which the spec's
9398
* description does not carry:

‎packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx‎

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
* objectui#11396): the spec row is an array of CLOSED entries — `childObject`
2121
* required, an undeclared key refused by name, `columns` the spec's inline grid
2222
* column — and `MasterDetailDetailConfig` is derived from that entry by
23-
* reference, with ONE stated fork: the spec declares `sortField`, which this
24-
* block does not read (objectui#11070 round 9 retired the authored override;
25-
* row 2c below pins that a written one reaches nothing). The registration
23+
* reference, with ONE stated fork: `@objectstack/spec` 17.6.0 declares
24+
* `sortField`, which this block does not read (objectui#11070 round 9 retired
25+
* the authored override; row 2c below pins that a written one reaches
26+
* nothing); objectstack `main` has since retired it too
27+
* (objectstack-ai/objectstack#21589, unreleased after 17.6.0), and the fork
28+
* closes at objectui's bump to the first release carrying that. The registration
2629
* declares `of: 'object'`, derived from that single-kind member contract. The
2730
* compile-time block at the end of this file holds the config equal to the
2831
* spec's entry minus that fork and spells the member list out, so the member
@@ -485,18 +488,19 @@ type SpecDetailEntry = NonNullable<ObjectMasterDetailFormProps['details']>[numbe
485488
* The config IS the spec's entry minus the one stated fork, by reference
486489
* (objectui#11396): a member the spec adds, drops or retypes lands on the
487490
* config with no hand edit, and a hand-written restatement that drifts is red
488-
* here before it is red anywhere else.
491+
* here before it is red anywhere else. It holds on both spec shapes this
492+
* repository is compiled against: 17.6.0, where the entry declares `sortField`
493+
* as a string, and objectstack `main`, where the entry carries a `retiredKey()`
494+
* tombstone for it (objectstack-ai/objectstack#21589, unreleased after 17.6.0)
495+
* — the subtraction removes the key either way. ⛔ No row here pins which of
496+
* the two shapes the spec has; the `Spec Main Shape Gate` compiles this file
497+
* against `main`, so such a row is red there the day the two differ. The
498+
* forward tripwire for the retirement is the runtime pin in `@object-ui/types`
499+
* (`object-master-detail-form-details-entry-11396.test.ts`), which records
500+
* `sortField` as ACCEPTED on 17.6.0 and flips at the bump.
489501
*/
490502
const derivedFromSpec: Equal<MasterDetailDetailConfig, Omit<SpecDetailEntry, 'sortField'>> = true;
491503

492-
/**
493-
* The fork is still open upstream: the spec entry declares `sortField`. When
494-
* this row reds, objectstack retired the member (objectstack-ai/objectstack#6017
495-
* records that half), and the `Omit` in `MasterDetailForm.tsx` is to be dropped
496-
* together with this row and the premise of row 2c and `retiredSortField`.
497-
*/
498-
const specStillDeclaresSortField: Equal<SpecDetailEntry['sortField'], string | undefined> = true;
499-
500504
/** A `columns` entry is the spec's inline grid column, the type the line grid reads. */
501505
const columnsAreTheGridColumn: Equal<NonNullable<MasterDetailDetailConfig['columns']>[number], GridColumn> = true;
502506

@@ -522,6 +526,6 @@ const memberList: Equal<
522526

523527
/** The lit control for the directive below: the same literal without the retired member compiles. */
524528
const authoredDetail: MasterDetailDetailConfig = { childObject: 'po_step', amountField: 'qty' };
525-
// @ts-expect-error objectui#11070 round 9: `sortField` is retired as a detail member; the sort field is derived from the child object. The spec still declares it (`specStillDeclaresSortField` above), and this face leaves it off on purpose (objectui#11396).
529+
// @ts-expect-error objectui#11070 round 9: `sortField` is retired as a detail member; the sort field is derived from the child object. This face leaves it off on purpose (objectui#11396), on 17.6.0 where the spec declares it and on objectstack main where the spec retired it too.
526530
const retiredSortField: MasterDetailDetailConfig = { childObject: 'po_step', sortField: 'line_no' };
527-
void [derivedFromSpec, specStillDeclaresSortField, columnsAreTheGridColumn, memberList, authoredDetail, retiredSortField];
531+
void [derivedFromSpec, columnsAreTheGridColumn, memberList, authoredDetail, retiredSortField];

‎packages/plugin-form/src/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ ComponentRegistry.register('object-master-detail-form', MasterDetailFormRenderer
560560
of: 'object',
561561
required: true,
562562
description:
563-
'The child collections, one OBJECT per entry, judged by the spec\'s closed `details` entry: `childObject` (required) names the child object; `relationshipField` is the FK on the child back to the parent, derived from the child\'s metadata when omitted; `columns` are the line grid\'s columns as the spec\'s inline grid column objects (`{ name, … }`, never a bare field name), derived from the child\'s fields when omitted; `formFields` names the per-row form\'s fields; `inlineMode` is `grid` (editable cells) or `form` (list + per-row form); `amountField` is the CHILD column summed and `totalField` the PARENT field that sum is saved to; `title`, `minRows`, `maxRows` and `addLabel` dress the section and its grid. An undeclared member key is refused by name. The entry\'s `sortField` is declared by the spec and read by nothing here: the line-position field is derived from the child object (objectui#11070).',
563+
'The child collections, one OBJECT per entry, judged by the spec\'s closed `details` entry: `childObject` (required) names the child object; `relationshipField` is the FK on the child back to the parent, derived from the child\'s metadata when omitted; `columns` are the line grid\'s columns as the spec\'s inline grid column objects (`{ name, … }`, never a bare field name), derived from the child\'s fields when omitted; `formFields` names the per-row form\'s fields; `inlineMode` is `grid` (editable cells) or `form` (list + per-row form); `amountField` is the CHILD column summed and `totalField` the PARENT field that sum is saved to; `title`, `minRows`, `maxRows` and `addLabel` dress the section and its grid. An undeclared member key is refused by name. The entry\'s `sortField` is read by nothing here — the line-position field is derived from the child object (objectui#11070) — and objectstack retires the key on its `main` (objectstack-ai/objectstack#21589, after 17.6.0).',
564564
},
565565
{ name: 'recordId', type: 'string', description: 'The parent record to load in `edit` mode. Leave unset for `create`.' },
566566
// TWO values, not the six `object-form` declares (objectui#5939). A bare

‎packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,20 @@
3838
*
3939
* ## The fork, pinned as ACCEPTED so that closing it is a deliberate change
4040
*
41-
* The spec entry declares `sortField`; `MasterDetailForm` reads no such member
42-
* (objectui#11070 round 9 retired the authored override, and
41+
* At 17.6.0 the spec entry declares `sortField`; `MasterDetailForm` reads no
42+
* such member (objectui#11070 round 9 retired the authored override, and
4343
* `plugin-form`'s `masterDetailDetailsMembers-8071.test.tsx` row 2c pins that a
4444
* written one reaches nothing). So `objectui validate` ACCEPTS an entry carrying
45-
* `sortField` while the renderer ignores it. The spec-side retirement is
46-
* objectstack's half (objectstack-ai/objectstack#6017); the row below records
47-
* today's verdict, and reds the day the spec retires the key, which is the cue
48-
* to flip it and drop the `Omit` on `MasterDetailDetailConfig`.
45+
* `sortField` while the renderer ignores it. objectstack has since retired the
46+
* key on its `main` with a `retiredKey()` tombstone on the entry
47+
* (objectstack-ai/objectstack#21589, PR objectstack-ai/objectstack#21632,
48+
* `6ec54f00`), unreleased after 17.6.0. The row below is the forward tripwire:
49+
* it records the 17.6.0 verdict, and reds at objectui's bump to the first
50+
* `@objectstack/spec` release that carries the retirement — the cue to flip it
51+
* to a refusal and drop the `Omit` on `MasterDetailDetailConfig`. It is a
52+
* RUNTIME row on purpose: the `Spec Main Shape Gate` compiles this repository
53+
* against objectstack `main`, so a compile-time row pinning the 17.6.0 shape
54+
* would be red there today.
4955
*/
5056

5157
import { describe, it, expect } from 'vitest';
@@ -148,9 +154,10 @@ describe('an `object-master-detail-form` `details` entry is judged by the spec\'
148154
]);
149155
});
150156

151-
it('ACCEPTS `sortField` — the fork the spec still declares and the renderer does not read (objectstack\'s half)', () => {
152-
// When this reds, the spec retired the key: flip this row and drop the
153-
// `Omit` on `MasterDetailDetailConfig` in `@object-ui/plugin-form`.
157+
it('ACCEPTS `sortField` on 17.6.0 — the forward tripwire for objectstack#21589, which retired the key on main after 17.6.0', () => {
158+
// When this reds, the installed spec carries the retirement: flip this
159+
// row to a refusal and drop the `Omit` on `MasterDetailDetailConfig` in
160+
// `@object-ui/plugin-form`.
154161
const result = parse(nodeWith({ childObject: 'po_line', sortField: 'line_no' }));
155162
expect(result.success, JSON.stringify(result.error?.issues)).toBe(true);
156163
});

0 commit comments

Comments
 (0)