You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 14e9685
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,app-shell)!: a form view's type is its layout only — simple, tabbed or wizard; split, drawer and modal are retired with pane, splitSize and splitResizable (#27229)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBVBQa5cSAGPWu1xigsGp9it)_Fixes#27155
Ruling-ref: 6464210455
Refs #27153
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up owed — #27235 (hotcrm
src/sales/views/lead.view.ts)
## 维护者速读
- 改了什么:表单视图的 `type` 只表示版式,可选
`simple`(默认)、`tabbed`、`wizard`。`split`、`drawer`、`modal`
退役,写了会被拒并附改法。只服务分栏表单的 `pane`、`splitSize`、`splitResizable` 一起退役。Studio
新建视图不再提供这三个值。
- 为什么改:落实 ADR-0050 裁决 A′。表单在哪里打开(抽屉或弹窗)由打开它的一方决定,不是表单自己声明;分栏已经在 #26804
变成用户个人偏好。
- 风险与回滚:这是收窄,旧元数据里的这三个值和三个键会被拒。已存的 `sys_metadata` 行在读取时自动转换,`os migrate
meta --from 17` 改写源码。本仓只有 showcase 用到,已改写;hotcrm 的 `lead.view.ts` 由
hotcrm 席位跟进。回滚就是 revert 本 PR。
- 先看:① `packages/spec/src/ui/view.zod.ts`,`FORM_VIEW_TYPE_RETIRED`
的三条改法文案和 `type` 枚举;②
`packages/spec/src/conversions/registry.ts`,`formViewPresentationTypesRemoved`
的 `apply`;③
`ui/packages/app-shell/src/views/metadata-admin/package-schema.ts`,`modal`
改为 `simple`。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run --project repo
src/ui/form-view-type-layout-retirement.test.ts`,预期 31 个测试全部通过。
- 需要你做的:无,席位落地。
## What changes
### Spec (`@objectstack/spec`)
- `FormViewSchema.type` is `simple | tabbed | wizard`, still defaulting
to `simple`. It is built with `enumWithRetiredValues`, so `split`,
`drawer` and `modal` are refused by name, each with its own
prescription. The prescriptions name live replacements only:
- `split` → `tabbed` or the `simple` default, with `subforms` for
editing child records beside the parent. Opening a record beside its
list is the reader's Display-menu preference.
- `drawer` / `modal` → the caller's open mode: the list's
`navigation.mode`, the console's New/Edit dialogs (sized by
`modalSize`), or an action `type: 'modal'` opening a page.
- The split-only keys `FormSectionSchema.pane` (and the legacy
`groups[].pane`), `FormViewSchema.splitSize` and
`FormViewSchema.splitResizable` are `retiredKey()` tombstones, refused
on every layout. The superRefine check that refused `pane` off a split
form is gone, because the tombstone refuses it everywhere.
- **Kept, as a stated boundary:** `splitDirection`, `drawerSide`,
`drawerWidth` and `modalSize`. See Acceptance notes.
- ADR-0087 kit (`spec-property-retirement`, value-level route plus key
tombstones):
- D2 conversion `form-view-presentation-types-removed`
(`retiredFromLoadPath: true`, `retiredAfter: '18.2.0'`, MAJOR_18 order
78). It deletes a retired `type`, so the form lands on `simple`, and
strips `splitSize`, `splitResizable` and every section's or group's
`pane`. It covers a view container's `form` and `formViews.*`, a form
view item's `config`, and a flattened form overlay, both in stored rows
and in existing sources. Its fixture has 9 notices.
- D3 semantic entry `ui-form-view-type-layout-only` (its `conversionIds`
names the conversion), plus a STEP18_RATIONALE fragment.
- Three `RETIRED_KEYS_BY_MAJOR` rows: `ui/FormSection:pane`,
`ui/FormView:splitSize`, `ui/FormView:splitResizable`.
- Liveness (`liveness/view.json`): `splitSize` and `splitResizable` are
`dead` with REMOVED notes; the `type` row is noted NARROWED;
`splitDirection`, `drawerSide` and `drawerWidth` are noted inert on a
form view; the `sections` note records the `pane` removal.
- Removal-version pin: one JOIN row (three value refusals plus three
tombstones).
- Regenerated: `authorable-surface/ui.json` (three `[RETIRED]` rows),
`content/docs/references/**`, `liveness/state-counts/view.md`.
`api-surface`, `react-blocks.md` and `skills/**` are byte-identical
(`check:react-blocks` and `check:skill-refs` are in sync).
- Docs: `content/docs/ui/views.mdx` drops the three rows from the Form
Types table and adds a paragraph on where a form opens.
`protocol/objectui/layout-dsl.mdx` drops the `pane` mention.
### Showcase (`examples/app-showcase`, private)
- In `task.view.ts`, `formViews.split` is now `type: 'tabbed'` with its
panes removed, and `formViews.quick` is now `type: 'simple'`. The
default list declares `navigation: { mode: 'drawer' }` to show the open
mode where it now lives.
- `coverage.ts` has `FORM_VIEW_TYPES = ['simple', 'tabbed', 'wizard']`.
Comments are updated in `translations/index.ts` and
`new-project-wizard.page.ts`.
- The lint package's frozen showcase snapshot
(`packages/lint/src/showcase-shape.fixtures.ts`, test-only, not shipped)
follows, with a header note on the protocol-forced edit.
### Studio (`@object-ui/app-shell`): a defect this PR would otherwise
create
The precedent is #26804, which fixed its own Studio inspector in the
same PR. Claim amendments 2 and 3 cover this.
- `metadata-admin/anchors.ts`: the New View form's `formType` picker
offers `simple | tabbed | wizard`. It offered six values, and the three
retired ones would now build a body the server refuses at save.
- `metadata-admin/default-schemas.ts`: the view `type` enum drops the
same three values.
- `metadata-admin/package-schema.ts`: `getPackageForm()` returns `type:
'simple'`. It returned `type: 'modal'`, a spec-typed literal that fails
`tsc` (TS2322) after the narrowing. `SectionedSchemaForm` reads only
`tabbed`, so the render is unchanged. `PackageFormDialog` is what opens
it as a dialog.
- ui test pins whose assertion the narrowing made false are re-pinned to
the refusal, never deleted:
- `view-create-body.test.ts`: the three layouts are accepted; a new case
shows each retired type refused at `config.type`.
- `formSectionGroupReference-7051.test.tsx`: `pane` beside `group` and
the three retired types are refused.
- `form-spec.containers.test.tsx` and `FormPage.viewSpec.test.ts`:
`pane` leaves the "was undeclarable" literals, and a `@ts-expect-error`
pins it as refused.
## Why
ADR-0050 was Accepted by ruling 6464210455 on #27032 (letter A′,
maintainer 「其他同意」). `type` is the form's layout. `drawer` and `modal`
described where a form is opened, which the caller already models.
`split` was a list-beside-record view mode, not a layout. The ruling
mapped `split` to the list's `navigation.mode: 'split'`, but #26804
(17b5152) retired that the day before and made the side-by-side pane a
reader's preference. Following the seat's ruling, the prescriptions
apply the ruling's intent to the current tree and name live values only.
## Tests
- New pin
`packages/spec/src/ui/form-view-type-layout-retirement.test.ts` (31
tests, `repo` project, added to `vitest.repo-tests.json`):
- five doors refuse each retired type with `invalid_value` at `type`,
plus the prescription's first sentence and migrate sentence. The doors
are `FormViewSchema`, `view.form`, `view.formViews.*`, a ViewItem form
record and the flattened form overlay;
- the five doors accept all three layouts;
- a misspelt value keeps zod's own message;
- `defineStack` refuses with `STACK_SCHEMA_INVALID` / 422 at
`views.0.formViews.quick.type`;
- `pane` is refused on every layout, in `sections` and in `groups`;
`splitSize` and `splitResizable` are refused too;
- the kept keys still parse;
- `@ts-expect-error` checks at the authoring site;
- the conversion over a stored view container and a stored form view
item, its idempotence, its absence from the load path, its step-18
wiring and its D3 entry;
- a tree-scoped absence walk with an anti-vacuity matcher test. As a
probe, I put a copy of the pre-change `task.view.ts` in the tree; the
walk named it, and I removed the copy.
- Updated spec tests:
- `view.test.ts`: the `pane` block now pins the tombstone;
- `view-form-features-root.test.ts`: tabbed/wizard replace split;
- `section-group-reference.test.ts`;
- `view-union-diagnostics.test.ts`: three form types, plus a new case
where a retired type settles nothing on its own and `viewKind` still
does;
- `view-metadata-schema.test.ts`: reads the vocabulary through the kept
`splitDirection`;
- `retired-key-removal-version.pin.test.ts`: JOIN row.
- `objectstack build` of the showcase with `formViews.quick` changed to
`type: 'modal'` (a temporary edit, restored with a trap) exits 2. The
refusal sits at `formViews.quick.type` and names `os migrate meta --from
17`. The unmodified showcase builds with exit 0.
## Acceptance notes
- **Kept keys.** `splitDirection`, `drawerSide` and `drawerWidth` stay
declared because the React `ObjectForm` block contract
(`react-blocks.ts`) takes its `dataProps` from `FormViewSchema`.
Tombstoning them would regenerate
`skills/objectstack-ui/references/react-blocks.md` and make this PR Tier
H. They are #27159's, together with the renderer branches
(`ObjectForm.tsx`, `ModalForm.tsx`, `SplitForm`), the `object-form`
`formType` in `component.zod.ts`, and the block-contract keys.
`modalSize` stays because the console's record dialog reads it on every
layout.
- **Survey of stored and authored metadata for the retired values,
measured on this tree at base:**
- 2 form views authored a retired type, both in
`examples/app-showcase/src/ui/views/task.view.ts`: `formViews.split`
(`split`) and `formViews.quick` (`drawer`). The lint package's frozen
copy of that file repeats them.
- 2 sections authored `pane`, in the same `split` form view.
- 0 `modal` form views and 0 `splitSize` / `splitResizable` authorings.
- 0 JSON fixtures or stored-row seeds.
- 1 spec-typed Studio literal (`package-schema.ts`, `modal`) and 2
Studio enum lists offering the three values.
- hotcrm was not reachable from this session. The card's reading is 3
form views in `src/sales/views/lead.view.ts` (L747, L796, L839 at hotcrm
6c52f22), owed to the hotcrm seat per the Skills-docs line, filed as
#27235.
- `selectViewMetadataBranch` reads its form vocabulary off
`FormViewSchema.type`, so a bare `{ type: 'modal' }` body now settles no
branch, as an unknown type does. With `viewKind: 'form'` it still
reaches the form overlay, whose parse names the prescription. This is
diagnostic routing only, and it is pinned.
- No `skills/**` edit, so the PR is not Tier H. `docs/adr/**` is
untouched.
## Verification
Head: `edd4570f5`. The final-head gate union and full dogfood ran on
`cd275780d`. The last commit (`edd4570f5`) changes two lines in one ui
test file, and the ratchet families plus plugin-form `type-check` were
re-run on it (listed under Derived gates).
<details><summary>Type-checks and build</summary>
Each was run separately, with its exit code read before any pipe:
- `pnpm build --concurrency=2` (whole workspace, under os-verify-lock):
`VERDICT command-exit 0`, 116/116 tasks. The first two runs exposed the
`package-schema.ts` TS2322 and the console test literals; both are fixed
in this PR.
- `pnpm --filter @objectstack/spec typecheck` (tsc, scripts and the test
layer): exit 0. `check:test-typecheck` is OK, with 132 pinned signatures
held.
- `type-check` for `@object-ui/types`, `@object-ui/plugin-form`,
`@object-ui/app-shell` (including `tsconfig.test.json`) and
`@object-ui/console`: exit 0 each. plugin-form was re-run on
`edd4570f5`: exit 0.
- `pnpm --filter @objectstack/example-showcase typecheck`: exit 0.
- `pnpm --filter @objectstack/example-showcase build`: exit 0. With
`formViews.quick` changed to `type: 'modal'`: exit 2, with an
`invalid_value` at `formViews.quick.type` whose message is the
prescription, ending in the `os migrate meta --from 17` sentence.
- `check:react-blocks` (1 generated file in sync) and `check:skill-refs`
(9 generated files in sync): `react-blocks.md` and `skills/**` are
unchanged.
</details>
<details><summary>Tests</summary>
- `@objectstack/spec` `pnpm test` (local project): 740 files, 22234
passed and 1 failed. The failure was `view-metadata-schema.test.ts`,
which read the vocabulary through `splitSize`; I re-pointed it to the
kept `splitDirection`, and the re-run passed 50/50.
- `@objectstack/spec` `pnpm test:repo`: 82 files, 1355 tests passed.
- The new pin, on its own: 31/31. With a copy of the pre-change
`task.view.ts` placed in the tree, the absence walk names it.
- `@objectstack/lint` `validate-translation-references` and
`validate-translatable-sections`: 2 files, 252 tests passed.
- `@objectstack/example-showcase` `test`: 41 files, 505 tests passed.
- ui, from `ui/`: every test file that reads a spec view or form schema
(371 files, chosen by grep for
`FormViewSchema|FormSectionSchema|ViewItemSchema|ViewSchema|ViewMetadataSchema|defineStack|FormView|FormSection`).
370 passed and 1 skipped; 7978 tests passed and 9 skipped; `VERDICT
command-exit 0`.
- `pnpm --dir ui exec vitest run scripts/`: 175 files passed and 2
skipped; 5019 tests passed.
</details>
<details><summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 112 commands on `cd275780d`. Each
ran with `eval "$cmd" > log 2>&1; status=$?`, and all 112 recorded `exit
0`.
- `dispatch-gates --ran`: `✓ 112 derived famil(ies) accounted for — 112
run, 0 NOT-MEASURED (a DERIVED zero — all 112 recorded an exit code and
none of them is 3)`.
- An earlier run, before the full build, had 11 gates exit 3 with
PREREQUISITE NOT MET because they read built output. After `pnpm build`
all 11 exited 0, and the final union above includes them.
- Re-run on `edd4570f5`, all exit 0: `check:type-check-debt`,
`check:type-check-coverage`, `check:nul-bytes`,
`check:test-source-alias`, `check:undeclared-dep-imports`,
`check-comment-mask-adoption.mjs` and `check:issue-citations`.
- eslint on the changed files: root exit 0. ui exit 0, with warnings
only; `formSectionGroupReference-7051.test.tsx` carries the same 18
warnings as at base.
</details>
<details><summary>Full dogfood</summary>
- `pnpm --filter @objectstack/dogfood test` under os-verify-lock, on
`cd275780d`, after a whole-workspace build: `VERDICT command-exit 0`.
- 351 files: 348 passed and 3 skipped. 2824 tests: 2804 passed and 20
skipped. No failures.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufziVeBj5ENSWqsDTaYe
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec)!: a form view's `type` is its layout only — `simple` (the default), `tabbed` or `wizard`; `split`, `drawer` and `modal` are retired, and the split-only `sections[].pane`, `splitSize` and `splitResizable` keys go with them
7
+
8
+
Clause-②: no (narrowing)
9
+
10
+
Metadata-protocol: narrowed (`FormViewSchema.type` refuses `split`, `drawer` and `modal`; `FormSectionSchema.pane`, `FormViewSchema.splitSize` and `FormViewSchema.splitResizable` are tombstones)
**BREAKING** accept-set narrowing on the form view surface (`@objectstack/spec`), ADR-0050.
15
+
16
+
The model: `type` says how the form lays out its sections. Where a form opens is decided by whoever opens it, so `drawer` and `modal` were never the form's to declare, and a side-by-side split is a reader's preference since `navigation.mode` dropped `split`.
17
+
18
+
-**FROM → TO.**`type: 'split'` → `type: 'tabbed'` (one tab per section) or delete the value for `simple`; delete each section's `pane` and the form's `splitSize` / `splitResizable`; declare `subforms` to edit child records beside the parent. `type: 'drawer'` or `type: 'modal'` → delete the value for `simple`, or write the layout you want. A list opens a clicked record as its `navigation.mode` says (the drawer by default), and the console's New and Edit entries open the record form themselves, sized by the form's `modalSize`.
19
+
-**The one-line fix.** Delete the retired value and the three split-only keys. `os migrate meta --from 17` lists the edits for existing sources, and `--write` applies them. A stored `sys_metadata` row is converted when it is read (the `form-view-presentation-types-removed` conversion), so a form view saved with one of the three values keeps loading as a `simple` form.
20
+
-**What the parse says.** Each retired value is refused by name with its prescription, for example "'modal' was removed from the form view `type` enum in @objectstack/spec 18.3.0 (ADR-0050) — …", at every form view door (a view's `form` and `formViews.*`, a form view item record, a flattened form overlay) and at `defineStack` as `STACK_SCHEMA_INVALID` / 422. `pane`, `splitSize` and `splitResizable` are refused on every layout. TypeScript rejects each at the authoring site. A misspelt value keeps zod's own message.
21
+
-**Unchanged.**`splitDirection`, `drawerSide`, `drawerWidth` and `modalSize` stay declared: the console reads `modalSize` for its record dialog, and the React `ObjectForm` block contract still takes the other three from this schema until its renderer follows.
22
+
23
+
The console (`@object-ui/app-shell`): Studio's New View form offers the three form layouts only. It offered all six, and a form view created with `split`, `drawer` or `modal` would now be refused when saved.
|**object**|`string`| optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. |
|**type**|`Enum<'simple' \| 'tabbed' \| 'wizard'>`| optional (default: `"simple"`) |Form layout: 'simple' (stacked sections, the default), 'tabbed' (one tab per section) or 'wizard' (one gated step per section). Where the form opens is the caller's choice, not the form's|
1899
1899
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
1900
1900
|**columns**|`integer`| optional | Number of columns for the form body |
1901
1901
|**title**|`string`| optional | Form title |
@@ -1905,8 +1905,8 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
1905
1905
|**allowSkip**|`boolean`| optional | Wizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization. |
1906
1906
|**showStepIndicator**|`boolean`| optional | Show the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface. |
|**splitResizable**|`boolean`| optional |Whether the split is resizable (split forms)|
1908
+
|**splitSize**|`never`| optional |[REMOVED]`view.form.splitSize` was removed in @objectstack/spec 18.3.0 (ADR-0050) — it sized the primary pane of a `type: 'split'` form, and `split` left the form view `type` in the same release, so no form view draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.|
1909
+
|**splitResizable**|`never`| optional |[REMOVED]`view.form.splitResizable` was removed in @objectstack/spec 18.3.0 (ADR-0050) — it made the panes of a `type: 'split'` form resizable, and `split` left the form view `type` in the same release, so no form view draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.|
|**drawerWidth**|`string`| optional |[DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. |
|**type**|`Enum<'simple' \| 'tabbed' \| 'wizard'>`| optional (default: `"simple"`) |Form layout: 'simple' (stacked sections, the default), 'tabbed' (one tab per section) or 'wizard' (one gated step per section). Where the form opens is the caller's choice, not the form's|
1978
1978
|**layout**|`Enum<'vertical' \| 'horizontal'>`| optional | Field layout direction — 'vertical' (the renderer default) or 'horizontal'. Multi-column is not a layout value: set `columns`|
1979
1979
|**columns**|`integer`| optional | Number of columns for the form body |
1980
1980
|**title**|`string`| optional | Form title |
@@ -1984,8 +1984,8 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
1984
1984
|**allowSkip**|`boolean`| optional | Wizard step-gate opt-out: allow entering a later step without submitting the one before it. Navigation freedom, NOT a validation exemption — the final submit still re-validates every step and returns to the first failing one. Default (absent/false): steps unlock in array order as each prior step submits validly. UI admission only, never authorization. |
1985
1985
|**showStepIndicator**|`boolean`| optional | Show the wizard step indicator (renderer default: shown). Step progress (completed/current/upcoming/invalid) is derived from the step gate — this boolean is the only authorable progress surface. |
|**splitResizable**|`boolean`| optional |Whether the split is resizable (split forms)|
1987
+
|**splitSize**|`never`| optional |[REMOVED]`view.form.splitSize` was removed in @objectstack/spec 18.3.0 (ADR-0050) — it sized the primary pane of a `type: 'split'` form, and `split` left the form view `type` in the same release, so no form view draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.|
1988
+
|**splitResizable**|`never`| optional |[REMOVED]`view.form.splitResizable` was removed in @objectstack/spec 18.3.0 (ADR-0050) — it made the panes of a `type: 'split'` form resizable, and `split` left the form view `type` in the same release, so no form view draws panes. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.|
|**drawerWidth**|`string`| optional |[DEPRECATED → size buckets] Drawer width, e.g. "480px". A pixel width cannot be chosen without knowing the client viewport — the renderer derives it. |
0 commit comments