Repository navigation
Commit 9f13ee8
fix(ui): list each failed validation rule as its own line in the form banner and toast (#28151)
domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao1RUqxpYW3tyxDQLmPkuSfY)_
Fixes #28053
Clause-②: yes (widening: @object-ui/react gains the public export
unmarkedRefusalLines; no accept set moves)
Metadata-protocol: none
## 维护者速读
- 改了什么:一次保存同时违反两条及以上校验规则时,表单横幅和 toast
改为每条规则一行;规则点名了表单上画出的字段时,消息留在该字段下,横幅只列其余的。
- 为什么改:以前横幅显示服务端用 `; ` 拼起来的一句话,用户读成一句长句,往往只改第一处;HotCRM 为此把两条预测规则改成互斥来绕开。
- 风险与回滚:只改控制台的展示,服务端拒绝信封不变(实测本就逐条规则带 `fields[]`);单条规则的拒绝在表单渲染器上不变;回滚即
revert 本 PR。
- 先看:`ui/packages/react/src/utils/error-message.ts` 的
`unmarkedRefusalLines`、`form.tsx` 提交 catch 里的
`shownInline`、`FormPage.tsx` 提交 catch。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/components/src/renderers/form/__tests__/form-refusal-lines-28053.test.tsx`,预期
3 个用例全绿。
- 需要你做的:无,席位落地。
## What changes
The server already says each failed rule separately: `ValidationError`
keeps one `fields[]` entry per rule, located on the field the rule
names, else on `_record`; only its top-level message is the `; `-join.
The console threw the entries away whenever one of them had no drawn
input and showed the joined text instead. Now:
- **`@object-ui/react`** adds `unmarkedRefusalLines(err,
isShownInline)`: one line per `fields[]` entry the caller could not show
under a visible input, in server order. It answers `null` (the caller
keeps the envelope's message) when the refusal has no entry list, or
when an entry it would have to say carries no human-readable `message`;
it answers `[]` when every entry is inline. It reads the same three
shapes `extractFieldErrors` reads (the adapter's `validationErrors`, the
client's `details.fields`, a bare `fields`), now through one private
`fieldEntries` reader.
- **Form renderer**
(`ui/packages/components/src/renderers/form/form.tsx`): the submit catch
records which rejected fields it wrote under a DRAWN input
(`fieldWrapper`), and the banner and toast say only the other entries,
one list item each. One remaining entry stays a plain string, so a
single-rule refusal reads exactly as before. The permission,
delegated-scope and author-marked `userMessage` paths keep their
precedence.
- **FormPage** (`ui/apps/console/src/components/FormPage.tsx`): the same
rule over the rows it marks. Its banner and toast used to show `Submit
failed (400): ` plus the raw response body for every server refusal; a
refusal with entries no row carries now shows those entries' messages
instead. A refusal whose every entry is marked on a row, or that has no
entry list, still shows what it showed before.
Both surfaces draw several lines as a `ul` of `li`, in the banner and in
the toast. No server, spec or envelope change.
## Tests
-
`ui/packages/react/src/utils/error-message.unmarked-refusal-lines.test.ts`
(new, 8 cases): the measured two-rule body; located entry left out; all
inline gives `[]`; two `_record` rules give two lines; adapter, client
and hand-rolled shapes agree; an entry with no field is said; an entry
with no message gives `null`; no entry list gives `null`.
-
`ui/packages/components/src/renderers/form/__tests__/form-refusal-lines-28053.test.tsx`
(new): two record-level rules give two banner items and two toast items
and no `; `; one located plus one record-level rule gives the inline
mark and one banner line; a single record-level rule is unchanged (the
toast gets the plain string).
- `form-server-field-errors.test.tsx`: the case for a rejected field the
form does not render now asserts the toast says only that field's reason
(it asserted a substring of the joined text).
- `FormPage.refusalMarks-27417.test.tsx`: two `#28053` cases (two
`_record` rules; one row-marked plus one record-level rule), and the
undrawn-field case now asserts the banner says the reason and no row
carries it (it asserted the reason appeared nowhere, which was the
defect).
**Reverse verification.** With both submit catches restored to the base
(`git restore --source=6410029d` of `form.tsx` and `FormPage.tsx`,
helper kept), 6 of the 22 cases in the three consumer files went red
(the four new consumer cases and the two updated ones); the single-rule
case stayed green, as it should. Restored with `git checkout HEAD --`,
proven by `git diff HEAD` empty and both blob hashes equal to `HEAD`'s.
## Acceptance notes
- **Premise measured on the wire, not only the evaluator.** On
`origin/main` `6410029d`, `POST /api/v1/data/:object` through
`RestServer` over a real `ObjectQL` and an SQLite `SqlDriver` (a
throwaway probe in `packages/rest`, not committed), with three
`cross_field` rules:
- one rule naming `best` plus one naming no field answered `400` with
`error` = the `; `-join and `fields` = `[{field: best, code:
rule_violation, message: …}, {field: _record, code: rule_violation,
message: …}]`;
- two rules naming no field answered two `_record` entries, each with
its own message.
So this is not a Seam; the ui lane fixes it.
- **FormPage single-entry change is deliberate.** A refusal with exactly
one entry no row carries used to show `Submit failed (400): ` plus the
raw JSON body; it now shows that entry's message. Same rule as the
multi-entry case.
- An entry with no human-readable `message` falls back to the envelope's
top-level text for the whole banner rather than putting an untranslated
code on a line by itself.
- Not changed here: FormPage's banner and toast for a server refusal
with no `fields[]` list, or whose every entry is marked on a row, still
show `Submit failed (STATUS): ` plus the raw response body. Noted, not
filed.
- Out of bounds and untouched:
`ui/packages/app-shell/src/console/AppContent.tsx`,
`providers/writeWarningToast.ts`, `views/RecordFormPage.tsx` (held by
#27999).
## Verification
- Head `f4877f89` adds only the changeset's `Clause-②` line over
`767d45bb`; the Check Changeset scripts (`check-adr-0087-registration`,
`check-changeset-no-major`, `check-empty-changeset`, each `--base
origin/main`) re-ran there, exit 0.
- Local subset on head `767d45bb`: the dispatch-derived gate list, every
line run with stdin from /dev/null, exit codes on disk, reconciled with
`dispatch-gates.mjs --ran`.
<details><summary>Derived gates (local)</summary>
```text
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 --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:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --dir ui run check:new-line-citations :: exit 0
dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN (exit 0)
pnpm --filter @object-ui/react type-check :: exit 0
pnpm --filter @object-ui/components type-check :: exit 0
pnpm --filter @object-ui/console type-check :: exit 0
```
</details>
<details><summary>Targeted tests</summary>
```text
pnpm --dir ui exec vitest run FILES (head 767d45b)
packages/react/src/utils/error-message.unmarked-refusal-lines.test.ts
packages/react/src/utils/error-message.test.ts
packages/react/src/utils/error-message.normalisation-boundary.test.ts
packages/components/src/renderers/form/__tests__/form-refusal-lines-28053.test.tsx
packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx
apps/console/src/components/FormPage.refusalMarks-27417.test.tsx
Test Files 6 passed (6)
Tests 67 passed (67)
os-verify-lock: VERDICT command-exit 0
Reverse verification (both submit catches at base 6410029): Tests 6 failed | 16 passed (22), VERDICT command-exit 1; restored, git diff HEAD empty.
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibwcER4bPAfAhbS8zYY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d70e94a commit 9f13ee8
9 files changed
Lines changed: 437 additions & 28 deletions
File tree
- .changeset
- ui
- apps/console/src/components
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
270 | 271 | | |
271 | 272 | | |
272 | 273 | | |
273 | | - | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
274 | 337 | | |
275 | 338 | | |
276 | 339 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
119 | | - | |
| 119 | + | |
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| |||
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| 165 | + | |
165 | 166 | | |
166 | 167 | | |
167 | 168 | | |
| |||
1948 | 1949 | | |
1949 | 1950 | | |
1950 | 1951 | | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
| 1955 | + | |
| 1956 | + | |
| 1957 | + | |
| 1958 | + | |
| 1959 | + | |
| 1960 | + | |
| 1961 | + | |
| 1962 | + | |
| 1963 | + | |
| 1964 | + | |
1951 | 1965 | | |
1952 | 1966 | | |
1953 | 1967 | | |
| |||
2382 | 2396 | | |
2383 | 2397 | | |
2384 | 2398 | | |
2385 | | - | |
| 2399 | + | |
| 2400 | + | |
2386 | 2401 | | |
2387 | 2402 | | |
2388 | 2403 | | |
| |||
2856 | 2871 | | |
2857 | 2872 | | |
2858 | 2873 | | |
2859 | | - | |
2860 | | - | |
2861 | | - | |
2862 | 2874 | | |
2863 | 2875 | | |
2864 | 2876 | | |
2865 | 2877 | | |
2866 | 2878 | | |
2867 | 2879 | | |
2868 | 2880 | | |
| 2881 | + | |
| 2882 | + | |
| 2883 | + | |
| 2884 | + | |
| 2885 | + | |
| 2886 | + | |
| 2887 | + | |
| 2888 | + | |
| 2889 | + | |
2869 | 2890 | | |
2870 | 2891 | | |
2871 | 2892 | | |
| |||
2921 | 2942 | | |
2922 | 2943 | | |
2923 | 2944 | | |
2924 | | - | |
| 2945 | + | |
2925 | 2946 | | |
2926 | 2947 | | |
2927 | 2948 | | |
| |||
3022 | 3043 | | |
3023 | 3044 | | |
3024 | 3045 | | |
3025 | | - | |
| 3046 | + | |
3026 | 3047 | | |
3027 | 3048 | | |
3028 | 3049 | | |
| |||
3325 | 3346 | | |
3326 | 3347 | | |
3327 | 3348 | | |
3328 | | - | |
| 3349 | + | |
3329 | 3350 | | |
3330 | 3351 | | |
3331 | 3352 | | |
| |||
| 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 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
138 | | - | |
| 138 | + | |
139 | 139 | | |
140 | 140 | | |
141 | | - | |
| 141 | + | |
| 142 | + | |
142 | 143 | | |
143 | 144 | | |
144 | 145 | | |
| |||
150 | 151 | | |
151 | 152 | | |
152 | 153 | | |
153 | | - | |
| 154 | + | |
154 | 155 | | |
155 | 156 | | |
156 | 157 | | |
| |||
0 commit comments