Skip to content

fix(plugin-form): a drawer or modal object-form save that answers warnings shows one toast, the record-named message carrying them - #28267

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-y1bgnu
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-y1bgnu

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

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" 已更新」,提醒逐行附在里面;不再另外弹一条「已保存,有提醒」。和记录页、控制台记录弹窗的表现一致。
  • 为什么改:fix(ui): object-form drawer and modal saves toast the record-named save message #28204 给抽屉/弹窗补上了保存提示,但提醒仍走写入提醒通道自己的提示,一次保存叠出两条,用户关掉成功提示后容易漏看那条提醒属于刚保存的记录(Drawer and modal form saves: answer warnings show as a second toast instead of riding the success toast #28216,fix(ui): object-form drawer and modal saves toast the record-named save message #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

Local runs

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.
Derived gates

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

Generated by Claude Code

…hat answers warnings

Move the answer-warning claim and the record save toast from app-shell's
write-warning channel into @object-ui/react, so the object-form drawer and
modal container can claim its own write's warnings like the record page
and the record dialogs do, with one claim registry for every surface.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvQRyTQaTC3jT6F19i
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 0bd8234ee3910be73bd763538e16bdadcad94760 · 2026-10-30T22:15Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 0bd8234ee3910be73bd763538e16bdadcad94760
Local-runs: none

Inputs: card #28216 (body, triage 6530250373 and 6533339668, claim 6533291658, os-dev-report 6534234747, claim amendment 6534268411), the S1 should-fix of PASS record 6528599404 on #28204 that filed it, the PR body, the 10-file diff against merge base f40b777 (main has moved 100 files since; none is in this diff, and GitHub reports the head mergeable, clean), the head's check-runs (read 2026-10-30T22:18Z: check-runs on 0bd8234: 60 total, 45 success (Auto Label, Build & E2E UI, Build UI Deps, Check Changeset, Check PR Size, Dogfood Regression Gate, Governed Surface Queue Guard, Lint & Repo Gates, Lint UI, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Test Core, Test UI, Test UI (all 16), Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 15 skipped path-filtered (18.x upgrade harness (opt-in), Auto Label, Build Core, Build Docs, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Plan Test Core shards, Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test Core (matrix shard/8), UI Published Artifacts); 0 failure, 0 in progress). Author user.login is objectstack-fleet[bot]; the drafter is session_01L9qibvQRyTQaTC3jT6F19i on claude/project-thread-y1bgnu; the reviewer is not the drafter.

① Derived judgments

  • The declaration face only: no packages/spec/**, no governed path, no server change. Clause-②: yes (widening) and Metadata-protocol: none on the body, the changeset and the claim amendment: consistent with the diff, which adds exports to one ui package and no spec member, key or route.
  • The registry moved, not rewritten: record-save-feedback.ts carries claimAnswerWarnings, matchingClaim, sameId, the take and release bodies, answerWarningLines and toastRecordSave line for line from the deleted half of writeWarningToast.ts. The two differences are typing only: the event is the structural AnswerWarningsEvent (operation, resource, id, warnings with message), which WriteWarningEvent satisfies, and RecordSaveSink spells its own success and warning members instead of extending the channel's sink. @object-ui/react imports nothing new; plugin-form and app-shell already depend on it, so no dependency edge is added.
  • The channel keeps its contract: emitWriteWarning still decides the park synchronously before its first await, through the new parkAnswerWarnings door, and toasts the answer warnings itself only when no claim took them; the stripped-fields toast is untouched. RecordFormPage and AppContent change one import line each, and writeWarningToast.test.ts only re-points its import. No importer of the moved helpers remains outside the diff, and @object-ui/app-shell's index never exported them, so nothing is removed from a public surface.
  • The container's claim follows the record page's shape: opened in an effect keyed on primitives and the adapter while the container is open (schema.open !== false, the same rule DrawerForm applies) and writes itself (adapter present, no submitHandler, create of its object or update of its recordId), released on close or unmount so a parked write it did not make gets the channel's toast, and take(saved) runs before the label lookup awaits. The inline object-form arm shares the hook, so it gets the same one toast; that is consistent with fix(ui): object-form drawer and modal saves toast the record-named save message #28204's toast already covering it.
  • Tests match the claims: the new plugin-form file drives the real drawer and modal submit path with parkAnswerWarnings called inside the adapter write (edit, create, declared successMessage, and CONTROL rows for another record, a submitHandler, a closed container, and release on unmount); the react file pins the door and the variant choice at the seam. The ablation (claim effect returning early) reddens the eight one-toast rows and leaves the controls green, as the body predicts.

② Semver level

@object-ui/react, @object-ui/plugin-form, @object-ui/app-shell: all patch. New exports from a package that removes nothing are an additive widening in this repo's own precedent (#28151, #28070, #28261: additive export = patch); no accept set moves and no metadata protocol member is added, so no package needs minor. The three carriers agree: PR body, claim amendment 6534268411 and the changeset.

③ Boundary flags

  • Serial note: none of the 100 files main moved since f40b777 is in this diff; the head is mergeable as read. A later land that touches writeWarningToast.ts or ObjectFormContainer.tsx would call for a delta re-read.
  • Should-fix, not blocking: two open claims on the same scope (a page drawer over the Console's own dialog for the same object) resolve to whichever claimed first, the registry's existing rule; the body names it. If a real double-claim shows up, the matching rule could prefer the most recent claim, in @object-ui/react where it now lives.
  • Should-fix, not blocking: release() is called from the effect cleanup, so a container that re-keys (for example recordId changing while open) releases and re-claims; a park landing in that gap would reach the channel's toast. Not reachable from the drawer or modal submit path, since the record id does not change during a save.

Implemented-by: claude/project-thread-y1bgnu
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-tesla · session_01L9qibzC6RxMfB59EsgAoNN

席位核对

ACCEPT · seat domain:ui#1 · head 0bd8234e · 2026-10-30T22:19Z

席位意见:页面里的抽屉或弹窗表单(object-form 块)保存成功、但服务端答复带警告时,现在只弹一个提示:点名记录的保存提示,警告附在其中,与记录页和控制台的记录对话框一致,不再另弹第二个警告提示。实现只有一份:这套认领逻辑从 app-shell 原样移到 @object-ui/react,三处共用。已按扩面申报 Clause-②: yes (widening)。

  1. Shape: Fixes #28216, Refs #27453, draft, base main, 10 files, +508/−131. ui/packages/react/src/record-save-feedback.ts holds claimAnswerWarnings, toastRecordSave and the claim registry, moved unchanged, plus parkAnswerWarnings and answerWarningLines, and is exported from the package index. app-shell's writeWarningToast.ts keeps the channel and parks through it. RecordFormPage and AppContent only re-point imports. ObjectFormContainer claims its own write while it is open. The test files are containerSaveToastCarriesWarnings-28216.test.tsx and record-save-feedback.test.ts, with a root changeset.
  2. Seat read: nothing narrows. On origin/main the moved helpers were never re-exported from @object-ui/app-shell, and nothing outside app-shell/src imported them. The new edge plugin-form → react already existed, so the dependency graph gains no edge. The container claims nothing under a submitHandler, and an edit with no recordId claims nothing either. It takes the claim before the label lookup awaits, and on unmount it releases the claim and shows anything still parked. The Console: a save that passes a warning-severity rule shows one toast, the record-named save toast carrying the warning #27999 tests are unchanged and green.
  3. Contract review: PASS at this head, record 6534687957 (TIER_DEFAULT), by the contract-review lane, not the drafter. Its two should-fixes are not blocking and stay as written. (a) Two open claims on one scope resolve to the first claimant. That is the registry's existing rule, and changing it would move the record page's and dialogs' behaviour, which this order holds exact. (b) A re-key while open can't happen on the drawer or modal save path, because the record id doesn't change during a save. If a real double claim ever shows up, (a) gets its own card.
  4. Gates / check-runs: on 0bd8234e, 60 runs: 45 success, 15 skipped, 0 failure. mergeable_state is clean. In the dev's local run, 34/34 derived gates exited 0, type-check of react, plugin-form and app-shell exited 0, and the locked run of 474 files passed 4903 tests. Ablating the container claim turns 8 one-toast rows red.
  5. Landing: check-governed-merges --pr 28267 reports NOT governed, 639 changed lines ≤ 3000. The seat readies the PR and arms auto-merge on the maintainer's 「按 pm-dispatch 审过的pr你可以自主落地」 (seat thread, 2026-10-30T14:19Z).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 22:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit ba5c754 Oct 30, 2026
64 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-y1bgnu branch October 30, 2026 22:36
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drawer and modal form saves: answer warnings show as a second toast instead of riding the success toast

1 participant