Skip to content

Commit 6ec54f0

Browse files
feat(spec)!: retire the object-master-detail-form detail entry sortField — the console derives the line-position field from the child object (#21589) (#21632)
Fixes #21589 Clause-②: no (narrowing) ## What this does Retires `sortField` from the `object-master-detail-form` detail entry (`ComponentPropsMap['object-master-detail-form'].details[]`). The console reads no authored value for it. At the `.objectui-sha` pin `89cad75d5570` (and unchanged at the current pin `ab1879721595`: `MasterDetailForm.tsx` and `deriveMasterDetail.ts` are byte-identical between the two), `MasterDetailDetailConfig` has no `sortField` member (`plugin-form/src/MasterDetailForm.tsx:83`). The field the line grid stamps with each line's position is the one `deriveDetail` derives from the child object (`deriveMasterDetail.ts:540`), and it reaches the grid as `sort_field` (`:874`). The spec still declared the key, so an authored `sortField` went through `os validate` clean and was then dropped. This executes the direction triage confirmed (`5969870827`), the one recorded on #21220's landing (`5937457620`) and mirrored on objectstack-ai/objectui#11396 ③: a `retiredKey()` tombstone, one ADR-0087 D2 conversion and one D3 entry, with no staged window because the writer census is zero. There is no objectui edit here: the objectui#11396 mirror follows once a published `@objectstack/spec` carries this. ## The retirement kit - **Tombstone** on the strict detail entry (`packages/spec/src/ui/component.zod.ts`). The refusal is the prescription, and the key's input type is the retired-key mark, so `tsc` refuses it too. The prescription opens with the fully qualified key: `object-master-detail-form` property `details[].sortField`. It ends with the house `os migrate meta --from 17` sentence. - **One declaration of the derived sort-field names.** `INLINE_GRID_SORT_FIELDS` (the same six names in the same order as the renderer's `SORT_FIELD_NAMES`) moves out of `data/inline-grid-columns.ts` into `data/inline-grid-sort-fields.ts`, a module reached only by relative import, like `ui/action-target-aliases.ts`. Exported from `inline-grid-columns.ts`, it would have reached the published API through the `data` barrel. `inline-grid-columns.ts` imports it, and the tombstone's prescription and `record:line_items`' answer print the list from it (`INLINE_GRID_SORT_FIELD_LIST`). `check:api-surface` is unchanged, as expected. - **D2 conversion `object-master-detail-form-detail-sort-field-removed`** (step 18, `retiredFromLoadPath`, `retiredAfter: '17.6.0'`, order 60). It is a lossless delete of `sortField` from each `properties.details[]` entry of an `object-master-detail-form`, scoped by component type and by position. Its fixture covers region, nested-card and slotted positions, an open-namespace control with the same shape, and an entry that is not an object. - **`RETIRED_KEYS_BY_MAJOR[18]`** registers the nested key `ui/ObjectMasterDetailFormProps:details.sortField` (an array member, spelled without its `[]`). It has no `authorable-surface/` line, so checks (b2) and (b3) resolve it against the emitted schema, and `check:authorable-surface` is green on it. - **D3 entry `object-master-detail-form-detail-sort-field-retired`**, plus a `STEP18_RATIONALE` fragment at order 70. - **Texts that became false, changed in this PR:** - the entry docblock: "exactly the twelve keys" becomes eleven plus the tombstone, re-read at the pin, and "the three it adds … and `sortField`" now says the line-position field is derived; - `record:line_items`' guidance for `sortField`, which used to say the key "belongs to an `object-master-detail-form` detail entry" and now says no block takes it; - the generated reference page row (`content/docs/references/ui/component.mdx`); - the carriers test's every-key control (`FULL_DETAIL_ENTRY`, now 11 keys); - two landed step-18 D3 entries. `ui-object-master-detail-form-details-closed` listed `sortField?` among the keys to write, and `ui-record-line-items-props-closed` sent `sortField` to the detail entry. Both entries shipped in 17.6.0's `registry.ts`. Step 18 is still open, so the 17 → 18 upgrade guide will be generated from these texts. Reviewer, please confirm that this correction belongs in this PR. - **Changeset** `@objectstack/spec` `minor`, with a BREAKING banner, the FROM → TO table and the ADR-0087 `registered` disposition. ## Premise checks (measured on origin/main 9a4182a, objectui at the pin 89cad75d5570) - Locations: the key was at `component.zod.ts:5566`, the docblock at `:5539` and the `record:line_items` guidance at `:2201`–`:2202`, as the claim said. - Renderer: `MasterDetailForm.tsx:83` says "⛔ No `sortField` member". The only `sortField` on that path is the derived one (`:372`, `:874`, `:1066`, `:1081`; `deriveMasterDetail.ts:55` and `:540`). A fully configured entry (FK plus every column typed) loads no child schema (`:977`–`:979`), so it stamps no line position. The prescription says this. - Writer census, `git grep -n -w sortField` over `examples apps packages skills content/docs`, excluding `packages/spec/**` and CHANGELOGs: 1 hit, the generated reference page. Same instrument, control `addLabel`: 6 hits, including the showcase project workspace's detail entry. At the objectui pin, the only detail entries that write the key are probes asserting that nothing reads it (`masterDetailDetailsMembers-8071.test.tsx` rows 2c and `:295`/`:304`). No objectui source assigns it through a spec type. - Landing site: `packages/spec` is the producer of the declared key. No other package was involved. ## Evidence All on the final head `f765e8caaf` unless noted. The branch merged `origin/main` three times: at `e367002e11` (which carries the sibling element:text landing `36ad3210d4`), at `5b5e83f446` (which carries #21622's landing `3f1bc816a2`), and at `15fe567c9c` (which carries #21625's pin bump `1cbe165bfc` and #21642's `15fe567c9c`). Readings marked `69f42d393d` predate the second merge. - `@objectstack/spec` local project at `f765e8caaf`: 609 files and 18059 tests passed (1 todo), with no skip. - `@objectstack/spec` repo project at `f765e8caaf`: the three merge-shape and retirement files (`step18-rationale-merge`, `conversions-major18-merge` and `master-detail-detail-sort-field-retirement`) passed 41 tests. The wider repo-project run is a `69f42d393d` reading: 52 of 53 files passed. NOT MEASURED: `scripts/build-schemas-check-mode.test.ts`, which alone exceeds the 590 s foreground cap. `check:authorable-surface`, the gate whose mode it tests, is green at `f765e8caaf`. - `pnpm --filter @objectstack/spec typecheck`: exit 0, including `check:test-typecheck`. The new pin's `@ts-expect-error` is compiled there, so an unused directive would be red. - Consumer packages that author an `object-master-detail-form` `details[]` entry: - `@objectstack/lint` at `f765e8caaf`: `validate-component-props.test.ts` passed 51 tests (including the new advisory pin). At `69f42d393d`, all 119 files and 5621 tests passed, and its typecheck passed; - `@objectstack/example-showcase` at `69f42d393d`: its typecheck passed, and 32 files and 399 tests passed. - `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts are up to date. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 114 commands at `f765e8caaf` (the same set as at `a1b0552acd`), and all 114 exited 0 on the first attempt after a full build. `--ran` reconciled them as 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN. At `a1b0552acd`, the round report that first quoted the same reading had stale provenance; the seat's note `5974199078` on the card corrects it. - ESLint, as a proven narrowing rather than the repo-wide `pnpm lint`: - Population: `eslint.config.mjs` lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`. - `--no-inline-config --format json` over the 12 changed lintable files at the final head `f765e8caaf` reported 12 files, 0 errors and 0 warnings (no ignored-file warnings, so all 12 are in the population). An earlier run, before the two D3 entry text edits, read 10 files with the same result. - Invariance: the config never enables type-aware linting (no `parserOptions.project` and no `projectService`), and the diff touches neither the config nor its baselines, so no verdict on an untouched file can move. - Ablations: one-shot runs through `scripts/ablation-replace.mjs`, wrapped in a trap. Each restore is proven by blob == HEAD and an empty `git diff HEAD`. - The tombstone reverted to `z.string().optional()`: 5 of the pin's tombstone rows went red (refusal, list, props-lint row, tombstone-not-strict-arm, tsc channel), and 15 stayed green. - `sortField: 'position'` planted on the showcase workspace's detail entry: the tree-scoped absence walk went red, naming `examples/app-showcase/src/ui/pages/project-workspace.page.ts authors sortField: 'position'`. ## New pins - `packages/spec/src/ui/master-detail-detail-sort-field-retirement.test.ts`, in the repo project because it walks the tree. It covers: - the tombstone: issue `code` `invalid_type`, path `details.0.sortField`, the prescription, and the derived names drawn from the one declaration; - that the props-lint row is the same schema; - the tombstone against the strict-arm control; - the walked shape keeping the key; - the tsc channel; - that the page parse never refuses; - `record:line_items`' answer; - the D2 strip: stored row, built artifact in every position, by-reference control, type and position scoping, idempotence, retired from the load path; - the ledger row and the D3 entry; - a tree-scoped absence walk over the radius `@objectstack/spec` already declares. `check:cross-package-test-inputs` is green, and the test is listed in `vitest.repo-tests.json`. - `packages/lint/src/validate-component-props.test.ts`: a detail-entry `sortField` is a `component-props-invalid` warning at `properties.details.1.sortField`, carrying the prescription. ## Acceptance notes - The D3 entry's `replacement` spells out the six derived names literally. The generator copies entry values into `registry.ts` and keeps no imports, so it cannot read the declaration. The pin asserts that every member of `INLINE_GRID_SORT_FIELDS` appears in it. - Deployed metadata is NOT MEASURED. Stored pages are converted by the D2 strip when they are read, and the props lint reports live sources. - Merge order with this seat's siblings: the element:text variant retirement (`36ad3210d4`) landed first at conversion order 59 and rationale order 68. #21622 (`3f1bc816a2`) landed next with rationale order 69. This PR takes 60 and 70. All three merges went through `scripts/pm/os-regen-merge.sh`. Each time, the os-regen driver kept this branch's copy of `component.mdx` and dropped main's rows: the variant row, then #21622's `object-metric` rows, then #21625's `object-grid` `keyboardNavigation` row. Each time, the page was regenerated from the merged source in its own commit (the last is `f765e8caaf`). Measured by comparing change lines, the head's delta against `main` is this PR's own delta at `a1b0552acd`, file by file; and the head's delta against `a1b0552acd` is `main`'s own delta. #21625 did not touch the detail entry or its docblock. - CI at `f765e8caaf`: complete and green, 33 success and 2 skipped (`Console Pin Gate` and the opt-in packed-tarball smoke, by their own filters). `Lint & Repo Gates`, red at `a1b0552acd` only because GitHub's issues API answered HTTP 503 in `check-issue-citations`, is success on this head. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f40bb32 commit 6ec54f0

15 files changed

Lines changed: 997 additions & 30 deletions
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an `object-master-detail-form` detail entry's `sortField` is retired — the console derives the line-position field from the child object and reads no authored value (#21589)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered object-master-detail-form-detail-sort-field-removed, object-master-detail-form-detail-sort-field-retired -->
10+
11+
**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
`ComponentPropsMap['object-master-detail-form'].details[].sortField` named the child field the line grid stamps with each line's position on drag-reorder. The console stopped reading it: the field it stamps is derived from the child object, and the pinned console crossed that change while the spec still declared the key. So an authored `sortField` went through `os validate` clean and was dropped, and a drag-reorder stamped the derived field, or none (ADR-0049 enforce-or-remove).
14+
15+
### FROM → TO
16+
17+
| before | what to write instead |
18+
| --- | --- |
19+
| `details: [{ childObject: 'crm_invoice_line', sortField: 'line_no' }]` | delete `sortField`. The grid stamps the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`. |
20+
| `sortField` naming a field outside that list | give the child object one of those fields; the line order is kept there. |
21+
| an entry that names `relationshipField` and at least one column and gives every column a `type` | unchanged: the renderer keeps that entry exactly as authored, loads no child schema for it, and stamps no line position, before and after the upgrade alike. |
22+
23+
**The one-line fix: delete `sortField` from every `object-master-detail-form` detail entry.** `os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand.
24+
25+
**What an author now sees.** Writing the key fails `tsc` (its input type is the retired-key mark), and `os validate`, `os build` and `os lint` report it as a `component-props-invalid` warning carrying the prescription at `properties.details.N.sortField`. A page that carries it still saves and loads: a page component's `properties` is not parsed on the metadata save or load path.
26+
27+
### The retirement kit
28+
29+
- **Tombstone.** `sortField` is a `retiredKey()` on the strict detail entry. Its prescription prints the derived field names from their one declaration, a module reached by relative import only (`data/inline-grid-sort-fields.ts`), which the derived inline-grid columns read too.
30+
- **D2 conversion `object-master-detail-form-detail-sort-field-removed`** (step 18, retired from the load path): a lossless delete of `sortField` from every `properties.details[]` entry of an `object-master-detail-form`, scoped by component type and by position. Stored `sys_metadata` pages and built artifacts replay it, one notice per entry.
31+
- **D3 entry `object-master-detail-form-detail-sort-field-retired`** carries the judgment the delete cannot make: whether the child object declares the field the line order is kept in.
32+
- **`RETIRED_KEYS_BY_MAJOR[18]`** registers the nested key `ui/ObjectMasterDetailFormProps:details.sortField`.
33+
- **`record:line_items`' answer to `sortField`** no longer sends the author to the detail entry: no block takes an authored `sortField` any more.
34+
- **No deprecation window**: the writer census is zero.
35+
36+
**Measured producers: none.** On origin/main 9a4182a752, no `object-master-detail-form` detail entry in `examples/`, `apps/`, `packages/`, `skills/` or `content/docs/` writes `sortField`, against the sibling detail-entry key `addLabel` on the showcase project workspace's entry as the control, through the same instrument. At the objectui pin `89cad75d5570` the only detail entries that write it are probes asserting that nothing reads it. Deployed metadata NOT MEASURED.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1084,7 +1084,7 @@ Sort field and direction pair
10841084
| **formFields** | `string[]` | optional | Child field names for the per-row expand form. When omitted they are derived from the child object's fields — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is derived, and the per-row form is offered only when `inlineMode` is 'form', where it draws the child object's full field list |
10851085
| **inlineMode** | `Enum<'grid' \| 'form'>` | optional | Inline-edit form factor: 'grid' = editable cells; 'form' = read-only list + per-row full form. When omitted it is resolved from the relationship field's `inlineEdit`, else from the child object's shape — except on an entry that names both `relationshipField` and at least one column, which is kept as authored: nothing is resolved, the collection renders as a grid, and the per-row form is offered only when `formFields` lists more fields than `columns` |
10861086
| **amountField** | `string` | optional | Numeric child column summed for the running total and the `totalField` rollup. When omitted it is picked from the grid's number and currency columns: a computed one, else one named `amount`, `total`, `subtotal`, `line_total`, `line_amount` or `net_amount`, else the last currency column, else the last numeric one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored, so nothing is picked. With no `amountField` authored or picked, the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set |
1087-
| **sortField** | `string` | optional | Child field holding the line sort position, stamped on drag-reorder. When omitted it is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, if it has one — except on an entry that names `relationshipField` and at least one column and gives every column a `type`. The renderer keeps that entry exactly as authored: nothing is derived, the grid stamps no line position, and a drag-reorder is not saved |
1087+
| **sortField** | `never` | optional | [REMOVED] `object-master-detail-form` property `details[].sortField` was removed in @objectstack/spec 17 (ADR-0087 D2) — the console reads no authored value: the field the line grid stamps with each line's position on drag-reorder is derived from the child object, so an authored `sortField` was accepted and dropped. Delete the key. For a drag-reorder to be saved, give the child object a field named `position` / `sort_order` / `sequence` / `line_no` / `line_number` / `sort`: the renderer stamps the child's first field with one of those names — except on an entry that names `relationshipField` and at least one column and gives every column a `type`, which the renderer keeps exactly as authored and stamps no line position on. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
10881088
| **totalField** | `string` | optional | Parent field to receive the rolled-up sum |
10891089
| **title** | `string` | optional | Section title |
10901090
| **minRows** | `number` | optional | Minimum number of rows |

‎packages/lint/src/validate-component-props.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,32 @@ describe('validateComponentProps — undeclared keys', () => {
9191
}
9292
});
9393

94+
// #21589 — an `object-master-detail-form` detail entry's `sortField` is a
95+
// retiredKey tombstone, one array level down. The same door, the same
96+
// warning: the finding names the entry's key, and the page is never refused.
97+
it('reports a retired detail-entry `sortField` as a warning carrying the prescription, at the entry\'s key', () => {
98+
const findings = validateComponentProps(
99+
stackWith([
100+
{
101+
type: 'object-master-detail-form',
102+
properties: {
103+
objectName: 'invoice',
104+
details: [
105+
{ title: 'Payments', childObject: 'invoice_payment' },
106+
{ title: 'Lines', childObject: 'invoice_line', sortField: 'line_no' },
107+
],
108+
},
109+
},
110+
]),
111+
);
112+
expect(findings).toHaveLength(1);
113+
const [f] = findings;
114+
expect(f.severity).toBe('warning');
115+
expect(f.rule).toBe(COMPONENT_PROPS_INVALID);
116+
expect(f.path).toBe('pages[0].regions[0].components[0].properties.details.1.sortField');
117+
expect(f.message).toContain('`object-master-detail-form` property `details[].sortField` was removed in @objectstack/spec 17');
118+
});
119+
94120
it('walks components nested inside `properties` (tabs items → children)', () => {
95121
const findings = validateComponentProps(
96122
stackWith([

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

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12473,6 +12473,208 @@ const dashboardWidgetChartConfigStructureRemoved: MetadataConversion = {
1247312473
},
1247412474
};
1247512475

12476+
/**
12477+
* `object-master-detail-form`'s detail entry `sortField` leaves the contract
12478+
* (protocol 18, #21589 — ADR-0049 enforce-or-remove; the spec half of
12479+
* objectui#11070 round 9, the direction recorded on #21220's landing and
12480+
* mirrored on objectui#11396 ③: tombstone plus ADR-0087).
12481+
*
12482+
* **A pure lossless delete.** The console stopped reading the authored
12483+
* override at objectui `0a3e5409f`, and the `.objectui-sha` pin
12484+
* (`89cad75d5570`) is past it: `MasterDetailDetailConfig` has no `sortField`
12485+
* member (`plugin-form/src/MasterDetailForm.tsx:83`), and the field the line
12486+
* grid stamps with each line's position is the one `deriveDetail` derives from
12487+
* the child object (`deriveMasterDetail.ts:540`), handed to the grid as
12488+
* `sort_field` (`:874`). An authored value changed nothing on that console, so
12489+
* deleting it preserves observed behaviour exactly; the line order is kept by
12490+
* the child object's own field, which the entry's tombstone names. The
12491+
* tombstone refuses the key for a live author (advisory, via the props lint:
12492+
* `PageComponentSchema.properties` is an open bag).
12493+
*
12494+
* ⚠️ Scoped by component `type` and by POSITION — `properties.details[]` of an
12495+
* `object-master-detail-form` — never by key name: `sortField` is an ordinary
12496+
* name for an open-namespace component's own prop, and the fixture's
12497+
* non-carrier control is such a component authoring the same shape. A
12498+
* `details` entry that is not an object rides through untouched (the props
12499+
* gate reports it; it is not this entry's to fix).
12500+
*
12501+
* Zero authored occurrences in this repo's corpora at the retirement — no
12502+
* detail entry under `examples/`, `apps/`, `packages/`, `skills/` or
12503+
* `content/docs/` writes the key (control: the sibling detail-entry key
12504+
* `addLabel`, authored on the showcase project workspace's entry, same
12505+
* instrument, origin/main 9a4182a752) — so this entry exists for stored
12506+
* `sys_metadata` rows and for authors outside the repo, which the census could
12507+
* not measure.
12508+
*/
12509+
const objectMasterDetailFormDetailSortFieldRemoved: MetadataConversion = {
12510+
id: 'object-master-detail-form-detail-sort-field-removed',
12511+
toMajor: 18,
12512+
retiredFromLoadPath: true,
12513+
retiredAfter: '17.6.0',
12514+
surface: 'page.component.object-master-detail-form.details[].sortField',
12515+
summary:
12516+
"object-master-detail-form detail entry prop 'sortField' removed (the console reads no authored "
12517+
+ 'value: the line grid stamps the field it derives from the child object, so the key was '
12518+
+ "accepted and dropped; delete the key — the child object's own position field keeps the line order)",
12519+
apply(stack, emit) {
12520+
return mapPageComponents(stack, (component, path) => {
12521+
if (component.type !== 'object-master-detail-form') return component;
12522+
const properties = component.properties;
12523+
if (!isDict(properties) || !Array.isArray(properties.details)) return component;
12524+
const details = properties.details as unknown[];
12525+
let changed = false;
12526+
const nextDetails = details.map((entry, i) => {
12527+
if (!isDict(entry) || !('sortField' in entry)) return entry;
12528+
changed = true;
12529+
return stripKeys(entry, ['sortField'], emit, `${path}.properties.details[${i}]`);
12530+
});
12531+
if (!changed) return component;
12532+
return { ...component, properties: { ...properties, details: nextDetails } };
12533+
});
12534+
},
12535+
fixture: {
12536+
before: {
12537+
pages: [
12538+
{
12539+
name: 'invoice_entry',
12540+
regions: [
12541+
{
12542+
name: 'main',
12543+
components: [
12544+
// The carrier: a detail entry authoring the retired override,
12545+
// beside an entry WITHOUT it, which rides through untouched.
12546+
{
12547+
type: 'object-master-detail-form',
12548+
id: 'm1',
12549+
properties: {
12550+
objectName: 'crm_invoice',
12551+
details: [
12552+
{ childObject: 'crm_invoice_line', title: 'Lines', sortField: 'line_no' },
12553+
{ childObject: 'crm_invoice_payment', title: 'Payments' },
12554+
],
12555+
},
12556+
},
12557+
// ⚠️ The same shape on a component that is NOT an
12558+
// `object-master-detail-form` — its own prop, not this entry's
12559+
// key. Untouched: the strip is scoped by component type.
12560+
{
12561+
type: 'acme:line_editor',
12562+
id: 'x1',
12563+
properties: { details: [{ childObject: 'crm_invoice_line', sortField: 'position' }] },
12564+
},
12565+
// A block whose entries carry no `sortField`, and one with a
12566+
// non-object entry: both ride through untouched, by reference.
12567+
{
12568+
type: 'object-master-detail-form',
12569+
id: 'm2',
12570+
properties: { objectName: 'crm_order', details: [{ childObject: 'crm_order_line' }, 'crm_order_note'] },
12571+
},
12572+
// The nested position (#6775's lesson): a block inside a
12573+
// card's `children` is still a component.
12574+
{
12575+
type: 'page:card',
12576+
id: 'c1',
12577+
properties: {
12578+
children: [
12579+
{
12580+
type: 'object-master-detail-form',
12581+
id: 'm3',
12582+
properties: {
12583+
objectName: 'crm_quote',
12584+
details: [{ childObject: 'crm_quote_line', sortField: 'position' }],
12585+
},
12586+
},
12587+
],
12588+
},
12589+
},
12590+
],
12591+
},
12592+
],
12593+
},
12594+
// The named-slot shape (#6776): the block authored into a slotted page.
12595+
{
12596+
name: 'invoice_entry_detail',
12597+
kind: 'slotted',
12598+
regions: [],
12599+
slots: {
12600+
details: {
12601+
type: 'object-master-detail-form',
12602+
id: 'm4',
12603+
properties: {
12604+
objectName: 'crm_invoice',
12605+
details: [{ childObject: 'crm_invoice_line', sortField: 'sequence' }],
12606+
},
12607+
},
12608+
},
12609+
},
12610+
],
12611+
},
12612+
after: {
12613+
pages: [
12614+
{
12615+
name: 'invoice_entry',
12616+
regions: [
12617+
{
12618+
name: 'main',
12619+
components: [
12620+
{
12621+
type: 'object-master-detail-form',
12622+
id: 'm1',
12623+
properties: {
12624+
objectName: 'crm_invoice',
12625+
details: [
12626+
{ childObject: 'crm_invoice_line', title: 'Lines' },
12627+
{ childObject: 'crm_invoice_payment', title: 'Payments' },
12628+
],
12629+
},
12630+
},
12631+
{
12632+
type: 'acme:line_editor',
12633+
id: 'x1',
12634+
properties: { details: [{ childObject: 'crm_invoice_line', sortField: 'position' }] },
12635+
},
12636+
{
12637+
type: 'object-master-detail-form',
12638+
id: 'm2',
12639+
properties: { objectName: 'crm_order', details: [{ childObject: 'crm_order_line' }, 'crm_order_note'] },
12640+
},
12641+
{
12642+
type: 'page:card',
12643+
id: 'c1',
12644+
properties: {
12645+
children: [
12646+
{
12647+
type: 'object-master-detail-form',
12648+
id: 'm3',
12649+
properties: { objectName: 'crm_quote', details: [{ childObject: 'crm_quote_line' }] },
12650+
},
12651+
],
12652+
},
12653+
},
12654+
],
12655+
},
12656+
],
12657+
},
12658+
{
12659+
name: 'invoice_entry_detail',
12660+
kind: 'slotted',
12661+
regions: [],
12662+
slots: {
12663+
details: {
12664+
type: 'object-master-detail-form',
12665+
id: 'm4',
12666+
properties: { objectName: 'crm_invoice', details: [{ childObject: 'crm_invoice_line' }] },
12667+
},
12668+
},
12669+
},
12670+
],
12671+
},
12672+
// Three notices: the region-level entry, the nested one and the slotted
12673+
// one. The open-namespace sibling and the entries without the key emit none.
12674+
expectedNotices: 3,
12675+
},
12676+
};
12677+
1247612678
/**
1247712679
* `object.tenancy.organizationField` leaves the authorable surface (protocol
1247812680
* 18, #19054 — ADR-0049 enforce-or-remove; maintainer ruling 2026-09-18,
@@ -14598,6 +14800,7 @@ const MAJOR_18_CONVERSIONS: readonly OrderedConversion[] = [
1459814800
{ conversion: objectGridDefaultSortRemoved, order: 14 },
1459914801
{ conversion: objectGridResizableColumnsRemoved, order: 57 },
1460014802
{ conversion: objectKanbanQuickAddRemoved, order: 15 },
14803+
{ conversion: objectMasterDetailFormDetailSortFieldRemoved, order: 60 },
1460114804
{ conversion: objectTenancyOrganizationFieldRemoved, order: 35 },
1460214805
{ conversion: pageAssignedProfilesRemoved, order: 31 },
1460314806
{ conversion: pageComponentFilterRecordToRuleArray, order: 36 },

‎packages/spec/src/data/inline-grid-columns.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
* no expand control.
8787
*/
8888

89+
// The sort-position names live in their own module, reached by relative
90+
// import only: the retired `object-master-detail-form` detail entry
91+
// `sortField`'s prescription prints the same list (`ui/component.zod.ts`).
92+
import { INLINE_GRID_SORT_FIELDS } from './inline-grid-sort-fields';
93+
8994
/** Default-visible column budget of a derived inline grid; the rest are `defaultHidden`. */
9095
export const DEFAULT_MAX_INLINE_GRID_COLUMNS = 6;
9196

@@ -107,11 +112,6 @@ const INLINE_GRID_SYSTEM_FIELDS: ReadonlySet<string> = new Set([
107112
'organization_id', 'tenant_id', 'space', 'owner',
108113
]);
109114

110-
/** Field names that hold a line's sort position: the grid stamps them on drag-reorder. */
111-
const INLINE_GRID_SORT_FIELDS: ReadonlySet<string> = new Set([
112-
'position', 'sort_order', 'sequence', 'line_no', 'line_number', 'sort',
113-
]);
114-
115115
/** Field types a line-item cell cannot edit. File-family types are absent: they render an upload cell. */
116116
const INLINE_GRID_NON_EDITABLE_TYPES: ReadonlySet<unknown> = new Set([
117117
'formula', 'summary', 'rollup', 'autonumber', 'auto_number',

0 commit comments

Comments
 (0)