Skip to content

fix(app-shell): hold a new item whose name the client refuses; one message per mistake on the Setup metadata edit page - #27938

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-uk5z33
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-uk5z33

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

Requested by Bill · project thread

维护者速读

  • 改了什么:Setup 里新建元数据(实测页面是「权限集 › 新建」)时,名称被编辑器自己的校验判为不合法,点保存就不再发请求,只在「名称」输入框下显示那一条错误;名称由标签自动生成、没碰过的,点保存后错误也会显示在输入框下。页面顶部的摘要横幅不再重复表单已经画在字段上的错误;服务端拒绝时如果点名的是表单画得出的字段,也只在那个字段下说一次。
  • 为什么改:以前坏名照样发出去,页面先出服务端拒绝("is not a legal metadata item name"),再改一下又叠出摘要横幅("does not match the spec — 1 validation error(s)")和字段下的错误,一个错误三条消息(Setup and Studio: suggestions from the 18.3.0 RC browser walkthrough: Setup naming and machine values, invitation and user onboarding, permission-set authoring, dialog focus, Studio first run, validation and flow builders, and publish-panel wording #27521 第 16 行拆出的卡)。
  • 风险与回滚:只改前端。暂缓只在「新建」且只看「名称」字段:新建表单总有名称输入框,作者总能改成两端都接受的名字,所以不会卡死保存;其余实时校验仍是提示性的,照常发送、以服务端为准(编辑已有条目从不因此暂缓)。回滚即 revert 本 PR。
  • 先看:ResourceEditPage.tsx 的 doSave(新增的暂缓分支与 422 分支里的 drawnInline)、SchemaForm.tsx 的 useInlineIssueFieldRegister 与 revealIssues、新测试 ResourceEditPage.heldBadName-27865.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/ResourceEditPage.heldBadName-27865.test.tsx,预期 1 个文件 4 条全部通过(坏名零请求只剩内联错误、服务端点名 name 只说一次、标签生成的坏名同样暂缓、好名照常保存)。
  • 需要你做的:无,席位落地。

Fixes #27865

Clause-②: no
Metadata-protocol: none

What changes

Measured first, on main at e6553619 against the stock showcase (objectstack dev --seed-admin --fresh, admin, Chromium 1440×900), Setup › Permission Sets › New, Name typed as Bad Name:

  • before Save: the summary banner (metadata-validation-banner, "does not match the spec — 1 validation error(s)", listing Name) and the inline error under Name, for the one mistake;
  • on Save: one PUT /api/v1/meta/permission/Bad%20Name left, answered 400 with "is not a legal metadata item name"; the page drew that refusal and cleared the inline error, so the next edit brought the summary banner and the inline error back beside the still-standing refusal: three messages.

Now, re-driven on the same stack at this branch: typing Bad Name shows only the inline error; Save sends no request and the inline error stays the one message; probe_set still creates (one PUT).

  1. Hold (doSave). On the create door, before any request, the page runs the same live client validator the form already shows (validateMetadataDraft, mode create, on the same draft; no second rule), awaited so a Save inside the 200 ms debounce cannot slip past it. When it refuses the identity field, nothing is sent: the issues are put back on the form and the Save attempt ends. This is the one place a predicted verdict holds Save, and it is bounded so the dead-bolt the advisory ruling guards against stays out of reach (see Acceptance notes): only createMode, only the identity field.
  2. Reveal (SchemaForm revealIssues). The create form defers a field's error until the field is touched. A Save attempt ends that deferral, so a Name derived from the Label and never touched (Label A gives a, under the spec's two-character floor) shows its error inline instead of nowhere.
  3. One message per mistake. The outermost SchemaForm now reports which top-level fields draw their issues inline (a row registers while mounted and not deferred; nested forms do not register; the register is held in state, not memoised, per ui/AGENTS.md chore: version packages #10). The page then:
    • leaves those fields' errors out of the summary banner, but only errors that are also among the issues the form received, so a server-only diagnostic is never dropped;
    • on a 422 whose issues all sit on fields the form draws, skips the refusal line (setError), so the server's refusal is said once, on the field. Only where a client validator exists, because that is where the summary banner reads these issues: if the field later leaves the screen, the banner lists it again.
      Errors the form cannot draw on a field (root, nested key, a field off screen or in a closed section) stay in the banner, unchanged.

The Save button gate, the autosave timer and the ⌘S door are untouched: the hold lives in doSave, which all three call.

Tests

  • New ResourceEditPage.heldBadName-27865.test.tsx: the real page over a real MetadataClient whose in-memory transport records every PUT and answers like the save door (an ungrammatical name 400 INVALID_REQUEST with no issues, read from the spec's own METADATA_ITEM_NAME_PATTERN; taken_set 422 with an issue at name). The live permission registration (registerBuiltinAnchors: Label → Name derive, createLive) is mounted. (a) bad Name, Save clicked inside the debounce, then again: zero PUT, exactly one inline issue whose text appears nowhere else, no summary banner, no server refusal; (b) a Name the client accepts and the server refuses at name: one PUT, one message, inline; (c) Label A derives a, untouched: held, zero PUT, the error shown inline; (d) control: a good Name saves.
  • ResourceEditPage.schemaAdvisory.test.tsx (the advisory half: a stored item with a key the bundled spec does not declare still saves) and ResourceEditPage.serverRefusalGate.test.tsx pass unchanged.
Readings
  • Affected set, on the head after merging main (ab56afe9; PackageFormDialog, which hosts SchemaForm, moved on main meanwhile): every test file under ui/ naming ResourceEditPage, SchemaForm or PackageFormDialog, 187 files: Test Files 187 passed, Tests 1921 passed; lock VERDICT command-exit 0 (6m59s). Before the merge, the same set without PackageFormDialog (169 files): 1765 passed.
  • Ablations (scripts/ablation-replace.mjs; app-shell tests import src, so no dist leg; each restored blob equal to HEAD, git diff HEAD empty):
    • hold condition disabled: (a) red, 1 failed of 3;
    • banner filter disabled: (a) and (b) red, 2 failed of 3. A first attempt at this one was a no-op (the marker spelling left the expression's value unchanged, 3 passed); re-run with a marker that removes the filter;
    • refusal-line skip disabled: (b) red, 1 failed of 3;
    • reveal disabled: (c) red, 1 failed of 4.
  • Known-stricter check (asked by the order): for every metadata type with a client create validator (20 types), probe names ok_name, ab, a, x1, crm_lead.pipeline, a.b, abc_, a__b and a 67-character name, judged by the client create pass against the server's registry schema plus the door's name grammar: no type where the client refuses a Name the server accepts (scratch run, not committed).
  • pnpm --filter @object-ui/app-shell run type-check (both the src and the test program), after rebuilding @object-ui/app-shell^... on ab56afe9: lock VERDICT command-exit 0.
  • ui check:new-line-citations: 0 new.
  • Derived gates (dispatch-gates --commands), run on ab56afe9: 34 derived, 34 run, 0 NOT-MEASURED, all exit 0 (also all exit 0 on a18abd01, before the merge).

Acceptance notes

  • The advisory ruling, named rather than worked around. ResourceEditPage records, and ResourceEditPage.schemaAdvisory.test.tsx pins, that the live client Zod pass stays advisory because a client stricter than the server would dead-bolt Save "on a draft the server accepts, with no on-screen editor able to take the key back out". This card asks for a hold on that same verdict, so the hold is bounded to where that failure cannot occur: the create door's identity field, which the create form always offers as an input, so the author can always type a name both ends accept. Every other live issue stays advisory, the Save button gate is unchanged, and an edit is never held. The boundary is written beside the ruling in ResourceEditPage.tsx and at the hold itself. The seat kept it (review on ab56afe9). The bound is pinned on the create door by case (e) of heldBadName-27865: a good Name beside a client-refused non-identity key still sends. Widening the hold to any issue turns (e) red, and only (e). schemaAdvisory's header names the carve-out beside its serverRefusalGate paragraph, with no assertion changed (rework head fc4cb80a).
  • A 400 refusal that carries no field path (the door's name grammar, for a name the client accepts) stays one message in the refusal line: it names no field to place it on. No such name exists for a permission set today: the client's SnakeCaseIdentifierSchema is at least as strict as the door's grammar.
  • On the create form, before any Save attempt, a field's error is shown once the field is touched and is then no longer repeated in the banner; an untouched field's error still shows in the banner until then, as before.
  • Out of this card: the sibling spec card New Permission Set form asks for the API Name before the Label and leaves Label optional (#27521 row 16) #27864 (Label first + required).

Generated by Claude Code

…age per mistake (#27865)

New Permission Set sent a name the client validator had already refused,
then stacked the server refusal, the summary banner and the inline error.
A create whose identity field the live pass refuses is now held with no
request, the Save attempt reveals the create form's deferred field errors,
and an error the form draws on its field is no longer repeated by the
summary banner or the refusal line.

Claude-Session: https://claude.ai/code/session_01Utp6CZcCCHhGAp4cezRhuD
Co-authored-by: Claude <noreply@anthropic.com>
…n an effect, not during render (#27865)

Claude-Session: https://claude.ai/code/session_01Utp6CZcCCHhGAp4cezRhuD
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 30, 2026
…out beside the advisory suite (#27865)

A good Name beside a client-refused non-identity key (the server-gained-a-key
skew shape) still sends on the create door, so widening the hold to any live
issue goes red. The advisory suite's header names the carve-out and points at
the suite that pins both of its halves.

Claude-Session: https://claude.ai/code/session_01Utp6CZcCCHhGAp4cezRhuD
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-bill · session_01Utp6CaJoinvGQSbHeDaWBe

席位核对

ACCEPT · head fc4cb80aa97eaba54ae00a819404d1362fc51573 · 2026-10-30T05:03Z

  • Shape: draft → main, Fixes #27865. Clause-②: no and Metadata-protocol: none match the claim. The change is @object-ui/app-shell only, so no contract review is owed.
  • Premise, measured: on New Permission Set, a bad Name was sent anyway. The result was a 400 refusal, then the summary banner, then the inline error: three messages for one mistake.
    • Now a create whose identity field the live client pass refuses is held in doSave: no request goes out, and the inline error is the only message. The pass runs the same validateMetadataDraft, awaited, so a Save inside its debounce can't slip past it.
    • A Save attempt ends the create form's show-on-touch deferral.
    • The banner leaves out errors the form already draws on a top-level field, and a refusal fully drawn on fields isn't repeated on the refusal line.
  • The advisory ruling (objectui#6980): the seat read it and kept the bounded hold (option A). The ruling's reason is a dead-bolt on a server-accepted draft with no editor to remove the key. That can't happen on the create door's Name, which is always an input; a stricter client costs the author a different name.
    • The bound is pinned by case (e): a good Name with a client-refused non-identity key on the create door still sends. Widening the hold to any issue turns only (e) red.
    • schemaAdvisory's header now names the carve-out beside serverRefusalGate, with no assertion changed.
    • Every edit and every other live issue stays advisory, and the Save button gate is untouched.
  • Tests: heldBadName-27865 5/5 on the real page over a real MetadataClient, with an in-memory door. Five ablations, each red as expected and restored to the HEAD blob.
  • Gates (check-runs on the head): 44 success, 11 skipped, 0 failure; mergeable_state clean. check-governed-merges --pr 27938: NOT governed, 377 changed lines.
  • Accepted deviations: the body's Acceptance note was patched to record the seat's reading and case (e). The branch was pushed once before its first commit, which has no effect on the diff.
  • Out of scope: New Permission Set form asks for the API Name before the Label and leaves Label optional (#27521 row 16) #27864 (Label first + required, spec).
  • Landing: non-governed, all checks green, mergeable_state clean ⇒ the seat lands it (pr_ready + automerge_enable).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 05:05
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 05:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 421bb6b Oct 30, 2026
60 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-uk5z33 branch October 30, 2026 05:40
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/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setup → metadata edit page saves a bad name anyway and stacks three messages (server refusal, client summary, inline error) (#27521 row 16)

1 participant