From ea40d40cd8827c3a573e019589f865c1cd3d9327 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 05:46:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(plugin-form,types,app-shell)!:=20object-fo?= =?UTF-8?q?rm.layout=20is=20vertical=20|=20horizontal=20on=20every=20face?= =?UTF-8?q?=20=E2=80=94=20delivers=20objectui#7759=20group=20C=20(objectui?= =?UTF-8?q?#11168=20slice=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@objectstack/spec` 17.5.0 retired `inline` and `grid` from the form layout enum (objectstack#20221). The authored `object-form` arm, which reads the row by reference, already refused both; every other face still published them. Measured through the real SchemaRenderer at the base, `inline` / `grid` / absent rendered byte-identical to `vertical` on the simple, tabbed, wizard and split layouts, with `horizontal` as the lit control, and the `form` renderer drew `grid` as `vertical` (it reads only `layout === 'horizontal'`). Narrowed: the `object-form` registration input; `ObjectFormSchema.layout` (TS and zod, the row's enum by reference); the `FormSchema` zod mirror (`grid`, whose WiderThanDeclared row resolves and is struck, header figures 4/4/4 -> 3/3/3); the designer's two options and their en/zh labels. The folds that mapped the retired values to `vertical` (ObjectForm simple/drawer/modal, DrawerForm, ModalForm) are retired; an absent layout still draws as vertical. OFF_SPEC_ARM_EXEMPTIONS is empty again: refusedArms 1 -> 0, owner 8 -> 7. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../10859-object-form-properties-bag.md | 2 + .../11168-designer-form-layout-options.md | 5 + .../11168-object-form-layout-two-values.md | 9 ++ .changeset/11168-types-form-layout-mirrors.md | 11 ++ .../registry-inputs-spec-parity.test.ts | 16 ++- content/docs/plugins/plugin-view.mdx | 2 +- .../src/views/metadata-admin/i18n.ts | 4 - .../metadata-admin/previews/block-config.ts | 5 +- packages/plugin-form/src/DrawerForm.tsx | 7 +- packages/plugin-form/src/ModalForm.tsx | 7 +- packages/plugin-form/src/ObjectForm.tsx | 24 ++-- .../__tests__/objectFormLayout-11168.test.tsx | 136 ++++++++++++++++++ packages/plugin-form/src/index.tsx | 7 +- .../src/__tests__/zod-mirror-parity.test.ts | 28 ++-- packages/types/src/objectql.ts | 21 +-- packages/types/src/zod/form.zod.ts | 8 +- packages/types/src/zod/objectql.zod.ts | 9 +- 17 files changed, 242 insertions(+), 59 deletions(-) create mode 100644 .changeset/11168-designer-form-layout-options.md create mode 100644 .changeset/11168-object-form-layout-two-values.md create mode 100644 .changeset/11168-types-form-layout-mirrors.md create mode 100644 packages/plugin-form/src/__tests__/objectFormLayout-11168.test.tsx diff --git a/.changeset/10859-object-form-properties-bag.md b/.changeset/10859-object-form-properties-bag.md index c40251c019..d69299a5df 100644 --- a/.changeset/10859-object-form-properties-bag.md +++ b/.changeset/10859-object-form-properties-bag.md @@ -26,3 +26,5 @@ Nothing changes at render time: `SchemaRenderer` hoists every `properties` key o **The same release's `.changeset/6152-object-form-unmirrored-members.md`** declared members on the flat mirror so that the strict face stopped refusing them on an authored node. On an authored node those members are now written in the bag, where the spec row judges them: every one of them except `buttons`, `defaults` and `subforms`, which the row does not declare. Those three stay authorable in an `object-view`'s `form` slot, which is built from the mirror, and a section there is still judged member by member. **What did not move.** The TypeScript `ObjectFormSchema` and its zod mirror `ObjectFormSchema` stay published and unchanged in shape. They are the node as `ObjectForm` reads it after the hoist, and as code composes it (`ObjectView`, `RecordFormPage`, `ScreenView`, a host mounting ``). The object-view `form` slot is still built from the mirror. + +**Note, 2026-10-01 (objectui#11168 slice 3, objectui#7759 group C, shipping in this same release).** `layout` is no longer one of the places the bag and the flat mirror differ. `ObjectFormSchema.layout`, on both faces, now takes the row's own `vertical | horizontal` by reference, the `object-form` registration publishes the same two, and the designer offers only those. `inline` and `grid` rendered as `vertical` before they were refused, so nothing that rendered is lost. The other differences listed above stand. diff --git a/.changeset/11168-designer-form-layout-options.md b/.changeset/11168-designer-form-layout-options.md new file mode 100644 index 0000000000..bf940bdc1d --- /dev/null +++ b/.changeset/11168-designer-form-layout-options.md @@ -0,0 +1,5 @@ +--- +'@object-ui/app-shell': patch +--- + +The page-block inspector no longer offers `Inline` and `Grid` for an `object-form`'s `layout` (objectui#11168 slice 3, objectui#7759 group C). `@objectstack/spec` 17.5.0 refuses both values, and both rendered as `Vertical`, so the designer was writing values that publish refuses. The two option labels (en, zh) are removed with the options. diff --git a/.changeset/11168-object-form-layout-two-values.md b/.changeset/11168-object-form-layout-two-values.md new file mode 100644 index 0000000000..1e5fb621d6 --- /dev/null +++ b/.changeset/11168-object-form-layout-two-values.md @@ -0,0 +1,9 @@ +--- +'@object-ui/plugin-form': minor +--- + +`object-form.layout` publishes only `vertical` and `horizontal`. This is objectui#11168 slice 3 and delivers objectui#7759 group C. `@objectstack/spec` 17.5.0 retired `inline` and `grid` from the form layout enum (objectstack#20221). The authored `object-form` arm, which reads the spec row by reference, already refused both. The registration still published them, so the page validator passed values `objectui validate` refuses. + +⚠️ This narrows a published input. Measured through the real `SchemaRenderer` before the change, `inline` and `grid` rendered byte-identical to `vertical` on the simple, tabbed, wizard and split layouts. Nothing that rendered is lost. A stored document carrying either value is refused at authoring; write `vertical` (the default) or `horizontal`. The fixed group ships the change as `minor`. + +The fold that mapped `inline` / `grid` to `vertical` is retired with them, in `ObjectForm` (simple, drawer and modal routes), `DrawerForm` and `ModalForm`. `layout` now passes through unfolded, and an absent `layout` still draws as `vertical`. diff --git a/.changeset/11168-types-form-layout-mirrors.md b/.changeset/11168-types-form-layout-mirrors.md new file mode 100644 index 0000000000..ca6c2283f3 --- /dev/null +++ b/.changeset/11168-types-form-layout-mirrors.md @@ -0,0 +1,11 @@ +--- +'@object-ui/types': minor +--- + +The form layout mirrors state the two values the spec and the renderers honour. This is objectui#11168 slice 3 and delivers objectui#7759 group C. + +- `ObjectFormSchema.layout`, TS and zod, is the `object-form` row's own enum, taken by reference: `vertical | horizontal`. It used to add `inline` and `grid`. `@objectstack/spec` 17.5.0 retired both, and both rendered as `vertical`. `ObjectViewSchema.form`, which picks this key, narrows with it. +- The zod `FormSchema.layout`, on the `form` node, drops `grid`. The TypeScript declaration and the `form` registration never had it, and the renderer reads only `layout === 'horizontal'`. Its `WiderThanDeclared` ledger row in the mirror-parity test resolves and is struck. +- The `columns` docs on both mirrors no longer say "for grid layout". `columns` sets the field grid's width, whatever the `layout`. + +⚠️ This narrows the accept set: `layout: 'inline'` or `'grid'` is now a type error and a zod refusal. The fixed group ships the change as `minor`. diff --git a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts index a100d6f341..501a217bce 100644 --- a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts +++ b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts @@ -760,7 +760,7 @@ const OBJECTUI_11111_LEDGER_CAPS = { unjudgedBlocks: 4, // objectui#11168: object-gantt, object-map, object-timeline, object-tree offSpecInputs: 0, // objectui#11168 slice 1 retired action:group.name unpublishedKeys: 3, // objectui#11168: 3 (action:button endpoint + undoable, action:icon endpoint); objectui#8652: 0 and objectui#8649: 0 (each struck by its landing) - refusedArms: 1, // objectui#11168: object-form (slice 2 narrowed element:definition-list.columns) + refusedArms: 0, // objectui#11168: slice 2 narrowed element:definition-list.columns, slice 3 object-form.layout memberPins: 0, // objectui#11168 slice 2 pinned element:definition-list.items and element:repeater ×3 } as const; @@ -1991,11 +1991,15 @@ const OFF_SPEC_ARM_EXEMPTIONS: Record = { * declares and the renderer compares (`props.columns === 2`); the strings it * declared before drew one column. The pin is * `packages/components/src/renderers/basic/__tests__/element-list-inputs-11168.test.tsx`. + * + * objectui#11168 slice 3 took the last entry, `object-form.layout:enum`, the + * same way, and with it objectui#7759 group C: the enum is `vertical` / + * `horizontal`, the two the spec row declares. Measured through the real + * `SchemaRenderer`, `inline` and `grid` rendered byte-identical to + * `vertical`, so nothing that rendered is lost. The pin is + * `packages/plugin-form/src/__tests__/objectFormLayout-11168.test.tsx`. + * The map is EMPTY again. */ - 'object-form.layout:enum': OWED_TO( - 'objectui#11168', - 'A REFUSED ARM: `object-form.layout` declares the members `inline` and `grid`, which the 17.5.0 contract refuses.', - ), }; // ── the MEMBER direction (objectui#8067) ───────────────────────────────────── @@ -5615,6 +5619,6 @@ describe('registry `inputs` vs `@objectstack/spec` ComponentPropsMap (repo-wide) reasons.filter((reason) => reason.startsWith(`${OWED_PREFIX}${owner}.`)).length, ]), ), - ).toEqual({ 'objectui#11168': 8, 'objectui#8652': 0, 'objectui#8649': 0 }); + ).toEqual({ 'objectui#11168': 7, 'objectui#8652': 0, 'objectui#8649': 0 }); }); }); diff --git a/content/docs/plugins/plugin-view.mdx b/content/docs/plugins/plugin-view.mdx index 62d98600f8..5d9d8362f3 100644 --- a/content/docs/plugins/plugin-view.mdx +++ b/content/docs/plugins/plugin-view.mdx @@ -604,7 +604,7 @@ derived from the child object's metadata unless you override them ### View tabs There is no `tabs` key, and `form.layout` has no tabbed value -(`vertical | horizontal | inline | grid`). The tab strip this package ships is +(`vertical | horizontal`). The tab strip this package ships is the **saved-view** tab bar: declare the views and render `` (or let a host such as `@object-ui/app-shell` do it): diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index 98ac68f1fd..3c179a74e2 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -672,8 +672,6 @@ const ENGINE_STRINGS_EN: Record = { 'engine.inspector.pageBlock.field.object-form.layout': 'Layout', 'engine.inspector.pageBlock.option.layout.vertical': 'Vertical', 'engine.inspector.pageBlock.option.layout.horizontal': 'Horizontal', - 'engine.inspector.pageBlock.option.layout.inline': 'Inline', - 'engine.inspector.pageBlock.option.layout.grid': 'Grid', 'engine.inspector.pageBlock.field.object-form.columns': 'Columns (grid layout)', 'engine.inspector.pageBlock.field.object-form.fields': 'Fields', 'engine.inspector.pageBlock.field.object-form.title': 'Title', @@ -3672,8 +3670,6 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.inspector.pageBlock.field.object-form.layout': '布局', 'engine.inspector.pageBlock.option.layout.vertical': '纵向', 'engine.inspector.pageBlock.option.layout.horizontal': '横向', - 'engine.inspector.pageBlock.option.layout.inline': '行内', - 'engine.inspector.pageBlock.option.layout.grid': '网格', 'engine.inspector.pageBlock.field.object-form.columns': '列数(网格布局)', 'engine.inspector.pageBlock.field.object-form.fields': '字段', 'engine.inspector.pageBlock.field.object-form.title': '标题', diff --git a/packages/app-shell/src/views/metadata-admin/previews/block-config.ts b/packages/app-shell/src/views/metadata-admin/previews/block-config.ts index fdc6bc86de..ca4b630528 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/block-config.ts +++ b/packages/app-shell/src/views/metadata-admin/previews/block-config.ts @@ -178,8 +178,9 @@ export const BLOCK_CONFIG: Record = { options: [ { value: 'vertical', label: 'engine.inspector.pageBlock.option.layout.vertical' }, { value: 'horizontal', label: 'engine.inspector.pageBlock.option.layout.horizontal' }, - { value: 'inline', label: 'engine.inspector.pageBlock.option.layout.inline' }, - { value: 'grid', label: 'engine.inspector.pageBlock.option.layout.grid' }, + // `inline` / `grid` are not offered: `@objectstack/spec` 17.5.0 refuses + // both on `object-form.layout`, and they rendered as `vertical` + // (objectui#11168 slice 3, objectui#7759 group C). ], }, { name: 'columns', label: 'engine.inspector.pageBlock.field.object-form.columns', kind: 'number', placeholder: { literal: '2' } }, diff --git a/packages/plugin-form/src/DrawerForm.tsx b/packages/plugin-form/src/DrawerForm.tsx index 190c34ca2a..dbf1b0e8f6 100644 --- a/packages/plugin-form/src/DrawerForm.tsx +++ b/packages/plugin-form/src/DrawerForm.tsx @@ -662,9 +662,10 @@ export const DrawerForm: React.FC = ({ : { height: schema.drawerWidth, maxHeight: schema.drawerWidth }; }, [schema.drawerWidth, side]); - const formLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') - ? schema.layout - : 'vertical'; + // `vertical | horizontal` on the declared face; the fold that mapped any + // other value to `vertical` was retired with `inline` / `grid` + // (objectui#11168 slice 3, objectui#7759 group C). + const formLayout = schema.layout ?? 'vertical'; // Action buttons live in the drawer's own footer (not inside the form // renderer). Routing Cancel through the footer lets it call the diff --git a/packages/plugin-form/src/ModalForm.tsx b/packages/plugin-form/src/ModalForm.tsx index f62617a2c2..d5662d2d03 100644 --- a/packages/plugin-form/src/ModalForm.tsx +++ b/packages/plugin-form/src/ModalForm.tsx @@ -715,9 +715,10 @@ export const ModalForm: React.FC = ({ finalizeClose(); }, [finalizeClose]); - const formLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') - ? schema.layout - : 'vertical'; + // `vertical | horizontal` on the declared face; the fold that mapped any + // other value to `vertical` was retired with `inline` / `grid` + // (objectui#11168 slice 3, objectui#7759 group C). + const formLayout = schema.layout ?? 'vertical'; // Build base form schema // Actions are hidden inside the form renderer — we render them in a sticky footer instead diff --git a/packages/plugin-form/src/ObjectForm.tsx b/packages/plugin-form/src/ObjectForm.tsx index 3c528a26f1..2ae1112530 100644 --- a/packages/plugin-form/src/ObjectForm.tsx +++ b/packages/plugin-form/src/ObjectForm.tsx @@ -545,13 +545,13 @@ export const ObjectForm: React.FC = ({ } if (schema.formType === 'drawer') { - const { layout: _layout, ...drawerRest } = schema; - const drawerLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') ? schema.layout : undefined; + // `layout` passes through unfolded: it is `vertical | horizontal` on both + // faces since objectui#11168 slice 3 (objectui#7759 group C), and the fold + // that mapped `inline` / `grid` away here is retired with them. return ( ({ name: s.name, @@ -581,13 +581,11 @@ export const ObjectForm: React.FC = ({ } if (schema.formType === 'modal') { - const { layout: _layout2, ...modalRest } = schema; - const modalLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') ? schema.layout : undefined; + // `layout` passes through unfolded — see the drawer branch above. return ( ({ name: s.name, @@ -1571,12 +1569,10 @@ const SimpleObjectForm: React.FC<{ schema: LocalizedObjectFormSchema; dataSource ); } - // Convert to FormSchema - // Note: FormSchema currently only supports 'vertical' and 'horizontal' layouts - // Map 'grid' and 'inline' to 'vertical' as fallback - const formLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') - ? schema.layout - : 'vertical'; + // Convert to FormSchema. `layout` is `vertical | horizontal` on both faces + // since objectui#11168 slice 3 (objectui#7759 group C); the fold that mapped + // the retired `inline` / `grid` to `vertical` here is retired with them. + const formLayout = schema.layout ?? 'vertical'; // If sections are provided (explicitly, or derived from the object's // `fieldGroups`) for the simple form, render them as full-width, optionally diff --git a/packages/plugin-form/src/__tests__/objectFormLayout-11168.test.tsx b/packages/plugin-form/src/__tests__/objectFormLayout-11168.test.tsx new file mode 100644 index 0000000000..c85e6fa2f2 --- /dev/null +++ b/packages/plugin-form/src/__tests__/objectFormLayout-11168.test.tsx @@ -0,0 +1,136 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#11168 slice 3 — `object-form.layout` is `vertical | horizontal` on + * every face, and objectui#7759 group C with it. + * + * `@objectstack/spec` 17.5.0 retired `inline` and `grid` from the form layout + * enum (objectstack#20221). The authored `object-form` arm reads its props + * through the spec row by reference, so it already refused both. Three objectui + * faces still published them: the `object-form` registration, the + * `ObjectFormSchema` mirror (TS and zod), and the `form` node's `FormSchema` + * zod mirror (`grid`). The designer offered both too. Measured through the real + * `SchemaRenderer` before the change, both rendered byte-identical to + * `vertical`: every layout folded them to `vertical`, and the `form` renderer + * reads only `layout === 'horizontal'`. So narrowing loses nothing that + * rendered, and the folds are retired with the values. + * + * The compile-time rows are checked by `tsc -p tsconfig.test.json`. + */ +import React from 'react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, cleanup, screen, waitFor } from '@testing-library/react'; +import { ComponentRegistry } from '@object-ui/core'; +import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; +import { registerAllFields } from '@object-ui/fields'; +import { safeValidateSchema, ObjectFormSchema as ObjectFormMirror, FormSchema as FormMirror } from '@object-ui/types/zod'; +import type { ObjectFormSchema, FormSchema } from '@object-ui/types'; +import type { ObjectFormProps } from '@objectstack/spec/ui'; +// Registers the `form` renderer the object-form draws through, and +// `object-form` itself, at module scope. +import '@object-ui/components'; +import '../index'; + +registerAllFields(); +afterEach(cleanup); + +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; + +/** The TS face IS the spec row's enum, so a spec release that moves it moves this type. */ +export type _ObjectFormLayoutIsTheRowsEnum = Expect>; +export type _FormLayoutIsTwoValues = Expect>; +// @ts-expect-error — `grid` is retired on the object-form's TS face. +const _retiredGrid: ObjectFormSchema['layout'] = 'grid'; +// @ts-expect-error — and so is `inline`. +const _retiredInline: ObjectFormSchema['layout'] = 'inline'; + +const RETIRED = ['inline', 'grid'] as const; +const LIVE = ['vertical', 'horizontal'] as const; + +/** `{ code, path }` of what a parse refused, so a row names WHICH key was refused and HOW. */ +const refusal = (result: { success: boolean; error?: { issues: Array<{ code: string; path: PropertyKey[] }> } }) => + result.success ? [] : result.error!.issues.map((issue) => ({ code: issue.code, path: issue.path.join('.') })); + +describe('`object-form.layout` publishes the spec row\'s two values (objectui#11168, objectui#7759 group C)', () => { + it('the registration declares exactly `vertical` and `horizontal`', () => { + const layout = (ComponentRegistry.getConfig('object-form', 'plugin-form')?.inputs ?? []).find( + (input) => input.name === 'layout', + ); + expect(layout?.enum).toEqual(['vertical', 'horizontal']); + }); + + it.each(RETIRED)('`objectui validate` refuses `layout: "%s"` on an authored object-form', (layout) => { + // The acceptance row: the authored arm reads the spec row by reference. + expect(refusal(safeValidateSchema({ type: 'object-form', properties: { objectName: 'contact', layout } }) as never)).toEqual([ + { code: 'invalid_value', path: 'properties.layout' }, + ]); + }); + + it.each(LIVE)('LIT CONTROL: `objectui validate` accepts `layout: "%s"`', (layout) => { + expect(safeValidateSchema({ type: 'object-form', properties: { objectName: 'contact', layout } }).success).toBe(true); + }); + + it.each(RETIRED)('the `ObjectFormSchema` mirror refuses `%s` at `layout`, and accepts both live values', (layout) => { + const node = { type: 'object-form', objectName: 'contact', mode: 'create' }; + expect(refusal(ObjectFormMirror.safeParse({ ...node, layout }) as never)).toEqual([ + { code: 'invalid_value', path: 'layout' }, + ]); + for (const live of LIVE) expect(ObjectFormMirror.safeParse({ ...node, layout: live }).success).toBe(true); + }); + + it('the `form` node\'s `FormSchema` mirror refuses `grid` — its declaration and registration never had it', () => { + const node = { type: 'form', fields: [] }; + expect(refusal(FormMirror.safeParse({ ...node, layout: 'grid' }) as never)).toEqual([ + { code: 'invalid_value', path: 'layout' }, + ]); + for (const live of LIVE) expect(FormMirror.safeParse({ ...node, layout: live }).success).toBe(true); + expect(ComponentRegistry.getConfig('form')?.inputs?.find((input) => input.name === 'layout')?.enum).toEqual(LIVE); + }); + + it('the compile-time rows are real values', () => { + expect([_retiredGrid, _retiredInline]).toEqual(['grid', 'inline']); + }); +}); + +describe('with the folds retired, the key still decides label placement (objectui#11168)', () => { + const OBJECT = { name: 'contact', fields: { name: { type: 'text', label: 'Name' } } }; + + /** The rendered form, ids normalised, once its field label is on screen. */ + async function rendered(layout: string | undefined): Promise { + const ds = { + getObjectSchema: vi.fn().mockResolvedValue(OBJECT), + find: vi.fn(async () => ({ data: [] })), + findOne: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }; + render( + + + , + ); + await waitFor(() => expect(screen.getAllByText('Name').length).toBeGreaterThan(0)); + const html = document.body.innerHTML + .replace(/(id|for|aria-[a-z]+|name)="[^"]*"/g, '$1=""') + .replace(/«[^»]*»|_r_[0-9a-z]+_|:r[0-9a-z]+:/g, ''); + cleanup(); + return html; + } + + it('an absent `layout` draws exactly what `vertical` draws, and `horizontal` draws something else', async () => { + const vertical = await rendered('vertical'); + expect(await rendered(undefined)).toBe(vertical); + // LIT CONTROL: the comparison can tell layouts apart. + expect(await rendered('horizontal')).not.toBe(vertical); + }); +}); diff --git a/packages/plugin-form/src/index.tsx b/packages/plugin-form/src/index.tsx index eb7b7119f3..84a6d25f67 100644 --- a/packages/plugin-form/src/index.tsx +++ b/packages/plugin-form/src/index.tsx @@ -254,7 +254,12 @@ ComponentRegistry.register('object-form', ObjectFormRenderer, { description: 'Subtitle under the drawer and modal heading. Accepts either a plain string or an inline per-locale map (`{ en: "Enter the order details", "zh-CN": "填写订单信息" }`), resolved against the active UI language with the same fallback chain as `title`.', }, - { name: 'layout', type: 'enum', enum: ['vertical', 'horizontal', 'inline', 'grid'] }, + // `inline` / `grid` RETIRED (objectui#11168 slice 3, objectui#7759 group C): + // `@objectstack/spec` 17.5.0's row refuses both (objectstack#20221), and + // measured through the real `SchemaRenderer` both rendered byte-identical + // to `vertical` — every layout folded them away. The authored bag already + // refused them by reference to the row; this publishes what it accepts. + { name: 'layout', type: 'enum', enum: ['vertical', 'horizontal'], description: 'Label placement: `vertical` puts each label above its field (the default), `horizontal` beside it.' }, { name: 'columns', type: 'number' }, // Tabbed { name: 'defaultTab', type: 'string' }, diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index fb27923b48..9f3f192e2d 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -560,8 +560,13 @@ * spelled "six" rots exactly as fast as one spelled `6`, it is just harder to * point a regex at. ⛔ Do not spell a live figure out again, and ⛔ do not * restate one without checking that the pin's spelling still reaches it. - * - **4 entries** in `WiderThanDeclared`, **4 keys** across them, and **4 arms** - * under those keys — split **2** SCHEMA-NODE, **2** CONCRETE, **0** MIXED, **0** unions. + * - **3 entries** in `WiderThanDeclared`, **3 keys** across them, and **3 arms** + * under those keys — split **2** SCHEMA-NODE, **1** CONCRETE, **0** MIXED, **0** unions. + * It read 4 / 4 / 4 — 2 / 2 / 0 / 0 — until objectui#11168 slice 3 (objectui#7759 + * group C) narrowed `form.zod.ts#FormSchema`'s `layout` to the `vertical` / + * `horizontal` its declaration states: `@objectstack/spec` 17.5.0 retired `grid`, and + * the renderer drew it as `vertical`. The entry, its one key and its one CONCRETE arm + * LEFT together. * It read 3 / 3 / 3 — 2 / 1 / 0 / 0 — until objectui#11117 waived * `objectql.zod.ts#ObjectGridSchema`'s `objectName` on the mirror beside a * `dataSource.object` binding while the TypeScript twin kept it required: the entry, @@ -3744,16 +3749,14 @@ interface WiderThanDeclared { // fourth. ⛔ Do not re-add them for the class labels they carried (F, // "inline shape against a named declaration"; D, "disjoint") — a row this // ledger records must be a measured widening. - /** - * CONCRETE. `layout` is the clearest single instance in this ledger: the mirror - * is `z.enum(['vertical', 'horizontal', 'grid'])` and the declaration states the - * first two, so the third spelling parses green and `tsc` refuses it. (`fields` - * left under objectui#7759 group E: its wider reading was the nested `z.function()` - * arms of `FormFieldSchema`, not the element shape. `mode`, the disjoint key - * objectui#5927 left in `KnownDrift`, LEFT under objectui#10286: retired on both - * faces under the objectui#7759 ruling's D1-(ii).) - */ - 'form.zod.ts#FormSchema': 'layout'; + // `form.zod.ts#FormSchema` LEFT this ledger WHOLE under objectui#11168 slice 3 + // (objectui#7759 group C). Its last key was `layout`, CONCRETE: the mirror was + // `z.enum(['vertical', 'horizontal', 'grid'])` while the declaration stated the + // first two. `@objectstack/spec` 17.5.0 retired `grid` (and `inline`) from the + // form layout enum, the `form` renderer reads only `layout === 'horizontal'` — + // measured, `grid` drew exactly what `vertical` draws — so the mirror narrowed to + // the declaration's two and the entry would be STALE if it stayed. (`fields` left + // under objectui#7759 group E; `mode` under objectui#10286.) // `form.zod.ts#SliderSchema` recorded `defaultValue` and `value` here (CONCRETE, the class // objectui#7069 was filed for: a single-or-list mirror against a list-only declaration). // objectui#10280 (objectui#7759 group B) resolved both by the read site, per the director's @@ -3930,7 +3933,6 @@ const WIDER_ARM_ROW_SEPARATOR = '::'; const WIDER_ARMS: Readonly< Record< string, readonly WiderArmClass[] > > = { 'app.zod.ts#AppComponentSchema::areas': ['SCHEMA-NODE'], - 'form.zod.ts#FormSchema::layout': ['CONCRETE'], 'layout.zod.ts#PageNodeSchema::slots': ['SCHEMA-NODE'], 'objectql.zod.ts#ObjectGridSchema::objectName': ['CONCRETE'], }; diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index eb25a95788..58a09a319a 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -151,6 +151,9 @@ import type { // local symbol under a `@objectstack/spec` export's name reads to the next // agent as the spec's own definition (`pnpm check:spec-symbols`). ObjectCalendarProps as SpecObjectCalendarProps, + // objectui#11168 slice 3 — `ComponentPropsMap['object-form']`'s author state, + // so `ObjectFormSchema.layout` takes the row's own enum by reference. + ObjectFormProps as SpecObjectFormProps, ChartDrillDown, I18nLabel, DashboardWidget as SpecDashboardWidget, @@ -1727,21 +1730,23 @@ export interface ObjectFormSchema extends BaseSchema { }>; /** - * Form layout. - * - * Supported layouts: + * Label placement — `@objectstack/spec`'s `object-form` row's own enum, by + * reference: * - `vertical` – label above field (default) * - `horizontal` – label and field in a row - * - `inline` – compact inline layout, typically used in toolbars - * - `grid` – **experimental** grid layout + * + * ⛔ `inline` and `grid` are RETIRED (objectui#11168 slice 3, objectui#7759 + * group C). The spec refuses both since 17.5.0 (objectstack#20221), and + * measured through the real `SchemaRenderer` both rendered byte-identical to + * `vertical` — every layout folded them away. * * @default 'vertical' */ - layout?: 'vertical' | 'horizontal' | 'inline' | 'grid'; + layout?: SpecObjectFormProps['layout']; /** - * Grid columns (for grid layout). - * @default 2 + * Number of columns the fields are laid out in (1-4). Independent of + * `layout`: the form body is one grid at this width. */ columns?: number; diff --git a/packages/types/src/zod/form.zod.ts b/packages/types/src/zod/form.zod.ts index 0912ec2e9a..c2a86e9946 100644 --- a/packages/types/src/zod/form.zod.ts +++ b/packages/types/src/zod/form.zod.ts @@ -1130,8 +1130,12 @@ export const FormSchema = BaseSchema.extend({ // default `true`; the submit button renders only while it holds) and // refused by the strict authoring face until declared here. showSubmit: z.boolean().optional().describe('Show the submit button (default true); `false` renders the fields with no submit button'), - layout: z.enum(['vertical', 'horizontal', 'grid']).optional().describe('Form layout'), - columns: z.number().optional().describe('Number of columns (for grid layout)'), + // objectui#11168 slice 3 (objectui#7759 group C): `grid` retired. The + // declaration states `vertical | horizontal`, the `form` registration + // publishes those two, and the renderer reads only `layout === 'horizontal'`, + // so `grid` drew exactly what `vertical` draws. + layout: z.enum(['vertical', 'horizontal']).optional().describe('Label placement: vertical (the default) or horizontal'), + columns: z.number().optional().describe('Number of columns the fields are laid out in (multi-column layout)'), validationMode: z.enum(['onSubmit', 'onChange', 'onBlur', 'onTouched', 'all']).optional().describe('Validation mode'), resetOnSubmit: z.boolean().optional().describe('Reset form on successful submit'), mode: retirementTombstone( diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 9bb489b456..0eddc9ec81 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -609,8 +609,13 @@ export const ObjectFormSchema = BaseSchema.extend({ collapsible: z.boolean().optional(), defaultCollapsed: z.boolean().optional(), })).optional().describe('Field groups'), - layout: z.enum(['vertical', 'horizontal', 'inline', 'grid']).optional().describe('Form layout'), - columns: z.number().optional().describe('Grid columns'), + // objectui#11168 slice 3 (objectui#7759 group C): the row's own enum, by + // reference — `inline` and `grid` are retired by `@objectstack/spec` 17.5.0 + // (objectstack#20221) and rendered byte-identical to `vertical` on every + // layout, so this face refuses them as the authored bag already did. + layout: stripImportedDefaults(SpecObjectFormPropsSchema).shape.layout + .describe('Label placement: vertical (the default) or horizontal — the spec row\'s enum, by reference'), + columns: z.number().optional().describe('Number of columns the fields are laid out in (1-4)'), showSubmit: z.boolean().optional().describe('Show submit button'), submitText: stripImportedDefaults(SpecI18nLabelSchema).optional() .describe('Submit button text. @objectstack/spec I18nLabel, a plain string or an inline locale map'),