Repository navigation
fix(core): a successful operation: 'update' action refreshes the record it wrote - #29970
Merged
Merged
Conversation
The spec materializes `refreshAfter: false` on an action authored without it, so the server-action dispatch's `refreshAfter !== false` refresh never fired for a declared update: the route wrote the record and the page kept showing the old values. A declared update's success is now a record change whatever `refreshAfter` says; every other dispatch keeps reading the flag. Claude-Session: https://claude.ai/code/session_01WGufziMa1x7KKmd4N1nPLV Co-authored-by: Claude <noreply@anthropic.com>
… write The tutorial's served action (operation: 'update', refreshAfter: false materialized) on the full page and in the drawer: after the 200 the page re-reads, the path bar reads Resolved, the Resolve button is gone, and a list reader of the object drops the record from its Open filter. CONTROL: a non-update script action served with refreshAfter: false sends no re-read. Adds the patch changeset for @object-ui/core. Claude-Session: https://claude.ai/code/session_01WGufziMa1x7KKmd4N1nPLV Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziMa1x7KKmd4N1nPLV Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd 维护者速读(席位终稿)
席位核对 · ACCEPT · head
|
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Nov 3, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Nov 3, 2026
9 tasks done
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Nov 3, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Nov 3, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Yibo Wang · project thread
Fixes #29935
Clause-②: no
Metadata-protocol: none
维护者速读
operation: 'update',比如教程里的「Resolve」)在服务器返回成功后,Console 现在会重新读取这条记录:记录页(整页和抽屉)显示写入后的值,visible条件变为假的按钮随之消失,来源列表里不再符合筛选条件的记录也会离开列表。作者不需要再额外写refreshAfter: true。refreshAfter填成false,Console 拿到的正是这个解析后的形状;服务端动作分派原本只在refreshAfter !== false时刷新,所以声明式更新从不刷新。服务器已经写成「已解决」、提示也说成功了,页面却还显示「Open」和「Resolve」按钮,用户会以为失败而重复点击。dataSource.update已有的行为一致);其他动作仍按refreshAfter决定是否刷新,spec 默认值没有动。回滚即还原本 PR。ui/packages/core/src/actions/serverActionHandler.ts(shouldRefresh一行及其注释)、ui/packages/app-shell/src/views/RecordDetailView.updateActionRefresh-29935.test.tsx(整页、抽屉、列表、对照组)。pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.updateActionRefresh-29935.test.tsx packages/core/src/actions/__tests__/serverActionHandler.test.ts,预期 33 个测试全部通过。What changes
Which path the tutorial action takes, measured. An
operation: 'update'action never reaches the record page'sapihandler (the tworefreshAfter === truegates inRecordDetailView.tsxthat the card names). The spec pinstype: 'script'on it, andActionRunner.dispatchCollectedreadsoperationbeforetype. So it goes toexecuteUpdateOperation, which hands it to the registeredscriptdispatch. On the record page that dispatch iscreateConsoleServerActionHandler, which wraps core'screateServerActionHandler. That handler refreshed onaction.refreshAfter !== false.Why the two paths look different but behave the same.
ActionSchema.safeParseon the card's action, run againstpackages/spec/srcat base40fa4335, gives{"type":"script","operation":"update","refreshAfter":false}. The spec's.default(false)fills in the key, and the Console is served that parsed shape. With the key present,!== falsebehaves exactly like=== true. The two spellings differ only for a row that reaches the client unparsed. That is why the network capture shows the 200 and no re-read.The fix. One line in
createServerActionHandler:A successful declared update now calls
onRefresh. On the record page,onRefreshisnotifyRecordChanged, which sendsnotifyDataChanged({ objectName, recordId }). The console list runtime (useConsoleActionRuntime) passes its ownonRefreshto the same factory, so a declared update run from a list row refreshes there too.Re-read, not apply. The route does answer with the written record (
{ ok, action, objectName, recordId, result }). The page does not apply it. The record page reads its record with$expandon every readable relation, and the server strips fields the reader may not read. The route'sresultis the engine's update answer, which has neither shape. Also, the invalidation bus carries scopes, not rows. So the page re-reads through its own read path.The list mechanism. A record-scoped change matches every reader that subscribes with
useDataInvalidation(objectName)and no record id (dataChangeMatches: "a hook WITHOUT recordId is a list/aggregate reader").ListViewandObjectViewsubscribe that way and refetch with their own filter, so the record leaves a list it no longer matches. The drawer is the sameRecordDetailView, mountedembeddedbeside its list. One event refreshes both.What
refreshAfter: falsestill governs. It still decides the refresh for:script) action that is not a declared update,apihandler (both=== truegates are unchanged),newTabUrlfast path increateConsoleServerActionHandler,result.reload.The spec default is untouched. Nothing under
packages/spec/**changes, and no lint rule is added.Docs (ui
AGENTS.md#2). Two one-line edits:onRefreshcomment inui/packages/core/README.mdnow says it also runs after everyoperation: 'update';content/docs/ui/actions.mdxnow says a declared update re-reads on its own.Tests
ui/packages/app-shell/src/views/RecordDetailView.updateActionRefresh-29935.test.tsx. It mounts the real record page, header, runner and console server-action handler, and only the authenticated fetch is faked. The action is the card's action as the Console is served it, withtype: 'script'andrefreshAfter: falsefilled in.findOneruns again, the current stage is "Resolved, current stage", and Resolve is gone from the header toolbar.embedded, mounted next to a list reader that holds the Open filter onuseDataInvalidation(objectName)). The same click re-reads the record and removes the Resolve button. The Open list goes from[tkt_1, tkt_2]to[tkt_2].scriptaction served withrefreshAfter: falsegets its 200 and its toast, andfindOneis not called again.ui/packages/core/src/actions/__tests__/serverActionHandler.test.tsadds two tests:refreshAfter: falsecallsonRefreshonce and returnsreload: true;onRefresh.Acceptance notes
RecordDetailView.tsx. That file is not touched, because the tutorial action never reaches the gates there (measured above). The diff stays on the action-success path, in core's dispatch.skills/objectstack-ui/rules/actions.mdstill says "SetrefreshAfter: truewhenever the action mutates the current record". That is still harmless, but it is no longer needed for anoperation: 'update'action.skills/**is a Tier H surface, so I left it alone. carrier: the ui seat, if it wants a skills follow-up.refreshAfterkey still refreshes through!== false, while the parsed defaultfalsedoes not. This difference was there before this PR and this diff does not change it. The spec's.default(false)is the contract. carrier: none.Verification
All of it ran on head
61f02c31, in worktreeobjectstack-issue-29935, underscripts/pm/os-verify-lock.sh.pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' --filter @objectstack/formula build, VERDICT command-exit 0 (9m51s).pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.updateActionRefresh-29935.test.tsx packages/core/src/actions/__tests__/serverActionHandler.test.ts, VERDICT 0, 2 files and 33 tests passed.createServerActionHandler,consoleServerActionoruseConsoleActionRuntimeand does not mockconsoleServerAction, plus all ofpackages/core/src/actions/__tests__/and the new file. CI's Test UI runs the rest.cd ui && pnpm exec turbo run build type-check --concurrency=2 --filter=@object-ui/core --filter=@object-ui/app-shell, VERDICT 0, 68/68 tasks. Bothtype-checkscripts ran with theirtsconfig.test.jsonleg, so the test files are compiled too.node scripts/check-sdui-manifest.mjs: exit 0.Ablation (with
scripts/ablation-replace.mjs, committed head; vitest aliases@object-ui/coretosrc, so no dist rebuild):const shouldRefresh = action.refreshAfter !== false;. 3 tests went red: the core update pin, plus the full-page and drawer tests. The other 30 stayed green, including the CONTROL and the refused-update pin. On restore, the blob wasdc0a20f3== HEAD andgit diff HEADwas empty.=== truegate the card names (const shouldRefresh = action.refreshAfter === true;). The full-page and drawer tests went red, and the CONTROL stayed green. The restored blob wasdc0a20f3== HEAD.Derived gates: dispatch-gates --commands, 67 at 61f02c3, all exit 0; --ran: 67 derived, 67 run, 0 NOT-MEASURED
check:skill-examplesfirst exited 3 (PREREQUISITE NOT MET, no client dist). It went green afterpnpm --filter '@objectstack/client-react...' build, under the lock, VERDICT 0.pnpm --dir ui exec vitest run scripts/: 170 files passed, 2 skipped (4979 tests).Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses. (CI runs it)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufziMa1x7KKmd4N1nPLV