Skip to content

fix(app-shell): optional choice rows in Studio metadata forms can be cleared again - #29805

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-pcbqof
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-pcbqof

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29733

Clause-②: no
Metadata-protocol: none

Claim: 6595203212 (amendment 6595600429)

维护者速读

  • 改了什么:Studio 元数据表单里「可选」的下拉选项(枚举字段)现在第一项是「— 无 —」,选它即把该键从元数据里删掉(恢复为「未声明」)。必填的下拉、以及列表里的单个元素不变,不出现这一项。另外修了同一条链路上的第二个原因:动作、仪表盘、钩子、页面、报表五个编辑器的「更多字段」区,清空任何一项(选「无」或清空文本框)以前会被宿主的合并立刻还原,现在清空能落到草稿并保存为「无此键」。
  • 为什么改:卡片 Studio metadata forms: an optional enum select has no empty item, so a chosen deleteBehavior on a user field cannot be cleared #29733 指出可选枚举选过一次就回不去「未声明」;对 user 字段的 deleteBehavior 来说「未声明」本身就是一种生效的语义(不进删除级联)。实测发现 Studio 的字段面板目前根本不显示 deleteBehavior 这一行(席位另立卡),但缺陷本身在「更多字段」区可复现(动作的 openIn 只能在 self / new-tab 之间选)。
  • 风险与回滚:只改界面,不动任何元数据契约;必填项、无值时的占位文字、合并逻辑本身都不变。清空的键以 undefined 进入草稿,保存时 JSON 自然省略。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx 的 offerUnset 与两处 Select;inspectors/fallbackFormPatch.ts;五个 DefaultInspector 各一行 onChange。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/SchemaForm.optionalEnumUnset-29733.test.tsx packages/app-shell/src/views/metadata-admin/inspectors/fallbackFormPatch-29733.test.tsx,预期 12 个用例全部通过。
  • 需要你做的:无,席位落地。

What changes

1. An optional enum select offers "— None —". Both builtin select paths of the metadata-admin SchemaForm (a form row's options, and a schema property's own enum) listed only the enum values, so an optional key, once picked, could never return to absent. FieldControl now leads an optional row's list with a "— None —" item: the existing engine.form.none key (en "— None —", zh "— 无 —"), the word the metadata-admin pickers already use, so no new i18n key. Radix refuses an item whose value is the empty string, so the item carries a sentinel that onValueChange maps back to undefined, the form's existing clear convention (setField deletes the key; a repeater row keeps an undefined that serialisation drops). With nothing set, the trigger still shows the "Select…" placeholder and nothing is written on render.

Row Before Now
optional enum, either path values only "— None —" first, then the values
required enum (schema required, a form row's required, a grid column's required) values only unchanged
one element of a scalar list of an enum values only unchanged (an element is removed with its own row button)

2. Clears in the "More fields" fallback form reach the draft. The action, dashboard, hook, page and report default inspectors render a fallback SchemaForm and wired it onChange={(next) => onPatch(next)}. The host's onPatch merges the patch over the draft, and SchemaForm reports a clear by deleting the key from next, so the merge kept the old value: a None pick, an emptied text box or an emptied number box there was undone at once. The new pure helper fallbackFormPatch(draft, next) returns next plus every key the draft had and next dropped, set to undefined; the five call sites route through it. onPatch's merge itself is unchanged, and a key the form did not touch is never turned into undefined.

Tests

  • SchemaForm.optionalEnumUnset-29733.test.tsx (new, 8), over the real SchemaForm: the schema-enum path and the form-options path each emit an object without the key after "— None —" (nothing else moves); a repeater card row (the object form's fields rows, where deleteBehavior sits) serialises without the key; the placeholder still shows and nothing is emitted on render. CONTROLS: a required enum on each path offers no None item; in a grid repeater a required column offers none while an optional one does; a scalar list element offers none.
  • inspectors/fallbackFormPatch-29733.test.tsx (new, 4): the helper through a merge leaves a cleared key absent and an edited key at its new value; CONTROL: untouched keys pass through and nothing becomes undefined; the real ActionDefaultInspector under a merging host: "— None —" on openIn and an emptied description each leave the draft without the key, with the curated keys untouched.
  • Ablations (scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty): A1 offerUnset forced false, 4 of 8 red (the three positive pins and the grid optional column), the 4 controls green. A2 the helper's undefined-for-dropped-key line removed, 3 of 4 red, the control green.
  • Touched suites, under the verify lock at --maxWorkers=2: the 144 ui test files that name SchemaForm, one of the five inspectors or engine.form.none (git grep -l), 1464 tests, all green.

Acceptance notes

  • The card's named door is not reachable in Studio today (measured). The Studio object designer's field panel (ObjectFieldInspector) offers no Delete Behavior row on a user field nor on a lookup field; the deleteBehavior rows live in object.form.ts / field.form.ts, which SchemaForm renders, and for object the editor shows the designer and its inspectors instead. So the named consequence (a cascade or restrict left in force on a user field) is prevented here for the moment a form that renders those rows is reached. The missing row in the designer's field panel is filed by the seat as its own finding.
  • Surface: widened by claim amendment 6595600429 to the five default-inspector onChange call sites and one helper, after the live run showed a None item alone would be a dead control there. Clearing a text box or a number box in those fallback forms is fixed by the same change.
  • Defaults: an enum whose schema carries a default is listed in the served schema's required and so offers no None item (hook onError, runAs). That is the existing required signal, not changed here.
  • Not changed: the action inspector's own curated variant select (an InspectorSelectField, not SchemaForm) still has no unset item; it is outside this card.
  • Narrowing declared: the full packages/app-shell suite is CI's (Test UI); locally the 144-file grep set above ran.

Verification

Real backend (examples/app-crm, objectstack dev --fresh) and the source Console (Vite on ui/apps/console), admin session, a scratch package with an object udb_ticket (a user field assignee, a lookup field parent_ticket) and an action udb_escalate (type: url, openIn: self). Screenshots are in the project folder (issue-29733/), not on GitHub.

Before (main 6f09464), the "More fields" Open In select on the action
  • Options read ["self","new-tab"]: no way back to absent.
  • With only the SchemaForm half applied, picking "— None —" left the trigger at self: the merging host kept the key.
  • The field panel for assignee (user) and parent_ticket (lookup) shows no Delete Behavior row.
After, on this branch
  • Options read ["— None —","self","new-tab"].
  • Pick "— None —": the trigger shows the "Select…" placeholder; Save (draft), reload: still unset; Publish.
  • GET /api/v1/meta/action/udb_escalate after publish: the item carries no openIn (before it read "openIn":"self").
Local gates

On head 74e47d1e (main merged at 25450217; 11 later main commits touch none of this PR's files): node scripts/pm/dispatch-gates.mjs --commands derives 35 commands; each ran with exit 0 (check:type-check-debt and pnpm --dir ui exec vitest run scripts/ under the verify lock), and --ran reconciles 35 derived, 35 run, 0 NOT-MEASURED. Also exit 0: pnpm --filter @object-ui/app-shell run type-check; eslint on the 9 touched files (0 errors; the warnings are on untouched lines); check-partof-closing-keyword; check-closing-keyword-parity --body (binds only this card). The two new test files rerun on this head: 12/12. dispatch-gates --tier: no path-derived mandate (default tier); 520 changed lines before the main merge, under 3000. The repo-wide sweep (pnpm lint and the rest of the farm) is CI's.

🤖 Generated with Claude Code


Generated by Claude Code

… that unsets the key

Both builtin select paths of the metadata-admin SchemaForm (form options and
the schema's own enum) listed only the enum values, so a picked value on an
optional key could never be returned to absent. An optional row now leads
with the existing engine.form.none item; choosing it calls onChange(undefined),
the form's clear convention (setField deletes the key). Required rows and
scalar list elements are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUDMc41NruhHkjaN5X
…draft as absent

The "More fields" SchemaForm in the action, dashboard, hook, page and report
default inspectors handed its emitted object straight to onPatch, which the
hosts merge over the draft. SchemaForm reports a cleared key by deleting it,
so the merge kept the old value and every clear (a None pick, an emptied text
box) was undone. fallbackFormPatch returns the dropped keys as explicit
undefined, which the merge applies and the saved JSON omits.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jUDMc41NruhHkjaN5X
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · zhuangjianguo · session_01QsR2jRFuJz9QBZqPVHYVhJ

维护者速读

  • 改了什么:Studio 元数据表单里可选的下拉项,第一项现在是「— 无 —」,选它就把该键从元数据里去掉。必填项和列表里的单个元素不加这一项。另外,动作、仪表盘、钩子、页面、报表这五个编辑器的「更多字段」区,以前清空任何一项(选「无」或清空文本框)都会被立刻还原,现在会真正清掉,并保存为没有这个键。
  • 为什么改:可选枚举一旦选过值,就再也回不到「未声明」。对 user 字段的 deleteBehavior 来说,「未声明」本身是一种生效的语义。
  • 风险与回滚:低。只改界面,元数据契约不动;必填项、未设值时的占位文字、宿主的合并逻辑都不变。回滚即 revert 本 PR。
  • 先看:ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx 的 offerUnset 和两处 Select;inspectors/fallbackFormPatch.ts。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/SchemaForm.optionalEnumUnset-29733.test.tsx packages/app-shell/src/views/metadata-admin/inspectors/fallbackFormPatch-29733.test.tsx,预期 12 个用例全绿。
  • 需要你做的:无。席位验收后进合并队列。

席位意见: 实测发现卡片举的 deleteBehavior 例子在 Studio 字段面板里根本没有这一行,已另立 #29807。缺陷本身在「更多字段」区能复现,而且只加「无」不够:宿主合并会把清空还原。所以同一 PR 修了五个调用点,否则新加的这一项是个点了没用的按钮。

席位核对 — ACCEPT · domain:ui#2 · head 74e47d1e · 2026-11-03T05:50Z

  • 形态: draft PR on main, Fixes #29733 on its own line. 10 files, +512/−8. Tier S: check-governed-merges --pr 29805 says not governed, under 3000 lines. All files are inside claim 6595203212 / amendment 6595600429: SchemaForm.tsx (the two enum Select paths, offerUnset, the listElement flag on scalar repeater rows), the new pure inspectors/fallbackFormPatch.ts, one onChange line in each of the five DefaultInspectors, two test files, one changeset. No new i18n key; it reuses engine.form.none.
  • 前提实测: the card's door (a user field's deleteBehavior in Studio) does not exist. The designer's field panel has no such row on a user or a lookup field (screenshots /mnt/project-files/issue-29733/before-user-field-panel-no-deleteBehavior.png, lookup-field-panel-no-deleteBehavior.png), so that is filed as Studio: the object designer's field panel offers no Delete Behavior row on a lookup or user field, so deleteBehavior cannot be set or cleared from Studio #29807. The defect is live on the action's fallback openIn: on main the options were ["self","new-tab"]. On this head they are ["— None —","self","new-tab"], and None → save → publish → GET /meta/action/udb_escalate has no openIn.
  • 门禁 check-runs (pinned to head 74e47d1e17fb2c0a18a9214b0844fe9d82cc0bc6): 55 runs, 0 failed, 0 pending, the rest success or skipped by path. That includes Lint & Repo Gates, TypeScript Type Check, Type Check UI, Lint UI, Test UI (16/16 plus the aggregate), Build & E2E UI, Live E2E UI, Check Changeset, Governed Surface Queue Guard and PR-body guards. Dev local gates: 35 derived, 35 run, all exit 0, 0 NOT-MEASURED. The touched-suite sweep is 144 files / 1,464 tests. Ablations: A1 (offerUnset false) reds 4 of 8 with the controls green; A2 (the helper's undefined line removed) reds 3 of 4 with the control green.
  • Code read: the seat checked that every top-level emit of SchemaForm goes through setField ({ ...v, [key]: value } with v = value ?? {}). Each of the five call sites passes the same draft as value. So fallbackFormPatch(draft, next) sets undefined only on keys the form removed, never on the hidden curated keys. The real-inspector pin under a merging host proves it (curated name/label/type/target untouched). The unset sentinel never reaches the draft: onValueChange maps it to undefined.
  • 接受的偏差: the surface widened by amendment to the five inspector call sites (the merge cause, found live). The live proof is on action openIn, not the card's user field, because that door is absent (Studio: the object designer's field panel offers no Delete Behavior row on a lookup or user field, so deleteBehavior cannot be set or cleared from Studio #29807).
  • 范围外去向: filed Studio: the object designer's field panel offers no Delete Behavior row on a lookup or user field, so deleteBehavior cannot be set or cleared from Studio #29807, the designer field panel's missing Delete Behavior row. Acceptance note, not filed: the action inspector's curated variant InspectorSelectField has no unset item. It is a separate curated control with a visible default style, so there is nothing to clear to on the card's terms. No Skills-docs: line is owed.
  • 落地条款: Clause-②: no (@object-ui/app-shell is private), Metadata-protocol: none, patch. No contract review is owed. Land by pr_ready + automerge_enable.

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/l tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio metadata forms: an optional enum select has no empty item, so a chosen deleteBehavior on a user field cannot be cleared

1 participant