You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ba5c754
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-form): a drawer or modal object-form save that answers warnings shows one toast, the record-named message carrying them (#28267)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoLk3JcUupC4oLjMFnZ2TS1Y)_Fixes#28216
Refs #27453
Clause-②: yes (widening: `@object-ui/react` gains the exports
`claimAnswerWarnings`, `parkAnswerWarnings`, `answerWarningLines`,
`toastRecordSave` and the types `AnswerWarningsEvent`,
`AnswerWarningScope`, `AnswerWarningClaim`, `RecordSaveSink`; they were
internal to `@object-ui/app-shell`, which exported none of them, so
nothing is removed)
Metadata-protocol: none
## 维护者速读
- 改了什么:页面上 `object-form`
抽屉或弹窗保存记录时,如果服务端带回提醒(例如一条警告级的校验规则),现在只弹一个提示:写着记录名的「任务 "Fix login"
已更新」,提醒逐行附在里面;不再另外弹一条「已保存,有提醒」。和记录页、控制台记录弹窗的表现一致。
- 为什么改:#28204
给抽屉/弹窗补上了保存提示,但提醒仍走写入提醒通道自己的提示,一次保存叠出两条,用户关掉成功提示后容易漏看那条提醒属于刚保存的记录(#28216,#28204
合同复审 S1)。
- 风险与回滚:认领逻辑从 app-shell 原样搬到 `@object-ui/react`,只有一份实现;记录页和记录弹窗只改了
import,它们原有测试一条未改、全绿。容器只在打开时认领自己那次写入,用 `submitHandler` 的容器不认领。回滚即 revert
本 PR。
- 先看:`ui/packages/react/src/record-save-feedback.ts`(搬下来的认领注册表与新的
`parkAnswerWarnings`);`ui/packages/plugin-form/src/ObjectFormContainer.tsx`(`useContainerSaveFeedback`
里的认领与
`toastRecordSave`);`ui/packages/app-shell/src/providers/writeWarningToast.ts`(`emitWriteWarning`
改走 `parkAnswerWarnings`)。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/plugin-form/src/__tests__/containerSaveToastCarriesWarnings-28216.test.tsx
packages/react/src/record-save-feedback.test.ts`,预期 2 个文件、17 个测试全绿。
- 需要你做的:无,席位落地。
## What changes
**One home for the claim**
(`ui/packages/react/src/record-save-feedback.ts`). The claim registry
the record page and the record dialogs use (#27999) lived in
`@object-ui/app-shell`'s write-warning channel, which
`@object-ui/plugin-form` cannot import (app-shell depends on
plugin-form). It moves, unchanged in behaviour, to `@object-ui/react`,
which both packages already depend on, beside the data-invalidation bus,
the other module-level seam between writers and readers. Moved as they
were: `claimAnswerWarnings`, the matching rule, `answerWarningLines`,
`toastRecordSave` and its sink type. New: `parkAnswerWarnings(adapter,
ev, show)`, the channel's side of the same registry, which parks an
event's warnings on the claim that names it and answers whether one did.
The event type is structural (`AnswerWarningsEvent`) because
`@object-ui/react` sits below `@object-ui/data-objectstack`;
`WriteWarningEvent` satisfies it.
Why this home and not the other options the order lists: an injected
seam (the container accepting a claim from its host) would keep the
registry in app-shell but leave a page block outside the Console with
two toasts, and needs a context type in a lower package anyway;
`@object-ui/i18n` (next to `recordSaveMessage`) would put a
write-coordination registry in a translation package; `@object-ui/core`
holds no React-facing runtime seams. `@object-ui/react` is the lowest
package all three save surfaces and the channel share with a precedent
of this shape. No dependency edge is added: `react` depends on `core`,
`i18n`, `types` and `@objectstack/spec` as before.
**The channel**
(`ui/packages/app-shell/src/providers/writeWarningToast.ts`).
`emitWriteWarning` parks through `parkAnswerWarnings`, still
synchronously before its first `await`, and toasts the warnings itself
only when no claim took them. The stripped-fields toast is never
claimed, as before. Its claim half is deleted, not re-exported, so there
is one spelling.
**The callers** (`RecordFormPage.tsx`, `AppContent.tsx`). Imports
re-pointed to `@object-ui/react`; nothing else changes. Their #27999
tests are untouched and green.
**The container**
(`ui/packages/plugin-form/src/ObjectFormContainer.tsx`).
`useContainerSaveFeedback` opens a claim while the container is open
(`schema.open !== false`) and makes the write itself (an adapter, no
`submitHandler`, `create` or `edit` with a `recordId`): a create of its
object, or an update of its record. On success it takes its write's
warnings first, before the label lookup awaits, and raises
`toastRecordSave`: the record-named message (or a declared
`successMessage`) on the warning variant with the warnings one per line,
the success variant when there are none. Closing or unmounting releases
the claim, and anything it still parks (a write it did not make) gets
the channel's toast. The effect is keyed on primitives and the adapter,
as `AppContent`'s is (ui/AGENTS.md #10).
## Tests
New:
-
`ui/packages/plugin-form/src/__tests__/containerSaveToastCarriesWarnings-28216.test.tsx`
(14, drawer and modal each): edit and create show one warning-variant
toast naming the record with the warning, and the channel shows nothing;
a declared `successMessage` carries them too; CONTROL: warnings about
another record keep the channel toast and the save toast stays the plain
success one; CONTROL: a `submitHandler` container claims nothing;
CONTROL: a closed container claims nothing; closing the container shows
what its claim still parks. The channel is stood in for by
`parkAnswerWarnings`, called inside the write as the adapter emits its
event (plugin-form cannot import app-shell).
- `ui/packages/react/src/record-save-feedback.test.ts` (3): park on a
matching claim and take distinct lines; CONTROL: no park for another
adapter, record or object, or after release; `toastRecordSave` picks the
variant.
Updated: `writeWarningToast.test.ts` imports `claimAnswerWarnings` from
`@object-ui/react`; its assertions are unchanged.
**Ablation** (through `scripts/ablation-replace.mjs`, mutation proved on
disk, restored to the HEAD blob): the container's claim effect returning
before it claims → the new file 8 failed / 6 passed (the four one-toast
rows per form type red, the controls green, as predicted before the
run); restored, blob `4f849d2d` = HEAD.
## Acceptance notes
- Behaviour of the record page and the record dialogs is unchanged: same
registry, same matching rule, same toast; their files differ only in an
import line.
- The container claims only while open, so a drawer mounted closed on a
page does not hold the warnings of a create of the same object made
elsewhere. While two surfaces claim the same scope at once (a page
drawer open over the Console's own dialog for the same object), the
earlier claim wins, which is the registry's existing rule, not new here.
- Not measured live in a browser; the chain is pinned through the real
`DrawerForm` / `ModalForm` submit path in jsdom.
## Verification
<details><summary>Local runs</summary>
Head `0bd8234e`, on `main` `f40b777d`. `main` has moved 6 commits since;
none touches a file in this diff and `git merge-tree` with `origin/main`
is clean.
- `pnpm turbo run build --filter='@object-ui/app-shell^...'
--concurrency=2` (the dependency closure, under the verify lock) →
`Tasks: 64 successful, 64 total`, VERDICT command-exit 0.
- `type-check` of `@object-ui/react`, `@object-ui/plugin-form`,
`@object-ui/app-shell` (`tsc --noEmit` and `tsc -p tsconfig.test.json`)
→ exit 0 each.
- Under the verify lock: `pnpm --dir ui exec vitest run packages/react/
packages/plugin-form/` plus the 129 app-shell and console test files
that name `writeWarningToast`, `RecordFormPage`, `AppContent`,
`toastRecordSave`, `claimAnswerWarnings` or `ObjectFormContainer` →
`Test Files 474 passed (474)`, `Tests 4903 passed | 1 skipped (4904)`,
VERDICT command-exit 0.
- The six toast files (the two new ones,
`containerSaveToastNamesRecord-27453`, `writeWarningToast`,
`RecordFormPage.saveToastCarriesWarnings-27999`,
`AppContent.recordSaveOneToast-27999`) → 6 files, 65 tests passed.
- eslint on the 9 changed `ts` / `tsx` files under `ui/` → 0 errors.
`pnpm --dir ui run check:new-line-citations` → 0 new citations, exit 0.
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `0bd8234e` → 34 commands, each run with
stdin from /dev/null and its exit code captured before any pipe; `--ran`
reconcile: `34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN`, exit 0.
```
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: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 exec vitest run scripts/ :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibvQRyTQaTC3jT6F19i)_
Co-authored-by: Claude <noreply@anthropic.com>
fix(plugin-form): a drawer or modal `object-form` save that answers warnings shows one toast, the record-named message carrying them
8
+
9
+
Clause-②: yes (widening)
10
+
Metadata-protocol: none
11
+
12
+
A save from an `object-form` block with `formType: 'drawer'` or `'modal'` that succeeded with advisory warnings (a warning-severity validation rule, for example) showed two toasts: the record-named success one and the write-warning channel's own "Saved — with warnings". The record page and the Console's record dialogs already showed one. The container now does the same: while it is open it claims the answer warnings of the write it makes (its record's update, or a create of its object), and its save toast carries them on the warning variant, one per line, whether the message is the record-named one or a declared `successMessage`. A write the container did not make keeps the channel's toast, and so does a container with a `submitHandler`, which writes nothing itself.
13
+
14
+
-**`@object-ui/react`**: new exports `claimAnswerWarnings`, `parkAnswerWarnings`, `answerWarningLines`, `toastRecordSave` and the types `AnswerWarningsEvent`, `AnswerWarningScope`, `AnswerWarningClaim` and `RecordSaveSink`. They are the one claim registry every record save surface and the write-warning channel share, moved here from `@object-ui/app-shell`, where they were internal, so `@object-ui/plugin-form` can reach them.
15
+
-**`@object-ui/app-shell`**: the write-warning channel, the record page and the record dialogs read the claim from `@object-ui/react`. Their behaviour is unchanged.
0 commit comments