Skip to content

fix(console): every console form entry point renders the form view's buttons, layout and defaults, and a tabbed form with subforms opens on its defaultTab - #27027

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-58h2qh
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-58h2qh

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Refs #26843 (items buttons, layout, defaults, defaultTab, tabPosition)

Clause-②: no
Metadata-protocol: semantic
Skills-docs: none — the skills and docs already teach these keys as rendered (content/docs/protocol/objectui/layout-dsl.mdx for defaultTab / tabPosition, the spec's property help for buttons / defaults / layout); this diff makes the console do what they say.

维护者速读

  • 改了什么:控制台三个表单入口(整页新建/编辑、新建/编辑弹窗、表单页 /forms/:name 与公开表单 /f/:slug)现在都按表单视图里声明的 buttons、layout、defaults 渲染;带子表的分页签表单按 defaultTab / tabPosition 打开。
  • 为什么改:这些键能保存、能发布,但各入口用自己写死的 showSubmit: true、按钮文案和 layout: 'vertical' 盖掉了作者的值,作者看不到任何效果(ADR-0049 声明即生效)。
  • 风险与回滚:只改 UI 渲染层,不动 spec 和服务端;表单视图没写这些键时行为不变(每组测试都有对照用例)。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/utils/recordFormNavigation.ts(resolveFormViewChrome 的优先级与 defaults 合并缓存)、ui/packages/app-shell/src/console/AppContent.tsx(新建弹窗那一处)、ui/packages/plugin-form/src/ModalForm.tsx(子表分支补 formType)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/utils/__tests__/formViewChrome-26843.test.ts packages/plugin-form/src/__tests__/formViewTabKeysAndButtons-26843.test.tsx,预期全部通过。
  • 需要你做的:无,席位落地。

What changes

One resolver, resolveFormViewChrome (ui/packages/app-shell/src/utils/recordFormNavigation.ts), maps the selected form view's keys onto the form props, and the full-page record form and the New / Edit dialog both spread it in place of their hard-coded sets:

  • buttons.submit / buttons.cancel: an authored show wins over the host's true; an authored label (an I18nLabel, resolved against the UI language) over the host's label. buttons.reset is not read: no host draws a reset button.
  • layout: the authored value when the spec's enum allows it, else 'vertical'.
  • defaults, on a create only: merged under the values the host already carries (URL prefill on the page; the formLink pre-link or the opener's seeded values in the dialog), which win per field. Before, any prefill dropped defaults wholesale, and the dialog never read them. The merged seed is cached on the metadata payload and on the host values by content, so it keeps its identity across renders (the forms re-seed on a new initialValues), with no dependency on a memoised identity (ui/AGENTS.md chore: version packages #10).

Precedence, as measured: no flat showSubmit / submitText / showCancel / cancelText key is author-written. FormViewSchema is strict and refuses them, and a census over examples/** and packages/** finds the one submitText on a page block (project-workspace.page.ts, an object-master-detail-form node), none on a form view. So the hosts' flat values are console defaults, and the authored form-view keys win.

The form page (ui/apps/console/src/components/FormPage.tsx) is its own renderer and reads the same keys locally: readFormButton for the submit / cancel label and visibility (the public route still draws no Cancel), and readPrefill takes the form view's defaults on a create, over the object's field defaults and under a ?prefill_ param.

The tab keys:

  • resolveFormViewLayout copies defaultTab / tabPosition beside contentLayout: 'tabbed', and ModalForm's tabbed arm hands them to the form renderer (defaultFieldTab / fieldTabsPosition).
  • ModalForm's subforms arm now passes formType (tabbed when the content layout is), the tab keys, layout and showSubmit to MasterDetailForm, and hands onCancel only when Cancel is shown.
  • ObjectForm's master-detail branch forwards defaultTab / tabPosition / layout, and onCancel only when showCancel is not false; MasterDetailForm passes the three keys to its header ObjectForm.

Not in this PR (the seat's split): buttons.reset and the drawer keys (drawerSide / drawerWidth), which stay on #26843 for an implement-or-retire spec card.

Tests

New, each block with a CONTROL where the key is absent:

  • ui/packages/app-shell/src/utils/__tests__/formViewChrome-26843.test.ts: the resolver's precedence, the per-locale label, the refused layout: 'grid' and tabPosition values, the per-field merge and its identity, edit reads no defaults.
  • ui/packages/app-shell/src/views/RecordFormPage.formViewChrome-26843.test.tsx: the page hands the authored chrome to ObjectForm; a URL prefill wins per field.
  • ui/packages/app-shell/src/console/__tests__/AppContent.formViewChrome-26843.test.tsx: the dialog hands the authored chrome and tab keys to ModalForm; a formLink pre-link wins per field and the seed has one identity across renders; the real dialog draws the authored label and no Cancel.
  • ui/packages/plugin-form/src/__tests__/formViewTabKeysAndButtons-26843.test.tsx: real renders. A tabbed form with subforms opens on defaultTab with a vertical strip for tabPosition: 'left'; the modal tabbed arm and its subforms arm likewise; master-detail Cancel and Save follow showCancel / showSubmit.
  • ui/apps/console/src/components/FormPage.formViewChrome-26843.test.tsx: authored labels and hidden buttons on the internal and public routes, defaults on a create, a prefill_ param wins, an edit reads the record.

Acceptance notes

  • layout: 'horizontal' now reaches every form the console opens, but the shared form renderer's only reading of it is the action row's alignment (sm:justify-end against sm:justify-start). The dialog draws its buttons in its own footer, which is always end-aligned, and the form page's action row is always end-aligned too. So in those two places the key arrives and nothing is drawn differently. What horizontal should draw (labels beside inputs, as the spec's "Field layout direction" reads) is a renderer-contract question for the seat; it is listed in the report.
  • TabbedForm hard-codes layout: 'vertical' on the form it builds, so horizontal on a tabbed form view is dropped there. The file is outside this card's surface; it is listed in the report.
  • A master-detail form with showSubmit: false hides its whole action bar, Cancel included, as before.
  • packages/spec/liveness/view.json rows for buttons and defaults cite RecordFormPage as forwarding them; it forwarded them, but then set its own flat keys over buttons, and the New dialog and the form page read neither. The ledger is not edited here (packages/spec/** is out of bounds); the stale evidence is listed in the report.

Verification

Head 7e4a1249 (after merging origin/main at 3271a083), all under scripts/pm/os-verify-lock.sh:

Affected-package tests and type-checks
  • pnpm --dir ui exec vitest run packages/plugin-form/: Test Files 204 passed, Tests 2422 passed / 1 skipped.
  • app-shell (every RecordFormPage*, AppContent* and utils test): Test Files 81 passed / 1 skipped, Tests 949 passed / 8 skipped.
  • every FormPage* test in ui/apps/console: Test Files 34 passed, Tests 328 passed.
  • type-check for @object-ui/plugin-form, @object-ui/app-shell and @object-ui/console: exit 0 (the console one after building its closure, --filter '@object-ui/console^...').
Ablations (each restored, blob equal to HEAD, `git diff HEAD` empty)

Driven through scripts/ablation-replace.mjs; every mutation landed on disk and turned its test red:

  • RecordFormPage hard-codes its chrome again: 4 failed.
  • the New dialog hard-codes its chrome again: 6 failed.
  • ModalForm's tabbed arm drops defaultFieldTab: 1 failed.
  • ModalForm's subforms arm drops formType: 3 failed.
  • ObjectForm's master-detail branch drops defaultTab / tabPosition: 1 failed.
  • MasterDetailForm's header drops defaultTab: 2 failed.
  • ObjectForm's master-detail branch hands onCancel regardless: 1 failed.
  • FormPage's submit label ignores buttons: 2 failed.
  • FormPage's load ignores defaults: 2 failed.
  • the resolver rebuilds the merged seed per call: 2 failed.

Two first attempts were no-ops the tool refused (a replacement containing its anchor, an ambiguous anchor); both were re-run on a different anchor, and the counts above are the re-runs.

Derived gates

node scripts/pm/dispatch-gates.mjs --ran on head 7e4a1249: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. Every one exited 0; the last two lines are run in addition to the derived 34.

  • node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0
  • node scripts/check-adr-0087-registration.mjs --self-test: exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0
  • node scripts/check-closing-keyword-parity.mjs: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test: exit 0
  • node scripts/check-comment-mask-corpus.mjs: exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main: exit 0
  • node scripts/check-empty-changeset.mjs --self-test: exit 0
  • node scripts/check-sdui-manifest.mjs: exit 0
  • node scripts/check-sdui-manifest.mjs --self-test: exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test: exit 0
  • node scripts/release-pending-publish.mjs --self-test: exit 0
  • pnpm --dir ui exec vitest run scripts/: exit 0
  • pnpm --filter @objectstack/spec run check:duration-unit-keys: exit 0
  • pnpm check:changeset-gate-self-tests: exit 0
  • pnpm check:closed-ceremony-text: exit 0
  • pnpm check:cross-package-test-inputs: exit 0
  • pnpm check:dispatcher-error-vocabulary: exit 0
  • pnpm check:driver-memory-census: exit 0
  • pnpm check:engine-double-contract: exit 0
  • pnpm check:gitlink-declared: exit 0
  • pnpm check:nul-bytes: exit 0
  • pnpm check:pm-changeset-deadline-census: exit 0
  • pnpm check:published-files: exit 0
  • pnpm check:query-options-erasure: exit 0
  • pnpm check:refd-timer-probe: exit 0
  • pnpm check:skill-identifier-liveness: exit 0
  • pnpm check:test-source-alias: exit 0
  • pnpm check:tier-file-adoption: exit 0
  • pnpm check:type-check-coverage: exit 0
  • pnpm check:type-check-debt: exit 0
  • pnpm check:undeclared-dep-imports: exit 0
  • pnpm check:watch-hint-literal: exit 0
  • pnpm check:where-matcher: exit 0
  • pnpm --dir ui check:spec-symbols: exit 0
  • pnpm --dir ui check:new-line-citations: exit 0

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (CI).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbabN2bfLWeaoNk6Jue

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

  • 改了什么:控制台所有打开表单的入口(记录表单页、新建/编辑弹窗、公开表单页)现在都按表单视图里写的 buttons(提交/取消按钮的显示和文字)、defaults(新建时的默认值)渲染;带子表的分页表单按 defaultTab / tabPosition 打开。
  • 为什么改:这些键能保存、能发布,但控制台三个入口各自写死了按钮和布局,作者写了等于没写。
  • 风险与回滚:只改控制台和表单插件的取值,不改 spec;URL 预填仍优先于 defaults;回滚即 revert。
  • 先看:ui/packages/app-shell/src/utils/recordFormNavigation.ts 的 resolveFormViewChrome。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/utils/__tests__/formViewChrome-26843.test.ts。
  • 需要你做的:无;CI 全绿后由席位落地(本会话落地动作若被权限拦截,会请你点一下)。
  • 席位意见:这是拆分后的部分落地(Refs,不关卡)。layout: horizontal 现在已送达表单,但渲染器只用它对齐按钮行,视觉上还没变,和 drawer 键、buttons.reset 一起留在卡上。

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head 7e4a12495b · 2026-10-28T02:09Z

  1. Shape: draft, base main; the body says Refs #26843 (items buttons, layout, defaults, defaultTab, tabPosition) and no closing keyword sits next to #26843 (the second mention, "Not in this PR (the seat's split) … stay on console forms: form-view keys that save and publish are not rendered — buttons, layout: "horizontal", drawerSide / drawerWidth; defaults apply only on the full-page /new; a tabbed form with subforms loses defaultTab / tabPosition #26843", carries no closing verb), so merging leaves the card open by design. 13 files, +1101/−34; not governed; ≤ 3000 lines.
  2. Premise measured: seat read of resolveFormViewChrome and its RecordFormPage use (commit 38c8664a): authored buttons.submit/cancel show/label (localized through pickLocalized) win over the host's labels; layout is schema-checked before use; defaults seed create only, and host prefill (createValues) wins on conflict; the hard-coded layout: 'vertical' and the flat label overrides are gone. defaultTab/tabPosition reach the tabbed layout only after a FormViewSchema shape check.
  3. Gate check-runs (pinned to head 7e4a12495b): in progress at this record; landing waits for every check green. The dev's local reading: 34/34 derived families exit 0, --ran 0 NOT MEASURED, 46 new tests, 10/10 ablations red then restored.
  4. Accepted deviations: none beyond the body note (Human Minimum item 8 vs the Chinese 速读, which the order requires).
  5. Out of scope (report out_of_scope_findings): layout: horizontal draws nothing different (renderer aligns only the action row; TabbedForm hard-codes vertical) and FormPage ignores type: tabbed — both stay on console forms: form-view keys that save and publish are not rendered — buttons, layout: "horizontal", drawerSide / drawerWidth; defaults apply only on the full-page /new; a tabbed form with subforms loses defaultTab / tabPosition #26843 with buttons.reset and the drawer keys, named in the release on merge. packages/spec/liveness/view.json stale evidence rows (buttons, defaults) — Acceptance notes, spec-lane bookkeeping, no user-visible defect. Master-detail showSubmit: false hiding Cancel and RecordFormPage reading subforms from the default form view — Acceptance notes (pre-existing, not measured at a public door). Skills-docs: declared kind is semantic: the keys were already taught as live, the diff makes the console honour them, so no skills follow-up is owed.
  6. Landing clause: not governed ⇒ when every check on the current head is green, the seat lands it (pr_ready + automerge_enable), or asks the maintainer to click if the classifier refuses. On merge: Release: on console forms: form-view keys that save and publish are not rendered — buttons, layout: "horizontal", drawerSide / drawerWidth; defaults apply only on the full-page /new; a tabbed form with subforms loses defaultTab / tabPosition #26843 naming the delivered items and the remainder, back to pm:queue, and an implement-or-retire spec card for the drawer keys and buttons.reset.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 40654628276 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test UI (13/16) — 失败步骤: Run the ui tests (shard 13/16)

    × zh-CN: the header reads the served 销售代表, and a save keeps the authored label 1150ms
    × ja-JP: the header reads the label the served row carries 1072ms
    × a set the published list does not hold keeps its draft label 1081ms
    × an edit to the label shows the edit, not the served label 1063ms
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > zh-CN: the he
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > ja-JP: the he
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > a set the pub
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    FAIL   dom  packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.servedLabel-26884.test.tsx > PermissionMatrixEditPage — header shows the served label (objectstack#26884) > an edit to th
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 30 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants