Skip to content

fix(app-shell): an api action declaring opensInNewTab + newTabUrl opens the tab and sends no request - #26922

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-bol9ec
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-bol9ec

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #26747

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:type: 'api' 动作若声明了 opensInNewTab + newTabUrl,现在点击后会像 spec 写的那样打开新标签页(地址为 newTabUrl,{recordId} 已替换),并且不再向 target 发请求。记录页头部按钮、头部 ⋯ 更多菜单、列表工具栏都生效。
  • 为什么改:QA(G8)在 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 newTabUrl as 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 by createConsoleServerActionHandler, which is registered for type: 'script'. The two handlers the console registers for type: 'api' (the shared runtime's apiHandler in useConsoleActionRuntime, and the record page's own apiHandler in RecordDetailView) never read the pair: they always sent their request to target. That is the link that failed. The runner (ActionRunner.dispatchCollected) hands an api action to the registered handler as designed, and the surfaces do route through it.

  • New utils/newTabUrlFastPath.ts holds the routing rule: takesNewTabUrlFastPath(action) (the fast path's own condition, now also used by createConsoleServerActionHandler instead of its inline check) and withNewTabUrlFastPath(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.
  • useConsoleActionRuntime registers the composed handler for api and returns it as apiHandler, so ConsoleShell's root provider gets it too. RecordDetailView registers it for its own provider, composed with the page's server-action handler, so the record id is this page's record.
  • No second interpolator: the {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.tsx mounts 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's location.href is the origin plus /export/ and the URL-encoded record id, and no request through either fetch reaches the target. CONTROL: without newTabUrl the click POSTs to target.
  • useConsoleActionRuntime.apiNewTabUrl-26747.test.tsx drives a real ActionRunner with the runtime's context and handlers. List toolbar with one row selected: the tab goes to the interpolated URL, nothing is sent. The returned apiHandler (what ConsoleShell registers) takes the same path. Two rows selected: refused before any tab opens. CONTROL: without newTabUrl the api action POSTs to target.
  • Ablation (one-off, not kept): with the fix committed, node scripts/ablation-replace.mjs replaced ? consoleHandler(action, context) with ? apiHandler(action, context) in newTabUrlFastPath.ts (anchor 1 → 0, blob 3ef0dcca → 087c133d). The two files ran red: Tests 4 failed, 3 passed (7), exactly the four fast-path cases. Restored: blob equals HEAD, git diff HEAD empty.

Acceptance notes

  • The rule lives in its own module on purpose. Nineteen app-shell test files stub utils/consoleServerAction with a factory naming only createConsoleServerActionHandler. A first cut that exported the rule from there made all nineteen throw on mount (131 tests); in newTabUrlFastPath.ts it is untouched by those stubs.
  • Absolute newTabUrl is not addressed (open question for the maintainer). The spec calls newTabUrl "A path template", and the fast path prefixes the backend origin (baseUrl() || window.location.origin). QA's repro used an absolute https://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.
  • opensInNewTab on an api action WITHOUT newTabUrl (pre-open, then follow a returned redirectUrl) is not wired for type: 'api' either; out of this card's scope, noted only.

Verification

All readings at head 1bcc587b (base 6090bb73), under scripts/pm/os-verify-lock.sh.

Touched package: @object-ui/app-shell
  • Dependency closure: 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.
  • The two new test files: Test Files 2 passed (2), Tests 7 passed (7).
  • Narrowed app-shell run: every test file named for useConsoleActionRuntime, consoleServerAction, RecordDetailView or ConsoleShell, plus every app-shell test naming apiHandler or newTabUrl (100 files): Test Files 100 passed (100), Tests 736 passed (736). The rest of app-shell is left to CI's Test UI shards.
  • ESLint with ui/eslint.config.js on the five touched files: 0 errors, no warning on a changed source line (the new tests carry the no-explicit-any warnings 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/objectstack derived 34 commands for the 7 changed paths; all 34 exit 0, each code captured before any pipe, among them check:nul-bytes, check:test-source-alias, check:cross-package-test-inputs, check:type-check-coverage, check:type-check-debt, check-sdui-manifest, check-empty-changeset and check-changeset-no-major. pnpm --dir ui exec vitest run scripts/: Test Files 178 passed, 2 skipped (180), Tests 5065 passed, 2 skipped.
  • --ran reconciled: "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.
  • Families CI runs over the whole tree (type-check lanes, lint, artifact rosters) are CI's; the PR's check-runs are the reading.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QsR2jSH8FbKjEztmGT1sbB


Generated by Claude Code

…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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#4 · session_01QsR2jTfvjNTaR29ehLk26T · 2026-10-27T21:44Z · head 1bcc587b53df049e4ddc5dfd584af3eee41ecbc1

  1. Shape: draft, base main, Fixes #26747. Clause-②: no and Metadata-protocol: none are in the body and the changeset, and they match the claim and amendment (6452823300 and 6452857550). Seven files: useConsoleActionRuntime.tsx (+14/−3), RecordDetailView.tsx (+12/−1, the handlers map only), consoleServerAction.ts (+3/−2, the predicate now imported), the new utils/newTabUrlFastPath.ts (+62), two test files beside, and one @object-ui/app-shell patch changeset. No ui/packages/core/**, no packages/spec/**, no server package, no key.
  2. Premise, measured: the seat read the diff. The break is in the console's two registered type: 'api' handlers (useConsoleActionRuntime.apiHandler, RecordDetailView.apiHandler), which never read opensInNewTab/newTabUrl. withNewTabUrlFastPath(apiHandler, serverActionHandler) routes an action carrying the pair to the server-action handler's existing zero-roundtrip path. That handler resolves the record id per surface, pre-opens the tab, and sends no request. Every other api action goes to apiHandler unchanged, and refuseBeforeInput is forwarded only for fast-path actions. takesNewTabUrlFastPath is the one predicate both sides read, and no second interpolator is added. The new tests are 7/7, and the ablation reddens 4.
  3. Gates, pinned to the head: check-runs?filter=latest 43 success, 9 skipped, 0 failing or pending · check-governed-merges --pr 26922: NOT governed, +546/−6. mergeable_state: clean. origin/main 8074feb6 carries neither withNewTabUrlFastPath nor takesNewTabUrlFastPath, so nothing here is already landed.
  4. Accepted deviations: the rule lives in a new utils/newTabUrlFastPath.ts rather than in consoleServerAction.ts. Nineteen host tests stub that module with a factory, so a rule kept there would vanish under every stub. The new file is the amendment surface's predicate, moved one file over, inside app-shell/src/utils. RecordDetailView composes the handler beside serverActionHandler and swaps the handlers-map entry instead of editing apiHandler's body. A second test file covers the list toolbar.
  5. Out of scope: the open question about an absolute newTabUrl (the spec's JSDoc says path template, its describe says URL template) is filed for the spec lane as spec: ActionSchema.newTabUrl is a "path template" in its JSDoc but a "URL template" in its describe; an absolute value opens backend origin + the URL #26924. The two read-only notes are recorded in the PR's Acceptance notes, and neither is a card yet: opensInNewTab without newTabUrl on an api action, and core ActionRunner.executeAPI on hosts with no registered handler. No skip, quarantine or raised timeout. Skills-docs: is not owed.
  6. Prose face, checked against the diff: the changeset names the three surfaces (record header, more-menu, list toolbar) the code composes, and its multi-select refusal is the fast path's own. The code comments cite console: a type: "api" action with opensInNewTab + newTabUrl opens no tab and POSTs to its target instead #26747.

Landing: no contract review is owed (no published schema, no governed text, Clause-②: no). The seat marks the PR ready and arms auto-merge. On MERGED, Fixes #26747 closes the card.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 21:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 21:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 77c9d13 Oct 27, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-bol9ec branch October 27, 2026 22:08
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 tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: a type: "api" action with opensInNewTab + newTabUrl opens no tab and POSTs to its target instead

2 participants