Repository navigation
fix(app-shell): an api action declaring opensInNewTab + newTabUrl opens the tab and sends no request - #26922
Merged
Conversation
…ns the tab on list surfaces
The console's registered `api` handler sent its request to `target` and
never read the pair, so the spec's zero-roundtrip new-tab path was dead for
`type: 'api'`. `withNewTabUrlFastPath` routes such an action to the
server-action handler's existing fast path (same record-id resolution,
pre-opened tab, `{recordId}` substitution); every other api action is
unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jSH8FbKjEztmGT1sbB
…newTabUrl tab The record page registers its own `api` handler, which also never read `opensInNewTab` + `newTabUrl`. It now goes through the same `withNewTabUrlFastPath` composition against the page's server-action handler. Tests drive the real header and more-menu clicks and the shared runtime's runner; a changeset records the patch. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSH8FbKjEztmGT1sbB
…ry-stubbed module Nineteen app-shell tests stub `utils/consoleServerAction` with a factory that names only `createConsoleServerActionHandler`, so a rule exported from there was undefined under each of them and the record page and the runtime hook threw on mount. The predicate and the api-handler composition now live in `utils/newTabUrlFastPath`, which those stubs do not touch. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsR2jSH8FbKjEztmGT1sbB
Contributor
Author
席位核对ACCEPT · seat
Landing: no contract review is owed (no published schema, no governed text, |
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 Steedos · project thread
Fixes #26747
Clause-②: no
Metadata-protocol: none
维护者速读
type: 'api'动作若声明了opensInNewTab+newTabUrl,现在点击后会像 spec 写的那样打开新标签页(地址为newTabUrl,{recordId}已替换),并且不再向target发请求。记录页头部按钮、头部 ⋯ 更多菜单、列表工具栏都生效。gapdata_item上实测:从记录页头部和更多菜单点击,没有新标签页,反而 POST 到了目标。原因是控制台为api类型注册的两个处理器(列表运行时一份、记录页一份)根本不读这对键;新标签快速通道只存在于script的服务端动作处理器里。opensInNewTab和非空newTabUrl的 api 动作改走既有快速通道,其余 api 动作行为不变;不动 spec、不动服务端。回滚即 revert 本 PR。utils/newTabUrlFastPath.ts(路由规则);RecordDetailView.tsx与useConsoleActionRuntime.tsx里新增的apiActionHandler。type: 'api'、opensInNewTab: true、newTabUrl: '/export/{recordId}'的头部动作,打开一条记录点击它,预期新标签页打开到/export/<记录id>,网络面板里没有发往target的请求。What changes
The spec documents
newTabUrlas the zero-roundtrip target of a new-tab action ("When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST"). The console implements that path in the server-action handler built bycreateConsoleServerActionHandler, which is registered fortype: 'script'. The two handlers the console registers fortype: 'api'(the shared runtime'sapiHandlerinuseConsoleActionRuntime, and the record page's ownapiHandlerinRecordDetailView) never read the pair: they always sent their request totarget. That is the link that failed. The runner (ActionRunner.dispatchCollected) hands anapiaction to the registered handler as designed, and the surfaces do route through it.utils/newTabUrlFastPath.tsholds the routing rule:takesNewTabUrlFastPath(action)(the fast path's own condition, now also used bycreateConsoleServerActionHandlerinstead of its inline check) andwithNewTabUrlFastPath(apiHandler, consoleHandler), which sends an action taking the fast path to the surface's console server-action handler and every other action to the api handler unchanged. The fast path's early row-count refusal is forwarded only for those actions.useConsoleActionRuntimeregisters the composed handler forapiand returns it asapiHandler, soConsoleShell's root provider gets it too.RecordDetailViewregisters it for its own provider, composed with the page's server-action handler, so the record id is this page's record.{recordId}substitution, the pre-opened tab and the record-id resolution are the existing fast path's.Why
Declared ⇒ enforced: an api action declaring the new-tab pair opened no tab and POSTed instead (QA run studio-qa-2026-10-27, id G8).
Tests
RecordDetailView.apiNewTabUrl-26747.test.tsxmounts the real record page and clicks through the real header and ⋯ menu. Header click and more-menu click:window.open('about:blank', '_blank')once, the tab'slocation.hrefis the origin plus/export/and the URL-encoded record id, and no request through either fetch reaches the target. CONTROL: withoutnewTabUrlthe click POSTs totarget.useConsoleActionRuntime.apiNewTabUrl-26747.test.tsxdrives a realActionRunnerwith the runtime's context and handlers. List toolbar with one row selected: the tab goes to the interpolated URL, nothing is sent. The returnedapiHandler(whatConsoleShellregisters) takes the same path. Two rows selected: refused before any tab opens. CONTROL: withoutnewTabUrlthe api action POSTs totarget.node scripts/ablation-replace.mjsreplaced? consoleHandler(action, context)with? apiHandler(action, context)innewTabUrlFastPath.ts(anchor 1 → 0, blob3ef0dcca→087c133d). The two files ran red: Tests 4 failed, 3 passed (7), exactly the four fast-path cases. Restored: blob equals HEAD,git diff HEADempty.Acceptance notes
utils/consoleServerActionwith a factory naming onlycreateConsoleServerActionHandler. A first cut that exported the rule from there made all nineteen throw on mount (131 tests); innewTabUrlFastPath.tsit is untouched by those stubs.newTabUrlis not addressed (open question for the maintainer). The spec callsnewTabUrl"A path template", and the fast path prefixes the backend origin (baseUrl() || window.location.origin). QA's repro used an absolutehttps://example.com/export/{recordId}, which this PR now routes to a tab, but at the origin followed by the absolute URL. Whether an absolute value should be refused at authoring or navigated verbatim is a spec decision, so it is left unchanged here.opensInNewTabon an api action WITHOUTnewTabUrl(pre-open, then follow a returnedredirectUrl) is not wired fortype: 'api'either; out of this card's scope, noted only.Verification
All readings at head
1bcc587b(base6090bb73), underscripts/pm/os-verify-lock.sh.Touched package: @object-ui/app-shell
turbo run build --filter='@object-ui/app-shell^...'plus formula, client, sdui-parser, lint: 63 tasks successful, VERDICT command-exit 0.pnpm --filter @object-ui/app-shell run type-check(covers test files): VERDICT command-exit 0.useConsoleActionRuntime,consoleServerAction,RecordDetailVieworConsoleShell, plus every app-shell test namingapiHandlerornewTabUrl(100 files): Test Files 100 passed (100), Tests 736 passed (736). The rest of app-shell is left to CI's Test UI shards.ui/eslint.config.json the five touched files: 0 errors, no warning on a changed source line (the new tests carry theno-explicit-anywarnings their sibling harnesses carry).pnpm --dir ui run check:new-line-citations: 0 new cross-file line-address citations.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 34 commands for the 7 changed paths; all 34 exit 0, each code captured before any pipe, among themcheck:nul-bytes,check:test-source-alias,check:cross-package-test-inputs,check:type-check-coverage,check:type-check-debt,check-sdui-manifest,check-empty-changesetandcheck-changeset-no-major.pnpm --dir ui exec vitest run scripts/: Test Files 178 passed, 2 skipped (180), Tests 5065 passed, 2 skipped.--ranreconciled: "34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED".node scripts/check-platform-checklist.mjs --changed-from origin/main: 0 anchored files in the change set.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.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_01QsR2jSH8FbKjEztmGT1sbB
Generated by Claude Code