Skip to content

fix(console): a record save whose answer carries warnings shows one toast, the record-named save message carrying the warning lines - #28114

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-j9w213
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-j9w213

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

Requested by Bill · project thread
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 chore: version packages #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 Record create/update success toasts name the record by its title, not only the object (ruling C on #25849) #26172 / app-shell: the full-page record form (/new, /edit route) still toasts "Task updated successfully" — #26172's fix covered AppContent's dialog paths only; RecordFormPage passes the object label alone #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 Record create/update success toasts name the record by its title, not only the object (ruling C on #25849) #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 resolveCheckToFiles matches only .mjs|.cjs|.js|.sh, so all 23 TypeScript-authored gates resolve to ZERO gate files and contribute ZERO watch hints #12107 cells in writeWarningToast.test.ts.
Test runs (head 2fcbdbc)
  • 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.
Ablation (head 2fcbdbc)

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.

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

Gates (head 2fcbdbc)

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:

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.


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

席位核对

ACCEPT · head 2fcbdbcfc8531d4cc6806ef2bb2516ab8f0c3e0e · 2026-10-30T13:26Z

  • Shape: draft → main, Fixes #27999. Clause-②: no and Metadata-protocol: none.
    • claimAnswerWarnings and toastRecordSave are exported from the private app-shell module only.
    • There is no new i18n key, and the published data-objectstack write API is unchanged.
    • The changeset is patch on @object-ui/app-shell and @object-ui/console.
  • Serial: the blocker perf(console): opening a record re-reads no list, asks its field access once, and the bell polls once a minute #27907 merged at 2026-10-30T11:04Z. The one merge of origin/main resolved an import conflict with fix(app-shell): a record form opened on the app-name address stays in the app after save #28066 in RecordFormPage.tsx by keeping both sides, and everything was re-run on the merged head.
  • Change, read (7 files, +800/−17):
    • While a record form is open, a claim on the adapter names that form's write: this record's update, or a create of this object. The overlay holds it in AppContent (keyed on the URL primitives), and the full-page form in RecordFormPage.
    • emitWriteWarning matches the claim synchronously, before its first await. The adapter emits inside the write, before the write's promise settles, and onSuccess runs after it. So the warnings are parked by the time the success handler takes them, with no race.
    • The success handler takes the warnings. The single toast becomes the record-named message on the warning variant, with one rule message per line.
    • Nothing is dropped:
      • a create's parked events from a different saved id are shown on the channel toast at take;
      • anything still parked shows on release;
      • stripped-field notices keep their own toast;
      • a write no claim names keeps "Saved — with warnings".
    • Edge, accepted: an inline edit of the same record while its overlay is open has its warnings shown at take or release, not at once. They are delayed, never lost.
  • Evidence (report 6523659294):
    • On the head: a 61-file pin set with 433 tests passes, and the app-shell type-check, which compiles the new tests, exits 0.
    • Ablation: the claim lookup was removed, and 11 of 35 tests went red, exactly the predicted cells. The blob was restored to HEAD.
    • Derived gates: 34/34 exit 0, with --ran reconciled. check:new-line-citations: 0 new.
  • Deviations, accepted: the full app-shell local run was stopped and left to CI's sharded Test UI, which is green. The other deviations are report and PR-body format points.
  • Out of scope, noted: the master-detail batch path carries no answer warnings into the event. That is an inference, not reproduced, and it is in the PR's Acceptance notes.
  • Gates (check-runs on the head): 44 success, 11 skipped, 0 failure. mergeable_state is clean. NOT governed, 817 changed lines.
  • Landing: non-governed and all green, so the seat lands it with pr_ready and automerge_enable.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 13:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 121d1a5 Oct 30, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-j9w213 branch October 30, 2026 13:44
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.

Console: a save that passes a warning-severity rule shows one toast, the record-named save toast carrying the warning

1 participant