From 6843f37d9e8a23d13946c8e6a5a7f9a185d11976 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 18:34:15 +0000 Subject: [PATCH 1/7] fix(app-showcase): key the project Tasks grid's five line-item columns by `name` The `record:line_items` block on the project detail page wrote its columns keyed `field`, while the line-items grid binds a column by `name`, so every cell rendered empty. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- .../app-showcase/src/ui/pages/project-detail.page.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/app-showcase/src/ui/pages/project-detail.page.ts b/examples/app-showcase/src/ui/pages/project-detail.page.ts index f2c6bf9e3e2..fc51fa1c1b4 100644 --- a/examples/app-showcase/src/ui/pages/project-detail.page.ts +++ b/examples/app-showcase/src/ui/pages/project-detail.page.ts @@ -76,9 +76,9 @@ export const ProjectDetailPage = definePage({ amountField: 'estimate_hours', title: 'Tasks', columns: [ - { field: 'title', label: 'Title', type: 'text', required: true }, + { name: 'title', label: 'Title', type: 'text', required: true }, { - field: 'status', + name: 'status', label: 'Status', type: 'select', options: [ @@ -90,7 +90,7 @@ export const ProjectDetailPage = definePage({ ], }, { - field: 'priority', + name: 'priority', label: 'Priority', type: 'select', options: [ @@ -100,8 +100,8 @@ export const ProjectDetailPage = definePage({ { label: 'Urgent', value: 'urgent' }, ], }, - { field: 'estimate_hours', label: 'Estimate (h)', type: 'number' }, - { field: 'due_date', label: 'Due Date', type: 'date' }, + { name: 'estimate_hours', label: 'Estimate (h)', type: 'number' }, + { name: 'due_date', label: 'Due Date', type: 'date' }, ], }, }, From e3f28a16163ae719666869b8a83f6f4c2910668f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 18:37:44 +0000 Subject: [PATCH 2/7] feat(spec)!: record:line_items gets its ComponentPropsMap row; its columns are the inline grid column contract The type leaves the string-arm registration ledger, which is now empty. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- .../src/inline-grid-column-carriers.test.ts | 158 +++++++++++++++++- .../src/ui/component-type-vocabulary.test.ts | 23 ++- .../spec/src/ui/component-type-vocabulary.ts | 32 ++-- packages/spec/src/ui/component.zod.ts | 139 +++++++++++++++ 4 files changed, 331 insertions(+), 21 deletions(-) diff --git a/packages/spec/src/inline-grid-column-carriers.test.ts b/packages/spec/src/inline-grid-column-carriers.test.ts index 3697867071f..4dcfe89ddb9 100644 --- a/packages/spec/src/inline-grid-column-carriers.test.ts +++ b/packages/spec/src/inline-grid-column-carriers.test.ts @@ -4,7 +4,8 @@ * #20901 — an inline grid column is judged on BOTH of its carriers, and by the * type it renders as. #20928 brought the THIRD carrier, an * `object-master-detail-form` page block's `details[].columns`, to the same - * contract (its own section at the bottom of this file). + * contract (its own section near the bottom of this file), and #21142 the + * FOURTH, a `record:line_items` page block's `columns` (the last section). * * The objectui master-detail grid reads one column shape from two carriers: a * relationship field's `inlineColumns` and a form view's `subforms[].columns`. @@ -595,3 +596,158 @@ describe('#20928 — defineStack judges an identity-only detail column by the ty expect(() => build(stackWithMasterDetailColumns([{ name: 'amount' }]))).not.toThrow(); }); }); + +// --------------------------------------------------------------------------- +// #21142 — the fourth carrier: a `record:line_items` block's `columns`. +// --------------------------------------------------------------------------- + +/** + * The page block `record:line_items` draws ONE inline grid of the record's + * child rows, through the same objectui grid as the other three carriers. It + * had no `ComponentPropsMap` row, so the component-props gate skipped it and + * the showcase project page's five `field`-keyed columns published green over + * a grid of empty cells. + * + * One difference from the master-detail block is pinned on purpose: this + * panel hands `columns` to the grid as authored, with no hydration from the + * child object's field. So an identity-only column has no resolved type for + * `defineStack` to judge, and the cross-reference check does not reach this + * block — the last test below is that control. + */ +const LINE_ITEMS_PROPS = ComponentPropsMap['record:line_items']; + +/** Every key objectui's `LineItemsPanel` reads off its schema (the `.objectui-sha` pin `31971ff1e28f`). */ +const FULL_LINE_ITEMS = { + childObject: 'crm_invoice_line', + relationshipField: 'invoice', + columns: [{ name: 'quantity', label: 'Qty', type: 'number' }], + parentObject: 'crm_invoice', + parentId: 'inv_1', + recordId: 'inv_1', + amountField: 'amount', + totalField: 'total', + title: 'Lines', + readonly: false, + minRows: 1, + maxRows: 20, + filter: [{ field: 'quantity', operator: 'greater_than', value: 0 }], + sort: [{ field: 'quantity', order: 'asc' }], + limit: 100, +} as const; + +const parseLineItems = (props: unknown): Result => LINE_ITEMS_PROPS.safeParse(props) as Result; + +const parseLineItemColumns = (columns: unknown[]): Result => + parseLineItems({ childObject: 'crm_invoice_line', relationshipField: 'invoice', columns }); + +describe('#21142 — the line-items block is strict, and its columns are the column contract', () => { + it('its column element IS InlineGridColumnSchema — one contract, not a copy', () => { + const shape = (LINE_ITEMS_PROPS as unknown as { shape: Record }).shape; + expect(shape.columns.element).toBe(InlineGridColumnSchema); + }); + + it('refuses the card\'s `field`-keyed column by name, with the prescription naming `name`', () => { + const result = parseLineItemColumns([{ field: 'title', label: 'Title', type: 'text' }]); + expect(result.success).toBe(false); + const unknown = result.error!.issues.find((i) => i.code === 'unrecognized_keys'); + expect(unknown?.path).toEqual(['columns', 0]); + expect(unknown?.message).toContain('`field` → `name`'); + // The column's identity is missing too — the grid would bind nothing. + expect(result.error!.issues.some((i) => i.code === 'invalid_type' && i.path.join('.') === 'columns.0.name')).toBe(true); + }); + + it('refuses the typed currency column carrying `scale`, at the column\'s `scale`, with the ruled first sentence', () => { + const result = parseLineItemColumns([{ name: 'quantity' }, TYPED_CURRENCY_WITH_SCALE]); + expect(result.success).toBe(false); + expect(result.error!.issues).toHaveLength(1); + const [issue] = result.error!.issues; + expect(issue.code).toBe('custom'); + expect(issue.path).toEqual(['columns', 1, 'scale']); + expect(issue.message.startsWith(COLUMN_FIRST_SENTENCE)).toBe(true); + }); + + it('refuses a bogus-key column: the key is named, and the missing `name` is required', () => { + const result = parseLineItemColumns([BOGUS_KEY_ONLY]); + expect(result.success).toBe(false); + const unknown = result.error!.issues.find((i) => i.code === 'unrecognized_keys'); + expect(unknown?.path).toEqual(['columns', 0]); + expect(unknown?.message).toContain('`zzz_not_a_key`'); + }); + + it('requires `relationshipField` and at least one column — nothing on this panel derives either', () => { + const noFk = parseLineItems({ childObject: 'crm_invoice_line', columns: [{ name: 'quantity' }] }); + expect(noFk.success).toBe(false); + expect(noFk.error!.issues.some((i) => i.code === 'invalid_type' && i.path.join('.') === 'relationshipField')).toBe(true); + + const noColumns = parseLineItems({ childObject: 'crm_invoice_line', relationshipField: 'invoice' }); + expect(noColumns.success).toBe(false); + expect(noColumns.error!.issues.some((i) => i.code === 'invalid_type' && i.path.join('.') === 'columns')).toBe(true); + + const emptyColumns = parseLineItemColumns([]); + expect(emptyColumns.success).toBe(false); + expect(emptyColumns.error!.issues.some((i) => i.code === 'too_small' && i.path.join('.') === 'columns')).toBe(true); + }); + + it('refuses a block key the renderer does not read, a near-miss with its rename, and a master-detail entry key with its reason', () => { + const bogus = parseLineItems({ ...FULL_LINE_ITEMS, zzz_not_a_key: 1 }); + expect(bogus.success).toBe(false); + const unknown = bogus.error!.issues.find((i) => i.code === 'unrecognized_keys'); + expect(unknown?.path).toEqual([]); + expect(unknown?.message).toContain('`zzz_not_a_key`'); + + const alias = parseLineItems({ childObject: 'crm_invoice_line', foreignKey: 'invoice', columns: [{ name: 'quantity' }] }); + expect(alias.success).toBe(false); + expect(alias.error!.issues.find((i) => i.code === 'unrecognized_keys')?.message).toContain('`foreignKey` → `relationshipField`'); + + const entryKey = parseLineItems({ ...FULL_LINE_ITEMS, addLabel: 'Add line' }); + expect(entryKey.success).toBe(false); + const refused = entryKey.error!.issues.find((i) => i.code === 'unrecognized_keys'); + expect(refused?.message).toContain('`addLabel`'); + expect(refused?.message).toContain('object-master-detail-form'); + }); + + it('CONTROLS — the showcase\'s five name-keyed columns, and a bag carrying every key the renderer reads, parse and keep their keys', () => { + // The showcase project page's block, copied (a spec test does not import an example app). + const showcase = { + childObject: 'showcase_task', + relationshipField: 'project', + amountField: 'estimate_hours', + title: 'Tasks', + columns: [ + { name: 'title', label: 'Title', type: 'text', required: true }, + { name: 'status', label: 'Status', type: 'select', options: [{ label: 'Backlog', value: 'backlog' }] }, + { name: 'priority', label: 'Priority', type: 'select', options: [{ label: 'Low', value: 'low' }] }, + { name: 'estimate_hours', label: 'Estimate (h)', type: 'number' }, + { name: 'due_date', label: 'Due Date', type: 'date' }, + ], + }; + for (const props of [showcase, FULL_LINE_ITEMS]) { + const result = parseLineItems(props); + expect(result.success, JSON.stringify(result.error?.issues)).toBe(true); + expect(result.data).toEqual(props); + } + expect(Object.keys(FULL_LINE_ITEMS)).toHaveLength(15); + // `childObject` may come from the component-level `dataSource` binding instead. + const viaBinding = Object.fromEntries(Object.entries(FULL_LINE_ITEMS).filter(([key]) => key !== 'childObject')); + expect(parseLineItems(viaBinding).success).toBe(true); + }); + + it('CONTROL — defineStack does not judge an identity-only line-items column: the panel draws it unhydrated', () => { + const page = { + name: 'crm_invoice_record', + label: 'Invoice', + type: 'record' as const, + object: 'crm_invoice', + regions: [{ + name: 'main', + components: [{ + type: 'record:line_items', + properties: { childObject: 'crm_invoice_line', relationshipField: 'invoice', columns: [IDENTITY_ONLY_WITH_SCALE] }, + }], + }], + }; + expect(() => build({ manifest, objects: [PARENT, childObject()], pages: [page] })).not.toThrow(); + // The same column under the master-detail block IS judged — the reach is the carrier's. + expect(() => build(stackWithMasterDetailColumns([IDENTITY_ONLY_WITH_SCALE]))).toThrow(); + }); +}); diff --git a/packages/spec/src/ui/component-type-vocabulary.test.ts b/packages/spec/src/ui/component-type-vocabulary.test.ts index 336a23a38c4..53247b9e0ef 100644 --- a/packages/spec/src/ui/component-type-vocabulary.test.ts +++ b/packages/spec/src/ui/component-type-vocabulary.test.ts @@ -137,9 +137,9 @@ describe('STRING_ARM_REGISTERED_TYPES ledger discipline', () => { // type — a ledger row there is dead weight. expect(hasReservedComponentNamespace(entry), entry).toBe(true); // An enum member or a map row is already known — a ledger row for one is - // a grandfather clause pretending to be an exemption. When - // `record:line_items` is measured into the map, this assertion forces - // its ledger row OUT in the same PR. + // a grandfather clause pretending to be an exemption. This assertion + // forced `record:line_items`'s ledger row OUT in the change that measured + // it into the map (#21142). expect(enumSet.has(entry), entry).toBe(false); expect(mapKeys.has(entry), entry).toBe(false); } @@ -150,6 +150,23 @@ describe('STRING_ARM_REGISTERED_TYPES ledger discipline', () => { expect(isKnownComponentType(entry), entry).toBe(true); } }); + + /** + * #21142 — the ledger's one entry was measured into the map, and the + * discipline above forced it out. `record:line_items` stays KNOWN (the + * `component-type-unknown` rule still accepts it) through its row now, and + * it is still not an enum member: it reaches the type union through the open + * string arm, as `element:metadata_viewer` does. With the ledger empty the + * two loops above hold vacuously, which is why this pin names the type. + */ + it('record:line_items is known through its ComponentPropsMap row, not the ledger', () => { + expect(STRING_ARM_REGISTERED_TYPES).not.toContain('record:line_items'); + expect(Object.keys(ComponentPropsMap)).toContain('record:line_items'); + expect(PageComponentType.options).not.toContain('record:line_items'); + expect(hasReservedComponentNamespace('record:line_items')).toBe(true); + expect(isKnownComponentType('record:line_items')).toBe(true); + expect(KNOWN_COMPONENT_TYPE_CANDIDATES).toContain('record:line_items'); + }); }); describe('the namespace predicate leaves the open arm open', () => { diff --git a/packages/spec/src/ui/component-type-vocabulary.ts b/packages/spec/src/ui/component-type-vocabulary.ts index 99b9820d073..4b282b309e5 100644 --- a/packages/spec/src/ui/component-type-vocabulary.ts +++ b/packages/spec/src/ui/component-type-vocabulary.ts @@ -49,23 +49,21 @@ import { ComponentPropsMap } from './component.zod'; * `REACT_OVERLAY_SHADOWS` house shape: an exemption is deliberate, evidenced, * and written down next to itself, or it does not exist. * - * - `record:line_items` — objectui registers it (`plugin-form/src/index.tsx`, - * the inline-editable child grid of objectui ADR-0001) and lists it as a - * public block (`core/src/registry/public-blocks.ts`); the showcase authors - * it (`examples/app-showcase/src/ui/pages/project-detail.page.ts`). Its - * row-lessness in `ComponentPropsMap` is pinned deliberately in - * `validate-component-props.test.ts` — a registered-but-unmeasured gap of the - * record-blocks class, to be measured into the map by the renderer-read-point - * method, not silently grandfathered by this file. When that row lands, the - * vocabulary test beside this file forces this entry OUT (a ledger row for a - * type the map declares is dead weight). + * EMPTY since #21142. Its one entry, `record:line_items` (objectui's + * inline-editable child grid, registered in `plugin-form/src/index.tsx`, + * listed as a public block, authored by the showcase project page), was + * measured into `ComponentPropsMap` by the renderer-read-point method, and a + * ledger row for a type the map declares is dead weight — the vocabulary test + * beside this file forced it OUT in the same change. The row-less state it + * ledgered was not harmless: the props gate skipped the type as unregistered, + * and the showcase's `field`-keyed columns published green over an empty grid. * * Growing this list is a contract decision, not a convenience: every entry is a - * type the spec's own namespace claim cannot see, so each one needs the same - * three-part evidence face as the entry above (registration, publication, - * authorship) written into its comment. + * type the spec's own namespace claim cannot see, so each one needs a + * three-part evidence face (registration, publication, authorship) written + * into its comment — and a gap measured into the map is the better answer. */ -export const STRING_ARM_REGISTERED_TYPES: readonly string[] = ['record:line_items']; +export const STRING_ARM_REGISTERED_TYPES: readonly string[] = []; /** * The namespaces the enum itself populates — DERIVED, never restated, so a new @@ -81,9 +79,9 @@ export const RESERVED_COMPONENT_TYPE_NAMESPACES: ReadonlySet = new Set( * Every type string the spec answers for: the enum vocabulary, every * `ComponentPropsMap` row (which is a superset of the enum by exactly the * measured string-arm registrations that DID get a row — `element:metadata_viewer`, - * the plugin console widgets, the `object-*` blocks — plus every type the - * vocabulary RETIRED by name, whose row is kept on purpose so the readers that - * dispatch on it keep recognising the name: `user:profile`, and the + * `record:line_items`, the plugin console widgets, the `object-*` blocks — plus + * every type the vocabulary RETIRED by name, whose row is kept on purpose so the + * readers that dispatch on it keep recognising the name: `user:profile`, and the * retired-with-tombstones `element:filter` / `element:form`), and the * string-arm ledger above. * diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 72d6abd987c..c7797b57d14 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -2041,6 +2041,137 @@ export const RecordHistoryProps = strictObject({ }); export type RecordHistoryProps = z.input; +/** + * [#21142] What the missing row cost: `record:line_items` was the one type on + * the string-arm registration ledger (`component-type-vocabulary.ts`), so the + * component-props gate skipped its props bag as unregistered. The showcase + * project page keyed all five of its grid columns `field` — the spelling the + * grid retired — and published green, and the grid drew every cell empty. + */ +const RECORD_LINE_ITEMS_HISTORY = + 'Until this type had a row, the component-props gate skipped it as unregistered: a column keyed ' + + '`field` (the spelling the grid retired) or any other key the renderer does not read parsed ' + + 'clean, and the grid drew its cells empty.'; + +/** + * `record:line_items` (#21142) — the inline-editable child grid bound to the + * record the page shows (objectui ADR-0001). Measured by the renderer + * read-point method at the `.objectui-sha` pin `31971ff1e28f`: objectui + * registers it in `plugin-form/src/index.tsx:579` and renders it through + * `LineItemsPanel` (`plugin-form/src/LineItemsPanel.tsx`), and `SchemaRenderer` + * hoists `properties` onto the schema that component reads. Its fifteen + * `schema.` reads are the key set, and a key nobody reads is not declared: + * + * - `childObject` `:327`, `:516`, `:673`, `:778`; `relationshipField` `:515`, + * `:674`; `columns` `:702`; + * - `parentObject` `:221`; `parentId` / `recordId` `:228` (`parentId` wins, + * then `recordId`, then the record on the page); + * - `amountField` `:669`, `:703`; `totalField` `:667`, `:669`, `:703`; + * - `title` `:722`; `readonly` `:706`, `:707`, `:723`, `:810`; `minRows` + * `:704`; `maxRows` `:705`; + * - `filter` `:366`; `sort` `:368`, `:377`; `limit` `:341`, `:437`. + * + * The wrapper adds no key: `ElementDataSourceGate` + * (`react/src/element-data-source/ElementDataSourceGate.tsx`) reads the + * node-level `dataSource` and the same `filter` / `sort` / `limit` (`:421`, + * `:434`, `:445`), and the block's mapping writes the binding's `object` onto + * `childObject` (`plugin-form/src/index.tsx:556`). The read set is unchanged on + * objectui `main` at `d59f11c0d3dc`. + * + * `filter` declares the one orthography every `filter` door in this map + * shares, the ViewFilterRule array — the `record:related_list.filter` + * declaration. The panel's lowering (`toFilterNodeSafely`) also takes the + * MongoDB-style record and the AST forms; the contract does not. + * + * `columns` IS {@link InlineGridColumnSchema} — the same object a relationship + * field's `inlineColumns`, a form view's `subforms[].columns` and an + * `object-master-detail-form` detail entry's `columns` take, not a copy: the + * panel hands its columns to the same objectui grid (`GridField`, whose + * `GridColumn` declares exactly that schema's twenty keys at the pin and IS + * the spec's type by reference on objectui `main`). One difference is the + * carrier's, not the column's, and the `describe()` says it: this panel does + * NOT hydrate a column from the child object's field — it passes `columns` + * straight through the field-security pass to the grid (`:702`) — so a + * column draws exactly what it declares, and an identity-only `{ name }` + * column is a text cell headed by its name. For the same reason + * `defineStack`'s identity-only check (`collectHydratedInlineColumnErrors` in + * `stack.zod.ts`) does not reach this block: there is no hydrated type to + * judge. + * + * The keys this block shares with an `object-master-detail-form` detail entry + * take that entry's types and alias table, so one concept is spelled one way + * on every child-collection surface. Three of them differ in presence for a + * measured reason: `relationshipField` and `columns` are required here because + * nothing derives them (the entry auto-detects the FK and derives the + * columns; this panel queries `{ [relationshipField]: parentId }` and draws + * `columns` as given), and `childObject` is optional because the + * component-level `dataSource` binding can supply it instead. `title` is a + * plain string because the panel draws it as a React child. + */ +export const RecordLineItemsProps = lazySchema(() => strictObject({ + surface: 'this `record:line_items`', + history: RECORD_LINE_ITEMS_HISTORY, + guidanceSets: COMPONENT_LEVEL_GUIDANCE, + aliases: { + object: 'childObject', childObjectName: 'childObject', child: 'childObject', + foreignKey: 'relationshipField', relationField: 'relationshipField', parentField: 'relationshipField', + fields: 'columns', label: 'title', sumField: 'amountField', rollupField: 'totalField', + ...FILTERS_TO_FILTER, + }, + guidance: { + // The four detail-entry keys this panel does not read — the spellings an + // author moving a child collection over from `object-master-detail-form` + // carries along. Measured at the pin: the panel hands the grid no + // `add_label` / `sort_field` and no `onRowExpand` (`:694-710`, `:806-817`). + addLabel: '`record:line_items` does not read `addLabel`: its grid draws the built-in, localized ' + + 'Add button. `addLabel` belongs to an `object-master-detail-form` detail entry.', + sortField: '`record:line_items` does not read `sortField`: its grid stamps no line position, so a ' + + 'drag-reorder is not saved. `sortField` belongs to an `object-master-detail-form` detail entry.', + formFields: '`record:line_items` draws an editable grid only, with no per-row expand form, so it ' + + 'does not read `formFields`. It belongs to an `object-master-detail-form` detail entry.', + inlineMode: '`record:line_items` draws an editable grid only, so it does not read `inlineMode`. It ' + + 'belongs to an `object-master-detail-form` detail entry.', + }, +}, { + childObject: z.string().optional() + .describe('Child object whose records this grid lists, edits and saves. Optional because the component-level `dataSource` binding can supply the object instead; with neither, the panel shows a configuration hint and fetches nothing'), + relationshipField: z.string() + .describe("FK on the child object pointing back to the parent record — the rows are queried as `{ [relationshipField]: parentId }`. Required: this panel does not auto-detect it"), + columns: z.array(InlineGridColumnSchema).min(1) + .describe("Editable grid columns, drawn exactly as declared. Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes ({ name, label?, type?, options?, … } — objectui GridColumn); unknown keys and the retired `field` spelling are refused. Unlike the master-detail carriers, this block does NOT hydrate a column from the child object's field: declare `label`, `type` and `options` yourself — an identity-only `{ name }` column is a text cell headed by its name. Required, with at least one column: nothing derives them, and an empty list draws a grid with no cells"), + parentObject: z.string().optional() + .describe("Parent object the `totalField` rollup is written to (default: the object of the record on the page)"), + parentId: z.string().optional() + .describe('Parent record id the rows belong to (default: the record on the page). Wins over `recordId`'), + recordId: z.string().optional() + .describe('Alternate spelling of `parentId` the renderer also reads; `parentId` wins when both are set'), + amountField: z.string().optional() + .describe("Numeric child column summed for the running total and for the `totalField` rollup (default `amount` when only `totalField` is set)"), + totalField: z.string().optional() + .describe('Parent field to receive the rolled-up sum on save'), + title: z.string().optional() + .describe('Panel title. A literal string rendered as-is in every locale (no inline locale map — the panel renders it as a React child); renderer default: the localized "Line Items"'), + readonly: z.boolean().optional() + .describe('Render the grid read-only: cells locked, no Save button, and no adding or removing lines (renderer default: editable)'), + minRows: z.number().optional().describe('Minimum number of rows'), + maxRows: z.number().optional().describe('Maximum number of rows'), + filter: z.array(ViewFilterRuleSchema).optional() + .describe('Additional criteria for the child rows — the ViewFilterRule array form `[{ field, operator, value }, ...]`, AND-combined with the parent relationship condition, never substituted for it: it can only narrow this record\'s lines'), + sort: z.array(SortItemSchema).optional() + .describe('Load order for the child rows — the SortItem array form `[{ field, order }, ...]` (renderer default: storage order)'), + limit: z.number().int().positive().optional() + .describe('Row cap for the child fetch (renderer default: 500). The grid has no pagination, so this is the window of rows that are editable and saved together'), +})); +/** Author state (ADR-0122: the bare name is the author state). */ +export type RecordLineItemsProps = z.input; +/** + * Post-parse shape of {@link RecordLineItemsProps} (ADR-0122). Differs from the + * author state because `columns` carries `InlineGridColumnSchema`, whose + * `readonlyWhen` / `requiredWhen` bare-string predicates normalize to + * Expression envelopes at parse. + */ +export type RecordLineItemsPropsParsed = z.infer; + export const PageAccordionProps = strictObject({ surface: 'this `page:accordion`', history: PROPS_HISTORY, @@ -6025,6 +6156,14 @@ export const ComponentPropsMap = { 'record:alert': RecordAlertProps, 'record:quick_actions': RecordQuickActionsProps, 'record:history': RecordHistoryProps, + // #21142 — the last registered `record:*` renderer without a row: it sat on + // the string-arm registration ledger instead (`component-type-vocabulary.ts`), + // so the gate skipped its props and a column keyed `field` published green. + // Not a `PageComponentType` member: it reaches the type union through the + // open string arm, as `element:metadata_viewer` does, and the row is what + // makes it known. Key set measured at the `.objectui-sha` pin; see the + // schema's own header. + 'record:line_items': RecordLineItemsProps, // Navigation 'app:launcher': emptyProps('app:launcher'), From 2479fb678333b6d631ae025d0b6cba468cacece8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 18:43:46 +0000 Subject: [PATCH 3/7] feat(spec,lint): register the record:line_items semantic entry, flip the lint pins, regenerate artifacts Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- content/docs/references/index.mdx | 10 +- content/docs/references/ui/component.mdx | 75 ++++++++++++- .../ui.md | 10 +- .../test/project-detail-line-items.test.ts | 105 ++++++++++++++++++ .../lint/src/validate-component-props.test.ts | 60 +++++++++- packages/lint/src/validate-component-props.ts | 24 ++-- .../lint/src/validate-component-types.test.ts | 8 +- .../src/validate-page-field-bindings.test.ts | 6 +- packages/spec/api-surface/ui.json | 3 + packages/spec/authorable-surface/ui.json | 15 +++ packages/spec/declaration-map/ui.json | 1 + .../spec/dropped-refinements.baseline.json | 10 +- packages/spec/export-origins/ui.json | 2 + packages/spec/json-schema.manifest/ui.json | 1 + .../18.ui-record-line-items-props-closed.ts | 60 ++++++++++ packages/spec/src/migrations/registry.ts | 70 ++++++++++++ packages/spec/src/ui/component.zod.ts | 5 +- 17 files changed, 429 insertions(+), 36 deletions(-) create mode 100644 examples/app-showcase/test/project-detail-line-items.test.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 0f71bf70185..3779f892d51 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1522 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1523 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **197** | **1522** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 167 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **197** | **1523** | 14 protocol modules | --- @@ -364,7 +364,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 166 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 167 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -375,7 +375,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`app.zod.ts`](/docs/references/ui/app) | `ActionNavItem`, `App`, `AppBranding`, `AppContextSelector`, `ComponentNavItem`, `DashboardNavItem`, `DocNavItem`, `GroupNavItem`, `NavigationArea`, `NavigationContribution`, `NavigationItem`, `ObjectNavItem`, `PageNavItem`, `ReportNavItem`, `UrlNavItem` | | [`bulk-action.zod.ts`](/docs/references/ui/bulk-action) | `BulkActionDef`, `BulkActionExecution`, `BulkActionOperation`, `BulkActionParam` | | [`chart.zod.ts`](/docs/references/ui/chart) | `ChartAggregate`, `ChartAggregateFunction`, `ChartAnnotation`, `ChartAxis`, `ChartConfig`, `ChartDrillDown`, `ChartGroupBy`, `ChartInteraction`, `ChartSeries`, `ChartType` | -| [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | +| [`component.zod.ts`](/docs/references/ui/component) | `AIChatWindowProps`, `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordLineItemsProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | | [`dashboard.zod.ts`](/docs/references/ui/dashboard) | `Dashboard`, `DashboardHeader`, `DashboardHeaderAction`, `DashboardWidget`, `DashboardWidgetChartConfig`, `DashboardWidgetOptions`, `GlobalFilter`, `GlobalFilterOptionsFrom`, `WidgetActionType`, `WidgetColorVariant` | | [`dataset.zod.ts`](/docs/references/ui/dataset) | `Dataset`, `DatasetDimension`, `DatasetMeasure`, `DerivedMeasureOp` | | [`expression-bindable-text-keys.zod.ts`](/docs/references/ui/expression-bindable-text-keys) | `ExpressionBindableTextKey` | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index d79e5ff6fc0..ad8283eb3ef 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -1,7 +1,7 @@ --- title: Component schema — UI Protocol reference navTitle: Component -description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProps and 43 more — each property with its type, default and a TypeScript example." +description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProps and 44 more — each property with its type, default and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -13,8 +13,8 @@ description: "Component schemas of the ObjectStack UI Protocol: AIChatWindowProp ## TypeScript Usage ```typescript -import { AIChatWindowProps, ActionButtonPropsSchema, ActionGroupPropsSchema, ActionIconPropsSchema, ActionMenuPropsSchema, ElementButtonPropsSchema, ElementDefinitionListPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementRepeaterPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui'; -import type { ActionButtonProps, ActionGroupProps, ActionIconProps, ActionMenuProps, ElementDefinitionListProps, ElementNumberProps, ElementRecordPickerProps, ElementRepeaterProps, ObjectCalendarProps, ObjectFormProps, ObjectGanttProps, ObjectGridProps, ObjectKanbanProps, ObjectMapProps, ObjectMasterDetailFormProps, ObjectMetricProps, ObjectTimelineProps, ObjectTreeProps, PageContainerProps, RecordAlertAction, RecordAlertProps, RecordHighlightsField, RecordHistoryProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, ReferenceRailEntry } from '@objectstack/spec/ui'; +import { AIChatWindowProps, ActionButtonPropsSchema, ActionGroupPropsSchema, ActionIconPropsSchema, ActionMenuPropsSchema, ElementButtonPropsSchema, ElementDefinitionListPropsSchema, ElementFilterPropsSchema, ElementFormPropsSchema, ElementImagePropsSchema, ElementMetadataViewerPropsSchema, ElementNumberPropsSchema, ElementRecordPickerPropsSchema, ElementRepeaterPropsSchema, ElementTextInputPropsSchema, ElementTextPropsSchema, ObjectCalendarPropsSchema, ObjectFormPropsSchema, ObjectGanttPropsSchema, ObjectGridPropsSchema, ObjectKanbanPropsSchema, ObjectMapPropsSchema, ObjectMasterDetailFormPropsSchema, ObjectMetricPropsSchema, ObjectTimelinePropsSchema, ObjectTreePropsSchema, PageAccordionProps, PageCardProps, PageContainerProps, PageHeaderProps, PageTabsProps, RecordActivityProps, RecordAlertActionSchema, RecordAlertProps, RecordChatterProps, RecordDetailsProps, RecordHighlightsField, RecordHighlightsProps, RecordHistoryProps, RecordLineItemsProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, RecordRelatedListProps, ReferenceRailEntrySchema } from '@objectstack/spec/ui'; +import type { ActionButtonProps, ActionGroupProps, ActionIconProps, ActionMenuProps, ElementDefinitionListProps, ElementNumberProps, ElementRecordPickerProps, ElementRepeaterProps, ObjectCalendarProps, ObjectFormProps, ObjectGanttProps, ObjectGridProps, ObjectKanbanProps, ObjectMapProps, ObjectMasterDetailFormProps, ObjectMetricProps, ObjectTimelineProps, ObjectTreeProps, PageContainerProps, RecordAlertAction, RecordAlertProps, RecordHighlightsField, RecordHistoryProps, RecordLineItemsProps, RecordPathProps, RecordQuickActionsProps, RecordReferenceRailProps, ReferenceRailEntry } from '@objectstack/spec/ui'; // Validate data const result = AIChatWindowProps.parse(data); @@ -1509,6 +1509,75 @@ Type: `string` | **unknownUserText** | `string` | optional | Copy substituted when an entry has no resolvable actor. Literal string, every locale (renderer default: "Unknown user"). | +--- + +## RecordLineItemsProps + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **childObject** | `string` | optional | Child object whose records this grid lists, edits and saves. Optional because the component-level `dataSource` binding can supply the object instead; with neither, the panel shows a configuration hint and fetches nothing | +| **relationshipField** | `string` | ✅ | FK on the child object pointing back to the parent record — the rows are queried as `{ [relationshipField]: parentId }`. Required: this panel does not auto-detect it | +| **columns** | `{ name: string; label?: string; type?: Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>; width?: number; … }[]` | ✅ | Editable grid columns, drawn exactly as declared. Each entry is the strict, name-keyed inline grid column a relationship field's `inlineColumns` takes (`{ name, label?, type?, options?, … }` — objectui GridColumn); unknown keys and the retired `field` spelling are refused. Unlike the master-detail carriers, this block does NOT hydrate a column from the child object's field: declare `label`, `type` and `options` yourself — an identity-only `{ name }` column is a text cell headed by its name. Required, with at least one column: nothing derives them, and an empty list draws a grid with no cells | +| **parentObject** | `string` | optional | Parent object the `totalField` rollup is written to (default: the object of the record on the page) | +| **parentId** | `string` | optional | Parent record id the rows belong to (default: the record on the page). Wins over `recordId` | +| **recordId** | `string` | optional | Alternate spelling of `parentId` the renderer also reads; `parentId` wins when both are set | +| **amountField** | `string` | optional | Numeric child column summed for the running total and for the `totalField` rollup (default `amount` when only `totalField` is set) | +| **totalField** | `string` | optional | Parent field to receive the rolled-up sum on save | +| **title** | `string` | optional | Panel title. A literal string rendered as-is in every locale (no inline locale map — the panel renders it as a React child); renderer default: the localized "Line Items" | +| **readonly** | `boolean` | optional | Render the grid read-only: cells locked, no Save button, and no adding or removing lines (renderer default: editable) | +| **minRows** | `number` | optional | Minimum number of rows | +| **maxRows** | `number` | optional | Maximum number of rows | +| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Additional criteria for the child rows — the ViewFilterRule array form `[{ field, operator, value }, ...]`, AND-combined with the parent relationship condition, never substituted for it: it can only narrow this record's lines | +| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Load order for the child rows — the SortItem array form `[{ field, order }, ...]` (renderer default: storage order) | +| **limit** | `integer` | optional | Row cap for the child fetch (renderer default: 500). The grid has no pagination, so this is the window of rows that are editable and saved together | + +### Nested Shape: `RecordLineItemsProps.columns[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Child field this column shows — the key the grid reads and writes on each row object (objectui GridColumn.name). The retired `field` spelling is refused. | +| **label** | `string` | optional | Column header; defaults to the child field's label via hydration. | +| **type** | `Enum<'text' \| 'number' \| 'currency' \| 'date' \| 'datetime' \| 'time' \| 'select' \| 'lookup' \| 'file'>` | optional | Cell control, derived from the child field's type when omitted. Declaring it opts the column out of schema hydration — supply the extras (options / reference / …) yourself. | +| **width** | `number` | optional | Fixed column width in px; omitted columns use type-based role sizing (text flexes, numeric/date/select stay fixed). | +| **required** | `boolean` | optional | Cell is flagged inline-invalid while empty. Computed columns are never required. | +| **options** | `{ label: string; value: string }[]` | optional | Select-cell options for `type: 'select'`; derived from the child field's options when the column declares no `type`. | +| **prefix** | `string` | optional | Symbol shown in a `currency` cell in place of the resolved currency's own symbol. No default: when omitted, the cell shows the symbol of the currency it resolves. It replaces the symbol only — the amount's decimal places stay the currency's. | +| **step** | `number` | optional | Input step for numeric cells. | +| **reference** | `string` | optional | Referenced object for `type: 'lookup'` cells; derived from the child lookup field when the column declares no `type`. | +| **displayField** | `string` | optional | Label field shown for a picked lookup record. | +| **idField** | `string` | optional | Id field stored for a picked lookup record. | +| **multiple** | `boolean` | optional | Multi-value column: multi-record lookup, or multi-file upload cell. | +| **accept** | `string[]` | optional | Accepted MIME types / extensions for a `file` cell's picker (e.g. ['image/*', '.pdf']); omit to accept anything. | +| **defaultHidden** | `boolean` | optional | Collapsed into the grid's column chooser by default (not dropped); required columns are never default-hidden. | +| **computed** | `boolean` | optional | Read-only computed column, recomputed live from sibling cells via `expr` and written back into the row. | +| **expr** | `string` | optional | Arithmetic expression for a computed column — a BARE string over `+ - * / %`, parentheses, numeric literals and field refs (`record.qty` or `qty`), evaluated by the grid's own safe evaluator. Deliberately NOT a CEL Expression envelope; `{ dialect, source }` is refused here. | +| **scale** | `integer` | optional | Decimal places to round a computed numeric result to (integer 0-100). REFUSED on a column declaring `type: 'currency'` — delete it there: the currency's ISO 4217 minor unit decides. The upper bound is the renderer's: the grid rounds with `toFixed`, which throws a RangeError above 100. | +| **autofill** | `boolean` | optional | For `lookup` columns: picking a record copies its same-named fields into sibling columns (a product's unit_price/description). On by default; set false to disable. | +| **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is read-only when TRUE, evaluated per row against the row as `record` plus the header as `parent` (e.g. P`parent.status == 'paid'`). | +| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. PRESENTATION ONLY: this flags the cell inline-invalid in the grid; nothing on the write path reads it. The server-enforced contract is the child FIELD's own `requiredWhen` — a transition gate, see `Field.requiredWhen` — which hydration copies onto an identity-only column, so declaring the requirement here alone enforces nothing. | + +### Nested Shape: `RecordLineItemsProps.filter[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | + +### Nested Shape: `RecordLineItemsProps.sort[number]` + +Sort field and direction pair + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to sort by | +| **order** | `Enum<'asc' \| 'desc'>` | ✅ | Sort direction | + + --- ## RecordPathProps diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md index 8c562e39010..e2540e9366e 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md @@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 189 | 179 | 3 | 0 | 7 | +| `ui/` | 190 | 180 | 3 | 0 | 7 | ## `ui/` — sites @@ -36,7 +36,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `app.zod.ts` | 19 | | `bulk-action.zod.ts` | 4 | | `chart.zod.ts` | 8 | -| `component.zod.ts` | 57 | +| `component.zod.ts` | 58 | | `dashboard.zod.ts` | 11 | | `dataset.zod.ts` | 4 | | `i18n.zod.ts` | 1 | @@ -46,7 +46,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `sharing.zod.ts` | 1 | | `view.zod.ts` | 62 | | `widget.zod.ts` | 1 | -| **total** | **189** | +| **total** | **190** | ## `ui/` — open @@ -54,7 +54,7 @@ Per file, how many of its sites still silently discard unknown keys. The `Class` column that decides the bucket split is hand-written in the ledger; the arithmetic over it is here. -**7 strip of 189**, in 4 file(s). +**7 strip of 190**, in 4 file(s). | File | Strip | Sites | |---|---|---| @@ -62,7 +62,7 @@ over it is here. | `app.zod.ts` | 1 | 19 | | `view.zod.ts` | 4 | 62 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **189** | +| **total** | **7** | **190** | | Bucket | Sites | |---|---| diff --git a/examples/app-showcase/test/project-detail-line-items.test.ts b/examples/app-showcase/test/project-detail-line-items.test.ts new file mode 100644 index 00000000000..ff4e374240e --- /dev/null +++ b/examples/app-showcase/test/project-detail-line-items.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { RecordLineItemsProps } from '@objectstack/spec/ui'; + +import * as pages from '../src/ui/pages/index.js'; +import { ProjectDetailPage } from '../src/ui/pages/index.js'; +import { Task } from '../src/data/objects/task.object.js'; + +/** + * Dogfood gate for the project page's Tasks grid (objectstack#21142). + * + * The `record:line_items` block on this page keyed all five of its columns + * `field`. objectui's line-items grid binds a column by `name` — the spelling + * objectui#3951 settled, with no tolerant alias — so every cell rendered + * empty, and nothing said so: the type had no `ComponentPropsMap` row, so the + * component-props gate skipped its props as unregistered. + * + * The assertions pin both halves: + * - the five columns are keyed `name`, each naming a field of the child + * object the grid lists, and the block parses against the spec's row; + * - no `record:line_items` column anywhere in the showcase carries `field`. + */ + +type AnyComponent = { + type?: unknown; + properties?: Record; + [k: string]: unknown; +}; + +/** Every component on a page — regions and slots alike, tab panels and children included. */ +function allComponents(page: Record): AnyComponent[] { + const out: AnyComponent[] = []; + const visit = (node: unknown): void => { + if (!node || typeof node !== 'object' || Array.isArray(node)) return; + const component = node as AnyComponent; + out.push(component); + const props = component.properties; + if (!props) return; + for (const item of Array.isArray(props.items) ? props.items : []) { + const children = (item as { children?: unknown })?.children; + for (const child of Array.isArray(children) ? children : []) visit(child); + } + for (const child of Array.isArray(props.children) ? props.children : []) visit(child); + }; + for (const region of (page.regions as { components?: unknown[] }[] | undefined) ?? []) { + for (const c of region.components ?? []) visit(c); + } + for (const slot of Object.values((page.slots as Record) ?? {})) { + for (const c of Array.isArray(slot) ? slot : [slot]) visit(c); + } + return out; +} + +const lineItemBlocks = (page: Record): AnyComponent[] => + allComponents(page).filter((c) => c.type === 'record:line_items'); + +const columnsOf = (block: AnyComponent): Record[] => + (Array.isArray(block.properties?.columns) ? block.properties!.columns : []) as Record[]; + +/** Every page the showcase exports. */ +const allPages = (Object.values(pages) as unknown[]).filter( + (p) => + !!p && typeof p === 'object' && !Array.isArray(p) && typeof (p as { name?: unknown }).name === 'string', +) as Record[]; + +describe('Project detail — the Tasks grid binds its columns by `name` (#21142)', () => { + const [block, ...rest] = lineItemBlocks(ProjectDetailPage as unknown as Record); + + it('carries one line-items block whose five columns are keyed `name`', () => { + expect(block, 'the project detail page must carry its Tasks grid').toBeTruthy(); + expect(rest).toHaveLength(0); + const columns = columnsOf(block); + expect(columns.map((c) => c.name)).toEqual(['title', 'status', 'priority', 'estimate_hours', 'due_date']); + for (const column of columns) expect(column).not.toHaveProperty('field'); + }); + + it('names a field of the child object in every column, and in the summed column', () => { + const props = block.properties!; + expect(props.childObject).toBe(Task.name); + const childFields = new Set(Object.keys(Task.fields ?? {})); + for (const column of columnsOf(block)) { + expect(childFields.has(column.name as string), `column '${String(column.name)}' must name a showcase_task field`).toBe(true); + } + expect(childFields.has(props.relationshipField as string)).toBe(true); + expect(columnsOf(block).map((c) => c.name)).toContain(props.amountField); + }); + + it('parses against the spec\'s `record:line_items` row with its keys intact', () => { + const result = RecordLineItemsProps.safeParse(block.properties); + expect(result.success, JSON.stringify(result.error?.issues)).toBe(true); + expect(result.data).toEqual(block.properties); + }); + + it('leaves no `field`-keyed line-items column anywhere in the showcase', () => { + for (const page of allPages) { + for (const lineItems of lineItemBlocks(page)) { + for (const column of columnsOf(lineItems)) { + expect(column, `page "${String(page.name)}" must key its line-items columns by \`name\``).not.toHaveProperty('field'); + expect(typeof column.name).toBe('string'); + } + } + } + }); +}); diff --git a/packages/lint/src/validate-component-props.test.ts b/packages/lint/src/validate-component-props.test.ts index 39d5fe04a52..8c2d5f34c7b 100644 --- a/packages/lint/src/validate-component-props.test.ts +++ b/packages/lint/src/validate-component-props.test.ts @@ -490,9 +490,10 @@ describe('validateComponentProps — unregistered types are skipped', () => { // section header), so it is the family's own living proof the skip survives. // `record:quick_actions` left it at #8744 for the same reason — it has a row // now, and its dispatch is pinned in the #8744 suite at the end of this - // file. `record:line_items` stays: still row-less, still the corpus's own - // specimen. - it.each(['record:line_items', 'flex', 'object-chart'])( + // file. `record:line_items` left it at #21142, the last registered `record:*` + // renderer to get a row; its dispatch is pinned in the #21142 suite below. + // `flex` stays as the corpus's own row-less specimen. + it.each(['flex', 'object-chart'])( 'says nothing about `%s`, whatever its props carry', (type) => { const findings = validateComponentProps( @@ -940,3 +941,56 @@ describe('validateComponentProps — mcp:connect-agent is dispatched (#12344)', expect(findings).toEqual([]); }); }); + +/** + * #21142 — `record:line_items` gets its row, so the gate's dispatch reaches it. + * + * The pre-fix state this pins against: the type had no `ComponentPropsMap` + * row (it was the string-arm registration ledger's one entry), so the + * walker's unregistered-type skip swallowed the whole props bag — the + * showcase project page's five `field`-keyed columns produced ZERO findings + * while the grid drew every cell empty. Remove the map row and the first test + * here goes back to that silence. + */ +describe('validateComponentProps — record:line_items is dispatched (#21142)', () => { + /** The showcase project page's block as it was authored before the fix (copied, not imported). */ + const fieldKeyed = { + type: 'record:line_items', + properties: { + childObject: 'showcase_task', + relationshipField: 'project', + amountField: 'estimate_hours', + title: 'Tasks', + columns: [ + { field: 'title', label: 'Title', type: 'text', required: true }, + { field: 'estimate_hours', label: 'Estimate (h)', type: 'number' }, + ], + }, + }; + + it('reports a `field`-keyed column at its own path, with the rename to `name`', () => { + const findings = validateComponentProps(stackWith([fieldKeyed])); + const unknown = unknownKeys(findings); + expect(unknown.map((f) => f.path)).toEqual([ + 'pages[0].regions[0].components[0].properties.columns[0].field', + 'pages[0].regions[0].components[0].properties.columns[1].field', + ]); + for (const f of unknown) { + expect(f.where).toBe('page "probe_page" · record:line_items'); + expect(f.message).toContain('`name`'); + } + // The column's identity is missing too: the value half names it. + expect(invalid(findings).map((f) => f.path)).toEqual([ + 'pages[0].regions[0].components[0].properties.columns[0].name', + 'pages[0].regions[0].components[0].properties.columns[1].name', + ]); + }); + + it('stays silent on the same block keyed by `name` — the fixed showcase shape', () => { + const columns = fieldKeyed.properties.columns.map(({ field, ...rest }) => ({ name: field, ...rest })); + const findings = validateComponentProps( + stackWith([{ ...fieldKeyed, properties: { ...fieldKeyed.properties, columns } }]), + ); + expect(findings).toEqual([]); + }); +}); diff --git a/packages/lint/src/validate-component-props.ts b/packages/lint/src/validate-component-props.ts index f4793c57828..3f6203e6e19 100644 --- a/packages/lint/src/validate-component-props.ts +++ b/packages/lint/src/validate-component-props.ts @@ -84,23 +84,23 @@ * ## Unregistered types are SKIPPED — a required semantic, not leniency * * `PageComponentSchema.type` is `z.union([PageComponentType, z.string()])`, an - * open union by design: the example corpus alone authors `flex`, `grid`, - * `object-chart` and `record:line_items` — nodes whose props schema - * `ComponentPropsMap` simply does not carry (SDUI blocks live in objectui's - * registry and in the ADR-0080 manifest). Judging those against an absent - * schema would report every one of them as broken. - * `validate-page-field-bindings` skips unknown types for the same reason and - * says so in its own header. + * open union by design: the example corpus alone authors `flex`, `grid` and + * `object-chart` — nodes whose props schema `ComponentPropsMap` simply does + * not carry (SDUI blocks live in objectui's registry and in the ADR-0080 + * manifest). Judging those against an absent schema would report every one of + * them as broken. `validate-page-field-bindings` skips unknown types for the + * same reason and says so in its own header. * * ⚠️ The skip is also the silent-failure direction, which is why the list * above keeps shrinking: earlier editions of this sentence named the * `object-*` family (#7751 declared six rows), then `record:reference_rail` * (#8691), then `record:quick_actions` and `record:alert` (#8744, with - * `record:history` and `record:discussion`) — each a type with a registered - * renderer whose authored keys this skip was quietly waving through. A type - * both registered in objectui AND absent from the map is a gap to measure - * (see #8691's method), not a state to preserve; `object-chart`'s absence is - * the recorded deliberate one (#7751). + * `record:history` and `record:discussion`), then `record:line_items` (#21142, + * whose five `field`-keyed showcase columns drew an empty grid) — each a type + * with a registered renderer whose authored keys this skip was quietly waving + * through. A type both registered in objectui AND absent from the map is a gap + * to measure (see #8691's method), not a state to preserve; `object-chart`'s + * absence is the recorded deliberate one (#7751). */ import { ComponentPropsMap } from '@objectstack/spec/ui'; diff --git a/packages/lint/src/validate-component-types.test.ts b/packages/lint/src/validate-component-types.test.ts index ee5ffab3420..8b2242b6e7f 100644 --- a/packages/lint/src/validate-component-types.test.ts +++ b/packages/lint/src/validate-component-types.test.ts @@ -82,14 +82,14 @@ describe('leaves the declared vocabulary and the open arm alone', () => { 'page:header', 'record:details', // ComponentPropsMap rows that are NOT enum members: the measured - // string-arm registrations that earned a row. + // string-arm registrations that earned a row. `record:line_items` was the + // string-arm registration ledger's one entry until #21142 measured it into + // the map; the ledger is empty now. 'element:metadata_viewer', + 'record:line_items', // ⛔ The RETIRED types are deliberately NOT here — their kept // `ComponentPropsMap` row makes them `isKnownComponentType`, and this rule // used to read that as "accepted". They now have their own describe below. - // The string-arm registration ledger (registered in objectui, row-less by - // pinned decision). - 'record:line_items', // Plugin namespaces — the open arm's declared story. 'mcp:connect-agent', 'cloud-connection:panel', diff --git a/packages/lint/src/validate-page-field-bindings.test.ts b/packages/lint/src/validate-page-field-bindings.test.ts index 405b5edfdb1..6d157a4212c 100644 --- a/packages/lint/src/validate-page-field-bindings.test.ts +++ b/packages/lint/src/validate-page-field-bindings.test.ts @@ -337,7 +337,11 @@ describe('validatePageFieldBindings — binding precedence and traversal', () => }); describe('validatePageFieldBindings — false-positive floor', () => { - it('skips an unregistered component type', () => { + // `record:line_items` has a `ComponentPropsMap` row since #21142, but this + // rule's own descriptor table (`COMPONENT_FIELD_SPECS`) does not carry it, so + // the rule skips it — which is what this pins. Its props are the props + // gate's to judge. + it('skips a component type its descriptor table does not carry', () => { const findings = validatePageFieldBindings({ ...baseStack(), pages: [pageWith([ diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index d7b8d6f6331..195b9113b5e 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -367,6 +367,9 @@ "RecordHighlightsProps (const)", "RecordHistoryProps (const)", "RecordHistoryProps (type)", + "RecordLineItemsProps (const)", + "RecordLineItemsProps (type)", + "RecordLineItemsPropsParsed (type)", "RecordPathProps (const)", "RecordPathProps (type)", "RecordQuickActionsProps (const)", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index fe2af4a331d..f0d1dd72345 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -1185,6 +1185,21 @@ "ui/RecordHistoryProps:emptyText", "ui/RecordHistoryProps:limit", "ui/RecordHistoryProps:unknownUserText", + "ui/RecordLineItemsProps:amountField", + "ui/RecordLineItemsProps:childObject", + "ui/RecordLineItemsProps:columns", + "ui/RecordLineItemsProps:filter", + "ui/RecordLineItemsProps:limit", + "ui/RecordLineItemsProps:maxRows", + "ui/RecordLineItemsProps:minRows", + "ui/RecordLineItemsProps:parentId", + "ui/RecordLineItemsProps:parentObject", + "ui/RecordLineItemsProps:readonly", + "ui/RecordLineItemsProps:recordId", + "ui/RecordLineItemsProps:relationshipField", + "ui/RecordLineItemsProps:sort", + "ui/RecordLineItemsProps:title", + "ui/RecordLineItemsProps:totalField", "ui/RecordPathProps:aria", "ui/RecordPathProps:stages", "ui/RecordPathProps:statusField", diff --git a/packages/spec/declaration-map/ui.json b/packages/spec/declaration-map/ui.json index 7477d4f958c..cf0c5d26032 100644 --- a/packages/spec/declaration-map/ui.json +++ b/packages/spec/declaration-map/ui.json @@ -235,6 +235,7 @@ "RecordHighlightsField": "ui/RecordHighlightsField", "RecordHighlightsProps": "ui/RecordHighlightsProps", "RecordHistoryProps": "ui/RecordHistoryProps", + "RecordLineItemsProps": "ui/RecordLineItemsProps", "RecordPathProps": "ui/RecordPathProps", "RecordQuickActionsProps": "ui/RecordQuickActionsProps", "RecordReferenceRailProps": "ui/RecordReferenceRailProps", diff --git a/packages/spec/dropped-refinements.baseline.json b/packages/spec/dropped-refinements.baseline.json index 63de5f1429a..16d6cb99117 100644 --- a/packages/spec/dropped-refinements.baseline.json +++ b/packages/spec/dropped-refinements.baseline.json @@ -2,8 +2,8 @@ "description": "Shrink-only ledger of every PUBLISHED JSON Schema that is STILL WIDER than the Zod type it was generated from, because a rule written as `.refine()` reaches the runtime and not the file (#18670). `z.toJSONSchema()` has no arm for a `custom` check: a plain record, the same record with a `.refine()`, and the same record with an ABORTING `.refine()` all project byte-identically (measured on zod 4.4.3, the version packages/spec resolves). So a document one of these files ACCEPTS can still be refused at parse time, and an author -- or an AI -- validating against packages/spec/json-schema/** finds out a release later. Each `sites` path is a position under that schema at which a refinement is dropped; the same paths are written onto the artifact itself as `x-dropped-refinements`. Item 2 closed the first patterns: a refinement DECLARED through the closed list in src/shared/refinement-projection.ts is emitted into the published file, reads `projected` rather than `dropped`, and its row LEAVES this ledger in the same PR -- which is why the ledger shrinks and never grows on a repair. Every refinement outside that closed list stays here, and adding an arm to the list is a public-contract decision, not a refactor. Hand-edited on purpose and with no `gen:` script: a generator would let a new gap be admitted by running a command instead of by a decision, which is the silence this ledger exists to end. Adding, removing or moving a site fails packages/spec/scripts/build-schemas.ts until the line moves with it, and the failure prints the corrected entry in full. ⛔ Do not delete or weaken a refinement to shorten this file -- the runtime rule is correct; it is the projection that is silent, and the remedy is to teach the closed list a NAMED pattern, never to drop the rule.", "measured": { "zod": "4.4.3", - "publishedSchemasWithDroppedRefinements": 213, - "droppedRefinementSites": 618, + "publishedSchemasWithDroppedRefinements": 214, + "droppedRefinementSites": 620, "refinementSitesThatDidProject": 369, "refinementSitesWithNoJsonFormToCompare": 0 }, @@ -1399,6 +1399,12 @@ "sections.element" ] }, + "ui/RecordLineItemsProps": { + "sites": [ + "columns.element", + "filter.element" + ] + }, "ui/RecordRelatedListProps": { "sites": [ "filter.element" diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index f995bd6f83e..8dac19b9f66 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -358,6 +358,8 @@ "RecordHighlightsField": "src/ui/component.zod.ts#RecordHighlightsField (type)", "RecordHighlightsProps": "src/ui/component.zod.ts#RecordHighlightsProps (const)", "RecordHistoryProps": "src/ui/component.zod.ts#RecordHistoryProps (type)", + "RecordLineItemsProps": "src/ui/component.zod.ts#RecordLineItemsProps (type)", + "RecordLineItemsPropsParsed": "src/ui/component.zod.ts#RecordLineItemsPropsParsed (type)", "RecordPathProps": "src/ui/component.zod.ts#RecordPathProps (type)", "RecordQuickActionsProps": "src/ui/component.zod.ts#RecordQuickActionsProps (type)", "RecordReferenceRailProps": "src/ui/component.zod.ts#RecordReferenceRailProps (type)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index cf577ded7ec..f3b071c13f9 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -133,6 +133,7 @@ "ui/RecordHighlightsField", "ui/RecordHighlightsProps", "ui/RecordHistoryProps", + "ui/RecordLineItemsProps", "ui/RecordPathProps", "ui/RecordQuickActionsProps", "ui/RecordReferenceRailProps", diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts new file mode 100644 index 00000000000..ffa6a4c36de --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts @@ -0,0 +1,60 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #21142 — the fourth carrier of the inline grid column, and the last +// registered `record:*` renderer without a `ComponentPropsMap` row. The type sat +// on the string-arm registration ledger instead, so the component-props gate +// skipped its props bag: the showcase project page keyed all five of its +// columns `field` and published green over a grid of empty cells. The row +// declares the fifteen keys objectui's `LineItemsPanel` reads, and its +// `columns` REFERENCES `InlineGridColumnSchema`. D3 only: page-component +// `properties` is not parsed on the metadata save or load path, so a stored +// page is never refused and there is no load-path refusal for a conversion to +// pre-empt; the one `field`-keyed producer the census found is respelled in the +// same change. +export const entry: SemanticMigration = { + id: 'ui-record-line-items-props-closed', + surface: 'page `record:line_items` components — `properties` (which used to accept any key) and ' + + '`properties.columns[]` (its inline grid columns)', + replacement: 'the declared shape the renderer reads: `{ childObject?, relationshipField, columns, ' + + 'parentObject?, parentId?, recordId?, amountField?, totalField?, title?, readonly?, minRows?, ' + + 'maxRows?, filter?, sort?, limit? }`, with `filter` the ViewFilterRule array, `sort` the SortItem ' + + 'array and `limit` a positive integer; `childObject` may come from the component-level ' + + '`dataSource` binding instead. `columns` is required and holds at least one column, each the ' + + 'strict, name-keyed inline grid column a relationship field\'s `inlineColumns` takes — ' + + '`{ name, label?, type?, options?, … }`. Write `name` where a column said `field` (or ' + + '`fieldName`, `key`); declare `label`, `type` and `options` on the column, because this block ' + + 'draws a column exactly as declared and hydrates nothing from the child object\'s field; ' + + 'delete `scale` from a column declaring `type: \'currency\'`; delete `addLabel`, `sortField`, ' + + '`formFields` and `inlineMode`, which belong to an `object-master-detail-form` detail entry and ' + + 'are not read here, and any other key the shape does not declare.', + reason: 'The block draws one inline grid of the record\'s child rows, through the same objectui ' + + 'grid as the other three carriers of the inline grid column, but it had no `ComponentPropsMap` ' + + 'row: it was the one entry on the string-arm registration ledger, so the component-props gate ' + + 'skipped it as unregistered and every authored key rode through. The showcase project page ' + + 'keyed all five of its columns `field`, the spelling the grid retired, and published green; the ' + + 'grid binds a column by `name`, so every cell rendered empty. The row is measured from the ' + + 'renderer\'s read points at the objectui pin, not from the registration\'s declared-input list, ' + + 'and its `columns` references the column schema, so every rule that schema holds applies here ' + + 'too, with its own prescription. It is read where every page component\'s props are: the ' + + 'component-props gate reports a failing key or column as an advisory ' + + '`component-props-unknown-key` / `component-props-invalid` finding on `objectstack validate`, ' + + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a ' + + 'page component\'s `properties` is not parsed on the metadata save or load path. `defineStack`\'s ' + + 'identity-only column check does not reach this block: the panel hands its columns to the grid ' + + 'as authored, so there is no hydrated type to judge. No conversion is registered: nothing on the ' + + 'load path refuses the shape, and the one `field`-keyed producer was respelled in the same ' + + 'change. Population measured at the change, on origin/main 1ecb871beb: one authored block in ' + + 'the examples (the showcase project detail page, five `field`-keyed columns, respelled `name`), ' + + 'zero in the documentation, against eight authored `record:*` blocks of other types through ' + + 'the same matcher as the control. ' + + 'Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `record:line_items` node validates: `objectstack validate` reports no ' + + '`component-props-unknown-key` / `component-props-invalid` finding on its `properties` path. ' + + 'Every node carries `relationshipField` and at least one column, every column is an object ' + + 'carrying `name`, no column carries `field`, `fieldName` or `key`, and no key outside the ' + + 'declared shape is present. The showcase project detail page\'s block parses with its five ' + + '`name`-keyed columns, and its grid renders a value — not a blank cell — in each column for a ' + + 'row that has one.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 29fa84a56c1..4115e338e99 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5993,6 +5993,20 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'other two carriers. Its D3 record is the semantic entry ' + '`ui-object-master-detail-form-details-closed`.', }, + { + id: 'ui-record-line-items-props-closed', + order: 57, + text: + 'It closes the fourth carrier the same way: `record:line_items` had no `ComponentPropsMap` ' + + 'row — it was the one entry on the string-arm registration ledger — so the component-props ' + + 'gate skipped its props, and the showcase project page\'s five `field`-keyed columns published ' + + 'green over a grid of empty cells (#21142). The row declares the fifteen keys the renderer ' + + 'reads, requires `relationshipField` and at least one column, and its `columns` references ' + + '`InlineGridColumnSchema`; the showcase columns are respelled `name` in the same change. The ' + + 'panel draws its columns as authored, with no hydration from the child object\'s field, so ' + + '`defineStack`\'s identity-only check does not reach it. Its D3 record is the semantic entry ' + + '`ui-record-line-items-props-closed`.', + }, { id: 'ui-report-joined-chart-retired', order: 38, @@ -18620,6 +18634,62 @@ const step18: MigrationStep = { + 'one parse-time normalization is `ExpressionInputSchema`\'s own: a bare-string `visible` ' + 'becomes the canonical `{ dialect: \'cel\', source }` envelope.', }, + // #21142 — the fourth carrier of the inline grid column, and the last + // registered `record:*` renderer without a `ComponentPropsMap` row. The type sat + // on the string-arm registration ledger instead, so the component-props gate + // skipped its props bag: the showcase project page keyed all five of its + // columns `field` and published green over a grid of empty cells. The row + // declares the fifteen keys objectui's `LineItemsPanel` reads, and its + // `columns` REFERENCES `InlineGridColumnSchema`. D3 only: page-component + // `properties` is not parsed on the metadata save or load path, so a stored + // page is never refused and there is no load-path refusal for a conversion to + // pre-empt; the one `field`-keyed producer the census found is respelled in the + // same change. + { + id: 'ui-record-line-items-props-closed', + surface: 'page `record:line_items` components — `properties` (which used to accept any key) and ' + + '`properties.columns[]` (its inline grid columns)', + replacement: 'the declared shape the renderer reads: `{ childObject?, relationshipField, columns, ' + + 'parentObject?, parentId?, recordId?, amountField?, totalField?, title?, readonly?, minRows?, ' + + 'maxRows?, filter?, sort?, limit? }`, with `filter` the ViewFilterRule array, `sort` the SortItem ' + + 'array and `limit` a positive integer; `childObject` may come from the component-level ' + + '`dataSource` binding instead. `columns` is required and holds at least one column, each the ' + + 'strict, name-keyed inline grid column a relationship field\'s `inlineColumns` takes — ' + + '`{ name, label?, type?, options?, … }`. Write `name` where a column said `field` (or ' + + '`fieldName`, `key`); declare `label`, `type` and `options` on the column, because this block ' + + 'draws a column exactly as declared and hydrates nothing from the child object\'s field; ' + + 'delete `scale` from a column declaring `type: \'currency\'`; delete `addLabel`, `sortField`, ' + + '`formFields` and `inlineMode`, which belong to an `object-master-detail-form` detail entry and ' + + 'are not read here, and any other key the shape does not declare.', + reason: 'The block draws one inline grid of the record\'s child rows, through the same objectui ' + + 'grid as the other three carriers of the inline grid column, but it had no `ComponentPropsMap` ' + + 'row: it was the one entry on the string-arm registration ledger, so the component-props gate ' + + 'skipped it as unregistered and every authored key rode through. The showcase project page ' + + 'keyed all five of its columns `field`, the spelling the grid retired, and published green; the ' + + 'grid binds a column by `name`, so every cell rendered empty. The row is measured from the ' + + 'renderer\'s read points at the objectui pin, not from the registration\'s declared-input list, ' + + 'and its `columns` references the column schema, so every rule that schema holds applies here ' + + 'too, with its own prescription. It is read where every page component\'s props are: the ' + + 'component-props gate reports a failing key or column as an advisory ' + + '`component-props-unknown-key` / `component-props-invalid` finding on `objectstack validate`, ' + + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a ' + + 'page component\'s `properties` is not parsed on the metadata save or load path. `defineStack`\'s ' + + 'identity-only column check does not reach this block: the panel hands its columns to the grid ' + + 'as authored, so there is no hydrated type to judge. No conversion is registered: nothing on the ' + + 'load path refuses the shape, and the one `field`-keyed producer was respelled in the same ' + + 'change. Population measured at the change, on origin/main 1ecb871beb: one authored block in ' + + 'the examples (the showcase project detail page, five `field`-keyed columns, respelled `name`), ' + + 'zero in the documentation, against eight authored `record:*` blocks of other types through ' + + 'the same matcher as the control. ' + + 'Deployed metadata NOT MEASURED.', + acceptanceCriteria: 'Every `record:line_items` node validates: `objectstack validate` reports no ' + + '`component-props-unknown-key` / `component-props-invalid` finding on its `properties` path. ' + + 'Every node carries `relationshipField` and at least one column, every column is an object ' + + 'carrying `name`, no column carries `field`, `fieldName` or `key`, and no key outside the ' + + 'declared shape is present. The showcase project detail page\'s block parses with its five ' + + '`name`-keyed columns, and its grid renders a value — not a blank cell — in each column for a ' + + 'row that has one.', + }, { id: 'ui-reference-rail-unknown-keys-refused', surface: 'page `record:reference_rail` components — `properties` and each `entries[]` item: ' diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index c7797b57d14..35459aca218 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -2116,7 +2116,10 @@ export const RecordLineItemsProps = lazySchema(() => strictObject({ object: 'childObject', childObjectName: 'childObject', child: 'childObject', foreignKey: 'relationshipField', relationField: 'relationshipField', parentField: 'relationshipField', fields: 'columns', label: 'title', sumField: 'amountField', rollupField: 'totalField', - ...FILTERS_TO_FILTER, + // The plural every `filter` door answers (`FILTERS_TO_FILTER`, declared + // further down — spelled here because `OS_EAGER_SCHEMAS=1` evaluates this + // body before that const is initialised). + filters: 'filter', }, guidance: { // The four detail-entry keys this panel does not read — the spellings an From ffcd210a69be1e8e7892d6f0d580a8261bca80be Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 18:49:31 +0000 Subject: [PATCH 4/7] chore(changeset): record:line_items props row (spec minor, narrowing) Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- .changeset/21142-line-items-columns-name.md | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .changeset/21142-line-items-columns-name.md diff --git a/.changeset/21142-line-items-columns-name.md b/.changeset/21142-line-items-columns-name.md new file mode 100644 index 00000000000..5a35922802b --- /dev/null +++ b/.changeset/21142-line-items-columns-name.md @@ -0,0 +1,33 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec)!: a `record:line_items` page block's props are a strict shape, and its columns are the inline grid column contract (#21142) + +Clause-②: yes (narrowing) + + + +**BREAKING** — an accept-set narrowing on a published authoring surface: a new `ComponentPropsMap` row judges a props bag nothing judged before. Shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a failing key or column as an advisory `component-props-unknown-key` / `component-props-invalid` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + +**`@objectstack/spec`** + +- **`ComponentPropsMap['record:line_items']`** — new row, `RecordLineItemsProps`. `record:line_items` was the one entry on the string-arm registration ledger (`STRING_ARM_REGISTERED_TYPES`, now empty), so the props gate skipped it as unregistered and any key rode through. The row declares the fifteen keys the console's `LineItemsPanel` reads: `childObject`, `relationshipField` (required), `columns` (required, at least one), `parentObject`, `parentId`, `recordId`, `amountField`, `totalField`, `title`, `readonly`, `minRows`, `maxRows`, `filter` (the ViewFilterRule array), `sort` (the SortItem array) and `limit` (a positive integer). `childObject` may come from the component-level `dataSource` binding instead. An unknown key is named. A near-miss gets its rename (`foreignKey` → `relationshipField`, `filters` → `filter`, …). The four keys of an `object-master-detail-form` detail entry that this block does not read (`addLabel`, `sortField`, `formFields`, `inlineMode`) are refused with the reason. +- **`columns`** references `InlineGridColumnSchema`, the strict, name-keyed column a relationship field's `inlineColumns` takes. The retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`, and a column without `name` is refused. This block draws a column exactly as declared: it does not hydrate `label`, `type` or `options` from the child object's field, so `defineStack`'s identity-only column check does not reach it. +- **New types `RecordLineItemsProps` and `RecordLineItemsPropsParsed`.** They differ because a column's `readonlyWhen` / `requiredWhen` bare-string predicate normalizes to an Expression envelope at parse. + +## FROM → TO + +| you wrote | write instead | +|:--|:--| +| `columns: [{ field: 'title', label: 'Title' }]` | `columns: [{ name: 'title', label: 'Title' }]` | +| `{ childObject: 'invoice_line', columns: [...] }` with no `relationshipField` | `{ childObject: 'invoice_line', relationshipField: 'invoice', columns: [...] }` | +| `columns: []`, or no `columns` | at least one `{ name, label?, type?, … }` column | +| `addLabel`, `sortField`, `formFields` or `inlineMode` on the block | the block without that key | +| any other key the shape does not declare | the block without that key | + +The one-line fix: key every column `name`, give the block its `relationshipField` and at least one column, and remove any key the shape does not declare. + +## Who is affected, measured + +On `origin/main` `1ecb871beb`: one authored `record:line_items` block in the examples, the showcase project detail page. All five of its columns were keyed `field`, so its Tasks grid rendered empty cells; they are keyed `name` in this change. No documentation example authors the block. Deployed metadata was not measured. From 742c697008600c0d47c294eee88ccdaa43d02c13 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 19:15:23 +0000 Subject: [PATCH 5/7] test(lint): pin the record:line_items findings at the gate's dotted path form Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- packages/lint/src/validate-component-props.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/lint/src/validate-component-props.test.ts b/packages/lint/src/validate-component-props.test.ts index 8c2d5f34c7b..a4f7cf10fd0 100644 --- a/packages/lint/src/validate-component-props.test.ts +++ b/packages/lint/src/validate-component-props.test.ts @@ -972,8 +972,8 @@ describe('validateComponentProps — record:line_items is dispatched (#21142)', const findings = validateComponentProps(stackWith([fieldKeyed])); const unknown = unknownKeys(findings); expect(unknown.map((f) => f.path)).toEqual([ - 'pages[0].regions[0].components[0].properties.columns[0].field', - 'pages[0].regions[0].components[0].properties.columns[1].field', + 'pages[0].regions[0].components[0].properties.columns.0.field', + 'pages[0].regions[0].components[0].properties.columns.1.field', ]); for (const f of unknown) { expect(f.where).toBe('page "probe_page" · record:line_items'); @@ -981,8 +981,8 @@ describe('validateComponentProps — record:line_items is dispatched (#21142)', } // The column's identity is missing too: the value half names it. expect(invalid(findings).map((f) => f.path)).toEqual([ - 'pages[0].regions[0].components[0].properties.columns[0].name', - 'pages[0].regions[0].components[0].properties.columns[1].name', + 'pages[0].regions[0].components[0].properties.columns.0.name', + 'pages[0].regions[0].components[0].properties.columns.1.name', ]); }); From c2b9101398cce19e72815d0d0837eaa2db672340 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 19:44:07 +0000 Subject: [PATCH 6/7] chore(spec): regenerate the os-regen artifacts on the merged tree The os-regen driver kept the branch side of authorable-surface/ui.json and the component reference page; regeneration restores main's `ui/Action:outcomeMessages` beside this branch's record:line_items keys. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- content/docs/references/ui/component.mdx | 2 +- packages/spec/authorable-surface/ui.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index ad8283eb3ef..7e9113fd048 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -179,7 +179,7 @@ const result = AIChatWindowProps.parse(data); | **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string \| Record; … }[]` | optional | Input parameters required from user — an ActionParam[] DEFINITION array, never a payload map (a static request body goes in `bodyExtra`). | | **bodyExtra** | `Record` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. | | **confirmText** | `string \| Record` | optional | Confirmation message before execution. On a registered action, pairing this with a non-empty `params` is refused — that opens a second dialog for one decision; put the question on `description` instead. Correct on a param-LESS action, where the confirm is the only dialog there is. | -| **successMessage** | `string \| Record` | optional | Success message to show after execution | +| **successMessage** | `string \| Record` | optional | Success message shown after the action succeeds. On a `type: 'api'` or `type: 'script'` action it may interpolate $`{result.*}` — the server response payload, the same scope `onSuccess.navigate` declares (e.g. $`{result.id}`). On a registered action that declares `outcomeMessages`, the entry named by the response's `outcome` is shown instead, and this message is the fallback. | | **errorMessage** | `string \| Record` | optional | Error message to show when the action fails (overrides the raw error). | | **refreshAfter** | `boolean` | optional (default: `false`) | Refresh view after execution | | **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index f0d1dd72345..0bef1ac2ebd 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -39,6 +39,7 @@ "ui/Action:opensInNewTab", "ui/Action:operation", "ui/Action:order", + "ui/Action:outcomeMessages", "ui/Action:params", "ui/Action:patch", "ui/Action:recordIdField", From 75f98b26a43fa1d485285b05a0eda306bcc13fb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 22:48:16 +0000 Subject: [PATCH 7/7] Recovery commit: chore(spec): regenerate the os-regen artifacts on the merged tree The os-regen driver kept this branch's side of the five ui.json shards, the strictness-ledger counts and the reference index in merge 21c74a7d, dropping main's FormFieldPublicPicker retirement. Regenerated from the merged source after a container restart; build-schemas reads 616 sites across 212 schemas, the counts the merge resolution recorded. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude --- content/docs/references/index.mdx | 10 +++++----- .../ui.md | 12 ++++++------ packages/spec/api-surface/ui.json | 3 --- packages/spec/authorable-surface/ui.json | 6 +----- packages/spec/declaration-map/ui.json | 2 -- packages/spec/export-origins/ui.json | 3 --- packages/spec/json-schema.manifest/ui.json | 1 - 7 files changed, 12 insertions(+), 25 deletions(-) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 3779f892d51..6eba36bff04 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1523 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1522 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 167 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **197** | **1523** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **197** | **1522** | 14 protocol modules | --- @@ -364,7 +364,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 167 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 166 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -385,7 +385,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | | [`responsive.zod.ts`](/docs/references/ui/responsive) | `ResponsiveStyles`, `StyleMap` | | [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` | -| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `EmptyState`, `FormButtonConfig`, `FormField`, `FormFieldPublicPicker`, `FormSection`, `FormSelectOption`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | +| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `EmptyState`, `FormButtonConfig`, `FormField`, `FormSection`, `FormSelectOption`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | --- diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md index e2540e9366e..e44f5b48756 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md @@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th | Dir | Sites | strict | passthrough | catchall | strip | |---|---|---|---|---|---| -| `ui/` | 190 | 180 | 3 | 0 | 7 | +| `ui/` | 189 | 179 | 3 | 0 | 7 | ## `ui/` — sites @@ -44,9 +44,9 @@ classify and is not listed (it becomes reportable the day it grows its first sit | `report.zod.ts` | 3 | | `responsive.zod.ts` | 1 | | `sharing.zod.ts` | 1 | -| `view.zod.ts` | 62 | +| `view.zod.ts` | 61 | | `widget.zod.ts` | 1 | -| **total** | **190** | +| **total** | **189** | ## `ui/` — open @@ -54,15 +54,15 @@ Per file, how many of its sites still silently discard unknown keys. The `Class` column that decides the bucket split is hand-written in the ledger; the arithmetic over it is here. -**7 strip of 190**, in 4 file(s). +**7 strip of 189**, in 4 file(s). | File | Strip | Sites | |---|---|---| | `action-params.zod.ts` | 1 | 1 | | `app.zod.ts` | 1 | 19 | -| `view.zod.ts` | 4 | 62 | +| `view.zod.ts` | 4 | 61 | | `widget.zod.ts` | 1 | 1 | -| **total** | **7** | **190** | +| **total** | **7** | **189** | | Bucket | Sites | |---|---| diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 195b9113b5e..eda5f281e62 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -190,9 +190,6 @@ "FormButtonConfigSchema (const)", "FormField (type)", "FormFieldInput (type)", - "FormFieldPublicPicker (type)", - "FormFieldPublicPickerParsed (type)", - "FormFieldPublicPickerSchema (const)", "FormFieldSchema (const)", "FormSection (type)", "FormSectionParsed (type)", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 0bef1ac2ebd..6f308fb3b70 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -538,7 +538,7 @@ "ui/FormField:options", "ui/FormField:placeholder", "ui/FormField:precision", - "ui/FormField:publicPicker", + "ui/FormField:publicPicker [RETIRED]", "ui/FormField:readonly", "ui/FormField:reference", "ui/FormField:required", @@ -548,10 +548,6 @@ "ui/FormField:visibleOn", "ui/FormField:visibleWhen", "ui/FormField:widget", - "ui/FormFieldPublicPicker:displayFields", - "ui/FormFieldPublicPicker:filter", - "ui/FormFieldPublicPicker:maxResults", - "ui/FormFieldPublicPicker:object", "ui/FormSection:collapsed", "ui/FormSection:collapsible", "ui/FormSection:columns", diff --git a/packages/spec/declaration-map/ui.json b/packages/spec/declaration-map/ui.json index cf0c5d26032..5239f6a5a3a 100644 --- a/packages/spec/declaration-map/ui.json +++ b/packages/spec/declaration-map/ui.json @@ -122,8 +122,6 @@ "FormButtonConfigSchema": "ui/FormButtonConfig", "FormField": "ui/FormField", "FormFieldBaseSchema": "ui/FormField", - "FormFieldPublicPicker": "ui/FormFieldPublicPicker", - "FormFieldPublicPickerSchema": "ui/FormFieldPublicPicker", "FormFieldSchema": "ui/FormField", "FormSection": "ui/FormSection", "FormSectionSchema": "ui/FormSection", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index 8dac19b9f66..8144a59475e 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -186,9 +186,6 @@ "FormButtonConfigSchema": "src/ui/view.zod.ts#FormButtonConfigSchema (const)", "FormField": "src/ui/view.zod.ts#FormField (type)", "FormFieldInput": "src/ui/view.zod.ts#FormFieldInput (type)", - "FormFieldPublicPicker": "src/ui/view.zod.ts#FormFieldPublicPicker (type)", - "FormFieldPublicPickerParsed": "src/ui/view.zod.ts#FormFieldPublicPickerParsed (type)", - "FormFieldPublicPickerSchema": "src/ui/view.zod.ts#FormFieldPublicPickerSchema (const)", "FormFieldSchema": "src/ui/view.zod.ts#FormFieldSchema (const)", "FormSection": "src/ui/view.zod.ts#FormSection (type)", "FormSectionParsed": "src/ui/view.zod.ts#FormSectionParsed (type)", diff --git a/packages/spec/json-schema.manifest/ui.json b/packages/spec/json-schema.manifest/ui.json index f3b071c13f9..42eed1b6b4a 100644 --- a/packages/spec/json-schema.manifest/ui.json +++ b/packages/spec/json-schema.manifest/ui.json @@ -67,7 +67,6 @@ "ui/ExpressionBindableTextKey", "ui/FormButtonConfig", "ui/FormField", - "ui/FormFieldPublicPicker", "ui/FormSection", "ui/FormSelectOption", "ui/FormView",