Repository navigation
Commit e1bfde3
fix(core): interpolate api action targets before a registered api handler (#26826)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb3cr6whx99bskUDH3xXu7A3)_
Fixes #26722
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:记录页上 `type: "api"` 的动作,目标地址里的 `${ctx.recordId}`
等占位符现在会先被替换成真实值再发请求,和 `url` 动作用的是同一套替换与编码。
- **为什么改**:控制台注册了自己的 api 处理器,绕过了 ActionRunner 里唯一做替换的
`executeAPI`,请求带着字面占位符发出(`…/$%7Bctx.recordId%7D`),记录级 PATCH/POST
动作在每条记录上都打到不存在的记录。
- **风险与回滚**:只影响「已注册 api 处理器 + 目标里带 `${` 占位符」的动作;不带占位符的动作原对象原样交给处理器。回滚即
revert 本 PR。
- **先看**:`ui/packages/core/src/actions/ActionRunner.ts` 中
`dispatchCollected`
的已注册处理器分支;`ActionRunner.apiHandlerTarget-26722.test.ts`。
- **两分钟验证**:`pnpm --dir ui exec vitest run
packages/core/src/actions/__tests__/ActionRunner.apiHandlerTarget-26722.test.ts`,预期
7 个用例全绿。
- **需要你做的**:无,席位落地。
## What changes
`ActionRunner.dispatchCollected` now runs `interpolateTarget` on an
`api` action's string `target` before handing the action to a registered
`api` handler. It is the same call the built-in `executeAPI` and
`executeUrl` make, so an api target gets the same scopes (`ctx`,
`param`) and the same per-value `encodeURIComponent` as a url target.
There is no second interpolator and no change in `app-shell`.
- The landing site differs from the card's first guess. `executeAPI`
already interpolated, but the Console registers its own `api` handler
(`useConsoleActionRuntime`'s `apiHandler`, wired through
`ActionProvider`'s `handlers`), and that handler replaces `executeAPI`
entirely. The handler only filled `{field}` row tokens, so
`${ctx.recordId}` reached `authFetch` literally.
- An api target with no token reaches the handler as the same action
object. A registered handler for any other type (`flow`, `script`,
`modal`) still gets its target exactly as authored.
- Comments on `interpolateTarget` now name the new caller.
## Why
The spec documents `${ctx.recordId}` for api targets, and a url action
with the same token already resolved. Record-level API actions could not
be built without code because they failed on every record.
## Tests
New
`ui/packages/core/src/actions/__tests__/ActionRunner.apiHandlerTarget-26722.test.ts`
(7 cases):
- a PATCH and a POST api action on a record page hand the registered
handler `/api/v1/data/gapdata_item/gd_001`;
- an id with a slash and a space is encoded the same way for an api
target and a url target (`a%2Fb%20c`);
- with no handler registered, the built-in executor sends the same
resolved path;
- a url action with the same token is unchanged;
- a registered `flow` handler still gets its target as authored;
- an untokenized api action reaches the handler as the same object.
## Acceptance notes
- The url path also runs the expression evaluator after substitution;
the api path did not before and does not now. The order asked for the
same variables and encoding, which this delivers, and nothing more.
- `${ctx.recordId}` on a list row action resolves from the runner's
context (the page record), not from the clicked row. That is how url
targets behave today too, so it is unchanged here. Row actions keep
using the `{field}` row tokens the Console's handler already fills.
## Verification
<details><summary>Ablation (one-time, on head 333eef5, mutation
restored)</summary>
With `actionType === 'api'` replaced by `false` in the new branch (via
`scripts/ablation-replace.mjs`, anchor 1 → 0, blob restored to HEAD's),
the new file reads 3 failed / 4 passed: the PATCH, POST and encoding
cases go red, and the built-in, url, flow and same-object controls stay
green.
</details>
<details><summary>Suites and type-check (head 333eef5)</summary>
- `pnpm --dir ui exec vitest run packages/core/ --maxWorkers=1` under
the verify lock: Test Files 215 passed (215), Tests 4112 passed, 27
skipped.
- `pnpm --filter @object-ui/core run type-check`: exit 0.
- `pnpm --dir ui exec vitest run scripts/ --maxWorkers=2` under the
verify lock: Test Files 178 passed, 2 skipped (180); Tests 5065 passed,
2 skipped.
- `pnpm check:new-line-citations` (from `ui/`): 0 new citations.
`eslint` on both changed files: 0 errors; the 15 warnings are existing
`no-explicit-any` lines the diff does not touch.
- Narrowing: `app-shell` and `components` suites were not run locally. A
`git grep` over ui tests finds no test outside `packages/core` that
registers an api handler and dispatches a target carrying a `${` token,
and untokenized actions reach the handler as the same object. CI's Test
UI runs those suites.
</details>
<details><summary>Derived gates (head 333eef5)</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 34 commands. 33 ran locally, each
exit 0, and with the scripts suite `--ran` reconciles all of them (`Run
reconciliation — 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN`). The
34th, `pnpm --dir ui exec vitest run scripts/`, is in the suites block
above.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jTDpMW8KaXLg1DFU6f)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bff1546 commit e1bfde3
3 files changed
Lines changed: 148 additions & 3 deletions
File tree
- .changeset
- ui/packages/core/src/actions
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1680 | 1680 | | |
1681 | 1681 | | |
1682 | 1682 | | |
1683 | | - | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
| 1686 | + | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
1684 | 1695 | | |
1685 | 1696 | | |
1686 | 1697 | | |
| |||
2802 | 2813 | | |
2803 | 2814 | | |
2804 | 2815 | | |
2805 | | - | |
| 2816 | + | |
| 2817 | + | |
2806 | 2818 | | |
2807 | 2819 | | |
2808 | 2820 | | |
| |||
2867 | 2879 | | |
2868 | 2880 | | |
2869 | 2881 | | |
2870 | | - | |
| 2882 | + | |
| 2883 | + | |
2871 | 2884 | | |
2872 | 2885 | | |
2873 | 2886 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
0 commit comments