Repository navigation
Commit 0892156
fix(react): mark the field a unique-index refusal names in create/edit forms (#27043)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8FFTSFXvg3gyNdUibNK8VkN)_
Fixes #26891
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`@object-ui/react` 的 `extractFieldErrors` 现在识别 409
`UNIQUE_VIOLATION` 里单个 `field`,返回一条带服务端句子的字段错误;表单因此把重复值标在对应输入框下。
- 为什么改:服务端(#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`.
<details><summary>Tests and ablation</summary>
- 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.
</details>
<details><summary>Derived gates</summary>
`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.
</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. (eslint on the touched
files: 0 errors; the repo-wide run is CI's)
- [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. (not touched)
- [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 by [Claude
Code](https://claude.ai/code/session_01JQwxbZRo35mpCPrKXVXevY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 85c40fd commit 0892156
5 files changed
Lines changed: 152 additions & 2 deletions
File tree
- .changeset
- ui/packages
- components/src/renderers/form/__tests__
- react/src/utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
91 | 140 | | |
92 | 141 | | |
93 | 142 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
174 | 174 | | |
175 | 175 | | |
176 | 176 | | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
177 | 180 | | |
178 | 181 | | |
179 | 182 | | |
| |||
204 | 207 | | |
205 | 208 | | |
206 | 209 | | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
207 | 218 | | |
208 | 219 | | |
209 | 220 | | |
| |||
214 | 225 | | |
215 | 226 | | |
216 | 227 | | |
217 | | - | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
218 | 233 | | |
219 | 234 | | |
220 | 235 | | |
| |||
0 commit comments