Repository navigation
Commit 121d1a5
fix(console): a record save whose answer carries warnings shows one toast, the record-named save message carrying the warning lines (#28114)
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDFH4S5pR1C8YvYCWWsZfXx)_
Fixes #27999
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:记录表单(弹窗、子表任务、整页表单)保存时,若答复带有 `severity: 'warning'`
校验规则的提示,现在只弹一条提示:带记录名的保存提示(如 `Task "Quarterly review"
updated`),警告级样式,每条规则提示单独一行。
- **为什么改**:之前同一次保存弹两条:写入警告通道先弹「Saved — with
warnings」,表单随后再弹带记录名的保存成功提示,用户看到两条说法不同的消息。
- **风险与回滚**:只动 `@object-ui/app-shell`
三个文件。表单打开期间,写入警告通道把「这张表单的那次写入」的答复警告交给表单,不再自己弹;字段被剥离的提示照旧单独弹;其它写入照旧弹「Saved
— with warnings」;表单关闭时把没认领的警告全部照常弹出,不丢。回滚 = revert 本 PR。
- **先看**:`ui/packages/app-shell/src/providers/writeWarningToast.ts` 的
`emitWriteWarning` 与 `claimAnswerWarnings`(风险点:认领判定必须在第一个 `await`
之前同步完成);`console/AppContent.tsx` 的 `answerWarningClaim` effect 与
`handleRecordFormSuccess`;`views/RecordFormPage.tsx` 的 `handleSuccess`。
- **两分钟验证**:在 `ui/` 下跑 `pnpm exec vitest run
packages/app-shell/src/views/RecordFormPage.saveToastCarriesWarnings-27999.test.tsx
packages/app-shell/src/console/__tests__/AppContent.recordSaveOneToast-27999.test.tsx`,预期
2 个文件、10 个用例全部通过。
- **需要你做的**:无,席位落地。
## What changes
- **The write-warning channel** (`providers/writeWarningToast.ts`). New
`claimAnswerWarnings(adapter, scope)`: a record save surface claims the
answer warnings of the writes it makes, for as long as it is open. The
scope is the record's update (object + id), or a create of the object.
`emitWriteWarning` now decides, before its first `await`, whether an
open claim on the adapter it is handed matches the event; if so the
event's `warnings` are parked on that claim instead of toasted. The
stripped-fields toast is never claimed and is unchanged. `take(saved)`
returns the distinct messages of the write that answered `saved` (an
update's parked events already name its record; a create's are its own
when the answer's record id is the saved record's id) and shows every
other parked event on the channel's own toast at once. `release()` shows
whatever is still parked. No warning is dropped on either path.
`toastRecordSave(sink, message, warnings)` is the one save toast: the
warning variant with the lines as its description when there are
warnings, the success toast otherwise.
- **Overlay form** (`console/AppContent.tsx`). While `?form=` is open,
an effect holds a claim keyed on the URL primitives (`recordFormParam`,
the form object's name), not on the memoised `editingRecord` /
`formObjectDef` (ui/AGENTS.md #10). `handleRecordFormSuccess` takes its
write's warnings first; the child-form task passes them to
`toastRecordSave`, the main path hands them to `crud_success` as
`params.warnings`, and the `crud_success` handler toasts through
`toastRecordSave`. With no `warnings` param it is the success toast it
was.
- **Page-mode form** (`views/RecordFormPage.tsx`). Same claim for the
page's mode, object and record id; `handleSuccess` takes the warnings
and toasts through `toastRecordSave`. The record naming of #26172 /
#26461 is unchanged.
- **Why this route and not the others.** The adapter emits the
write-warning event inside `create` / `update`, before the write's
promise settles, and the form calls `onSuccess` only after that promise
settles. So a claim opened while the form is open is already holding the
warnings when the surface's success handler runs. That order is fixed by
the code, not by timing, and it does not depend on how long the
stripped-fields label lookup takes. The other routes were weaker.
Deferring the channel's toast and letting the surface cancel it is a
race against the form's own awaits. A per-write flag on the request, or
returning the answer's `warnings` to `onSuccess`, would change
`@object-ui/data-objectstack`'s published write API and every
`plugin-form` variant: a wider surface and a Clause-② change. The claim
stays inside the private `@object-ui/app-shell` and moves no published
export or i18n key.
- **Changeset** `.changeset/27999-save-with-warnings-one-toast.md`,
`@object-ui/app-shell` + `@object-ui/console` patch.
## Tests
- `providers/writeWarningToast.test.ts`: 5 new cells through the real
adapter chain (the stubbed transport answers, `onWriteWarning` hands the
event on): a claimed update leaves the channel silent and `take` returns
the message; a claimed create takes only its own record id's warnings
and shows another create's on the channel toast; the stripped-fields
toast keeps firing while the warnings are claimed; CONTROL: writes the
claim does not name keep the channel toast; a claim closed without a
take shows what it parked and stops claiming.
- `views/RecordFormPage.saveToastCarriesWarnings-27999.test.tsx` (new, 5
cells) and
`console/__tests__/AppContent.recordSaveOneToast-27999.test.tsx` (new, 5
cells, from the #26172 harness, with the runner running the real
`crud_success` handler). Each replays the real order: the channel's
`emitWriteWarning` is called with the adapter, NOT awaited, and then
`onSuccess(saved)` runs. The channel's sink and the surface's toast are
the same sonner `toast`, so every toast the user would see is counted:
one toast, the record-named message carrying the warning lines (update,
create, child-form task); the stripped-fields notice still toasts beside
it; CONTROL: a write the surface did not make keeps "Saved — with
warnings" and the save toast stays a success; a save without warnings is
the success toast.
- Existing pins kept unchanged and green:
`AppContent.recordSaveToastNamesRecord-26172`,
`RecordFormPage.saveToastNamesRecord-26461`, the `ConsoleToaster.*`
files (including the `crud_success` auto-dismiss pin),
`AdapterProvider.*`, and the #12107 cells in
`writeWarningToast.test.ts`.
<details><summary>Test runs (head 2fcbdbc)</summary>
- Narrowed pin set, 61 files: every `RecordFormPage*` and `AppContent*`
test under `packages/app-shell` and `apps/console`, `AdapterProvider*`,
`ConsoleToaster*`, `writeWarningToast.test.ts`. `pnpm --dir ui exec
vitest run --maxWorkers=2 FILES` under `scripts/pm/os-verify-lock.sh`:
`VERDICT command-exit 0`, Test Files 61 passed (61), Tests 433 passed
(433).
- `pnpm --filter @object-ui/app-shell run type-check`: `VERDICT
command-exit 0`. It covers the new tests: on an earlier head, `tsc -p
tsconfig.test.json` went red with TS7031 on two callbacks in the new
overlay test, which were then typed.
- The whole `packages/app-shell` suite (1963 files) was not run locally.
At two workers it would have held the shared verification lock for over
an hour, so the run was stopped and narrowed to the set above. `Test UI`
in CI runs the full sharded set.
</details>
<details><summary>Ablation (head 2fcbdbc)</summary>
`node scripts/ablation-replace.mjs` on `providers/writeWarningToast.ts`:
the anchor `const claim = messages.length > 0 && adapter ?
matchingClaim(adapter, ev) : undefined;` was replaced so that the claim
lookup never runs (the channel ignores every claim). The anchor went x1
to x0, the replacement x0 to x1, and the blob went `af23c268` to
`95733a37`.
Direction, written before the run: 11 RED (4 channel cells, 3 page-mode
cells, 4 overlay cells); every CONTROL cell and the earlier #12107 cells
GREEN.
Observed: Tests 11 failed, 24 passed (35), exactly those 11. Restored
blob `af23c268` = HEAD blob, `git diff HEAD` empty.
</details>
## Acceptance notes
- The batch path (`batchTransaction`, used by master-detail record
forms) carries no answer `warnings` into the write-warning event today.
A master-detail save therefore has no warnings for either toast to show.
This PR neither widens nor narrows that.
- While a create form is open, a create of the same object made
elsewhere in the same tab is parked on the claim, then shown on its own
channel toast when the form saves or closes. Its warning arrives late
but is not lost. The modal blocks the page behind it, so no ordinary
flow reaches this case.
- The save toast with warnings uses sonner's warning variant, so the
advisory is visible at a glance. Its title is still the record-named
save message.
## Verification
<details><summary>Gates (head 2fcbdbc)</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 34 commands; each is recorded as
command :: exit code, captured before any pipe:
```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 --dir ui exec vitest run scripts/ :: 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:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm --dir ui run check:new-line-citations :: exit 0
node scripts/check-platform-checklist.mjs --changed-from origin/main :: exit 0 (0 anchored files in the change set)
```
`dispatch-gates --ran`: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
`node scripts/check-sdui-manifest.mjs` exit 0. The CI-owned families
(the lint farm, Test UI, Type Check UI) are read from this PR's check
runs, not re-run here.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbDAucNyQ4UtzCJKyh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 86c6955 commit 121d1a5
7 files changed
Lines changed: 800 additions & 17 deletions
File tree
- .changeset
- ui/packages/app-shell/src
- console
- __tests__
- providers
- views
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
764 | 765 | | |
765 | 766 | | |
766 | 767 | | |
767 | | - | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
768 | 771 | | |
769 | 772 | | |
770 | 773 | | |
| |||
901 | 904 | | |
902 | 905 | | |
903 | 906 | | |
904 | | - | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
905 | 928 | | |
906 | 929 | | |
907 | | - | |
| 930 | + | |
908 | 931 | | |
909 | 932 | | |
910 | 933 | | |
| |||
925 | 948 | | |
926 | 949 | | |
927 | 950 | | |
| 951 | + | |
928 | 952 | | |
929 | | - | |
| 953 | + | |
930 | 954 | | |
931 | 955 | | |
932 | 956 | | |
933 | | - | |
| 957 | + | |
934 | 958 | | |
935 | 959 | | |
936 | 960 | | |
| |||
| 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 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 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 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
0 commit comments