Skip to content

fix(react): mark the field a unique-index refusal names in create/edit forms - #27043

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-mgvheq
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-mgvheq

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #26891

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:@object-ui/react 的 extractFieldErrors 现在识别 409 UNIQUE_VIOLATION 里单个 field,返回一条带服务端句子的字段错误;表单因此把重复值标在对应输入框下。
  • 为什么改:服务端(types(data-error-classification): a unique refusal on a single-column index tells the user "A record with this gsku already exists" — the field by its API key, while developerMessage carries its label #26867)已在拒绝里点名字段并用标签造句,但前端只读字段数组,唯一索引冲突仍只显示横幅加 toast,用户要猜是哪个字段重复。
  • 风险与回滚:只在 code 为 UNIQUE_VIOLATION 且带字符串 field 时生效;无 field(复合索引、索引名)照旧走横幅。回滚 = revert 本 PR。
  • 先看:ui/packages/react/src/utils/error-message.ts 的 extractFieldErrors 新分支;form-server-field-errors.test.tsx 新增的 unique-index 两例。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/react/src/utils/error-message.test.ts packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx,预期全绿;在 console 新建一个 SKU 重复的产品,SKU 输入框下出现 "A record with this SKU already exists",无 toast。
  • 需要你做的:无,席位落地。

What changes

  • extractFieldErrors (@object-ui/react): when no field-error list is present and the refusal's code is UNIQUE_VIOLATION (read from code, then details.code, the same pair isPermissionError reads), the single string field (details.field on the raw @objectstack/client error, field on a hand-rolled one) becomes one WriteFieldError whose message is the server's sentence (extractWriteErrorMessage). Without field the answer stays null.
  • No change to form.tsx: its existing extractFieldErrors then form.setError path marks the input, and a fully attributed refusal shows no banner and no toast.
  • Changeset: @object-ui/react patch.

Why

The server half shipped in #26867: the 409 UNIQUE_VIOLATION body is { error, code, developerMessage?, field?, object? }, with error naming the field by its label. @objectstack/client parks that flat body on err.details, and normaliseClientError (@object-ui/data-objectstack) passes a unique 409 through unchanged. The last hop read only arrays (validationErrors, details.fields, fields), so it answered null and the dialog showed the generic banner and toast.

Side effect, measured: InlineEditSaveBar (@object-ui/plugin-detail) calls the same helper, so on the atomic save path a unique refusal with field is now attributed to that field (labelled in the bar and published as the per-field hint) instead of the generic text; in callback mode it was already attributed to the in-flight field. Its suites stay green (below).

Tests

  • error-message.test.ts: a unique refusal with field yields one entry carrying the server's sentence (asserts code and httpStatus 409 on the refusal); a hand-rolled one with top-level field; without field it answers null; an INVALID_FIELD carrying field still answers null.
  • error-message.normalisation-boundary.test.ts: the refusal survives normaliseClientError with code, httpStatus and the field entry intact.
  • form-server-field-errors.test.tsx: a rejected submit marks the sku input (aria-invalid, sentence under it, no toast); without field the toast carries the generic sentence and no input is marked.

Acceptance notes

  • ApiDataSource, which embeds the body as a JSON tail in the message string, is not read for field; the ObjectStack adapter is the console's path.

Verification

Tree: a17ebf4e on base 7eb613c3.

Tests and ablation
  • Three touched files: Test Files 3 passed, Tests 46 passed.
  • Ablation via scripts/ablation-replace.mjs (unique branch replaced by return null): exactly the four new positive tests red (two helper, one boundary, one form render), 42 passed; restored, blob equals HEAD, git diff HEAD empty. Tests resolve @object-ui/react to src through the ui vitest alias, so no dist leg.
  • Consumer sweep under the lock: packages/plugin-detail/src/__tests__/, packages/react/, apps/console/src/pages/settings/, apps/console/src/pages/system/: Test Files 385 passed, 1 skipped; Tests 3607 passed, 18 skipped.
  • type-check for @object-ui/react and @object-ui/components: exit 0. eslint on the four files: 0 errors.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 34 families, each run, all exit 0, including node scripts/check-sdui-manifest.mjs and pnpm --dir ui exec vitest run scripts/. --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.

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. (eslint on the touched files: 0 errors; the repo-wide run is CI's)
  • 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: its generated artifacts are regenerated and committed. (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 by Claude Code

…rors

A unique-index refusal (409 UNIQUE_VIOLATION) names its one field on a
single string `field`, not a list, so extractFieldErrors answered null and
create/edit dialogs showed a banner and a toast instead of marking the
input. Read that field (details.field, or field on a hand-rolled error)
and return one entry carrying the server's sentence; without `field` the
answer stays null and the banner stays.

Claude-Session: https://claude.ai/code/session_01JQwxbZRo35mpCPrKXVXevY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head a17ebf4eb3 · 2026-10-28T02:56Z

席位意见:只认 UNIQUE_VIOLATION 且带单个字符串 field 的拒绝;服务端说不清是哪一列时照旧走横幅,不会把错误标到无辜的输入框上。行内编辑的保存条也随之点名字段,PR 已写明。

  1. Shape: draft, base main; the body says Fixes #26891, which matches the order (whole card). 5 files, +152/−2; not governed (check-governed-merges --pr 27043); ≤ 3000 lines.
  2. Premise measured: seat read of extractFieldErrors at head. The new branch runs only when no field list exists, and only when code (top-level or on details) is UNIQUE_VIOLATION and details.field / field is a non-empty string (firstString). Without field it still returns null, so the banner stays. The message is the server's sentence via extractWriteErrorMessage. The code check reuses errorCodeIs from @object-ui/types, as the delegation-scope branch does.
  3. Gate check-runs (pinned to head a17ebf4eb3): 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/46 tests, ablation turns exactly the 4 new positive tests red, consumer sweep 3607 passed.
  4. Accepted deviations: none. form.tsx is untouched; only a test was added beside it.
  5. Out of scope: none. The InlineEditSaveBar atomic path now attributes the refusal to the field; the order named this side effect and it is intended.
  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.

@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    × zh-CN: the header reads the served 销售代表, and a save keeps the authored label 1137ms
    × ja-JP: the header reads the label the served row carries 1078ms
    × a set the published list does not hold keeps its draft label 1063ms
    × an edit to the label shows the edit, not the served label 1053ms
    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 队列共有 32 个失败构建(不含本次)。

分诊清单:

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

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

Merged via the queue into main with commit 0892156 Oct 28, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mgvheq branch October 28, 2026 08:30
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 tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create / edit dialog: a unique-index refusal says "A record with this value already exists" without naming or highlighting the field

2 participants