Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-form): consume submitBehavior.url as objectstack#7496 ruled it (#4989) - #5032
Conversation
…d it
ObjectForm and WizardForm read a declared `submitBehavior: { kind: 'redirect' }`
through `isSameOriginUrl` and navigated when that answered yes. Measured against
the ruled shape (objectstack#7496, landed by objectstack#7657, live on the
`@objectstack/spec` 17.0.0 GA pin) that diverged three ways, all fixed here
(objectui#4989 defects 2, 3 and 5):
- an out-of-contract destination was dropped in SILENCE after a successful
write, leaving the submitter facing a still-filled form whose obvious next
move wrote a second record. A refusal is now visible: the spec's own
author-facing prescription in an alert beside the confirmation that the record
WAS written, toasted as well, with the filled form replaced so there is
nothing left to resubmit.
- a same-origin ABSOLUTE url was followed, where the contract is relative-only,
so this renderer accepted a spelling the authoring door refuses. The verdict
is no longer restated here at all: `resolveSubmitRedirect` asks the spec's own
`FormViewSchema` at the moment of use, so a later widening of the ruling is
followed by the version pin rather than by an edit.
- `{{record.field_name}}` was never substituted. It now is, from the values as
submitted with whatever the DataSource answered layered on top, every
interpolated value through `encodeURIComponent` so a token is a value in the
path and never a way to add path structure.
`delayMs` semantics are unchanged. `navigateOnSuccess` is a different declared
key with its own dialect and its own open contract question and is untouched;
`isSameOriginUrl` survives because that key still needs it.
Defect 4 (mount-blindness) is deliberately NOT fixed and is escalated on the
issue: applying a host's mount means learning it, and every mechanism available
to a published renderer changes its contract. `submitRedirect.mountBlind.test.tsx`
pins the measurement so the open defect stays visible.
Refs objectui#4989
Co-authored-by: Claude <noreply@anthropic.com>The four mutations were predicted in these docblocks and then run. The predictions held; the docblocks now carry the MEASURED numbers rather than prose, including the two facts a reader would otherwise have to rediscover: - restoring the pre-ruling consumption (`isSameOriginUrl` + assign the authored string) turns 12 of the 16 component tests red and leaves the unit file ENTIRELY green — it kills the call site, not the module, which is why the consumption is pinned per component. - deleting only the refusal arm while keeping the stricter parse turns exactly the 3 refusal tests in each component red. That mutation IS the original defect 2, reproduced with a relative-only guard in place, so "the guard is stricter now" is not by itself the fix. Also measured: swapping the spec's refusal prose for a local sentence turns 18 red (14 of them in the unit file), which is the load-bearing probe that the sentence on screen comes from the live schema parse; and deleting the `encodeURIComponent` turns 10 red. Refs objectui#4989 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 17, 2026
PM 验收 ✅ ACCEPT(#4989 的缺陷 2/3/5 半边,批次 21) 实物核验(merge-base CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。 缺陷 4 的升级处置采信:PM 裁定的中间路线被两点实测拆解 —— React 层可行(context 读法、basename 吞吃均量到),依赖层不可行(读 context 必须 import react-router;包四个依赖字段均未声明; 合并卫生(PM 裁定):保留 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#4989
ObjectForm/WizardForm现在按 objectstack#7496 的裁定消费submitBehavior: { kind: 'redirect' }(该裁定由 objectstack#7657 落地,活在本仓@objectstack/spec17.0.0 GA 钉子上)。卡面五点里的 2、3、5 机械修完;第 4 点(挂载盲)实测后判定不可按派发的机制落地,已升级为决策题,详见下文"未修的一半"。契约判据不在本包重写一份
两个消费点原来都是
isSameOriginUrl(behavior.url)把 URL 解析到window.location.href再比 origin。新的packages/plugin-form/src/submitRedirect.ts不再复述规则,而是在使用的那一刻拿 spec 自己的FormViewSchema解析最小 form view —— 与 PR #4992(console 侧同裁定实施件)同一份来源。因此绝对 URL、协议相对//host、反斜杠、空白与控制字符走私、畸形 token、文档相对路径,全部用 spec 自己的作者向措辞拒绝;裁定日后放宽(它自己写了绝对 origin 白名单"等实测需求")时,本包跟着版本钉子走,不需要再改这里。⛔ 没有 import console 的模块(已发布包不能依赖 app),⛔ 没有手写第二份七族规则镜像 —— 那正是
scripts/check-spec-symbol-derivation.mjs反对的形状。两包唯一重复的是 token 替换与转义那约 20 行(spec 不导出替换器),docblock 里把两处的差异逐条写明了,便于将来收敛。三处修正
if直接不进:无 toast、无错误、无确认面板。写入已经成功了,提交者面对的是一张仍然填满的表单和零反馈,而最自然的下一步 —— 再提交一次 —— 会写出第二条记录。现在拒绝同时说两件事:记录确实写成了(保留确认面板,顺带把填满的表单换掉,没有可重复提交的东西了),以及为什么没有跳转(spec 的作者向措辞,role="alert"面板 +toast.error)。拒绝挂在submitted上而不是error上 ——error是加载/提交失败通道,契约外的目的地不是提交失败,放进去等于告诉用户记录丢了。isSameOriginUrl('https://own-host/thanks')为真,所以本消费点接受了一个 spec 已在录入门口拒绝的拼法 —— 被拒拼法就是这样活在语料里的。relative-only 严格强于 same-origin(带根的相对路径天然同源),所以这条 arm 上保留isSameOriginUrl作纵深毫无增益,已移除;函数本身留着,因为resolveSuccessNavigate还需要它(见下)。submitRedirectScope,所以{{record.id}}不会在 wizard 里解析成一个样、在平表单里另一个样。每个插值都过encodeURIComponent—— token 是路径里的值,永远不能变成新的路径结构。delayMs语义一字未动。navigateOnSuccess是另一个声明键(自己的{id}方言、自己尚未裁定的契约问题),按卡面划界一字未动。未修的一半:缺陷 4(挂载盲)—— 升级为决策题,未擅自选路
派发给我的中间路线是"可选读 React Router context"。React 那一半实测成立:provider 外
useInRouterContext()返回false、useContext(UNSAFE_NavigationContext)返回null(不抛);挂basename="/_console"时 context 在场、basename读得到,且useNavigate()('/thanks')确实吃掉 basename 落在/thanks路由上。依赖那一半不成立,这是停下的原因:
@object-ui/plugin-form在 dependencies / peerDependencies / optionalDependencies 里都没有 react-router,本仓能解析到它纯粹因为它是仓根 devDependency 被抬上来了 —— 这正是scripts/check-phantom-dependencies.mjs(objectui#4394)存在的理由,它的文件头还记着plugin-detail那次同类真缺陷。vite.config.ts把所有裸 specifier 全部 external,所以静态 import 会在发布的dist/index.js里留下裸react-router-dom,每个消费者都必须解析它。而确实存在没有 react-router 的消费者:apps/site把@object-ui/plugin-form列为真依赖并在app/components/PluginLoader.tsx里 import,自身任何字段都没有 react-router;packages/plugin-view同样。import()也不成立:消费者的打包器仍要在构建期解析它;而用打包器分析不到的 specifier 绕过,是 AGENTS.md 准则 Fix documentation deployment for www.objectui.org #6 明令禁止的。按派发纪律,⛔ 没有改走注入回调/新 prop,也没有擅自换第三种机制。选项与两轴分析写在报告的
open_questions里交维护者裁。今天的行为保持不变(相对路径交window.location.assign),submitRedirect.mountBlind.test.tsx把这个开放缺陷的实测钉住 —— 包括对照:同一个字符串按 app-relative 解析确实会落进挂载点内。钉它是为了不让一个"衡量过、故意留着"的缺陷退化成口头传说。Fixes #4989(按派发要求逐字),但合并即关闭会连带关掉尚未裁定的第 4 点。合并前请按裁定结果改成Refs、或先把第 4 点拆成后续 issue。验证
pnpm exec vitest run packages/plugin-form/(仓根,与 CI 同路径):51 文件 / 535 测试全绿。新增 3 个测试文件、1 个实测记录文件。submitRedirect.mountBlind一开始写成.test.ts,从仓根跑时被分到 node 环境的unitproject,document is not defined3 红 —— 只在包目录里跑是看不到的。按vitest.config.mts自己写的约定改名.test.tsx(而不是去改共享的根配置)。packages/plugin-view/+packages/react/src/spec-bridge/(17 文件 / 222 测试全绿);app-shell 里承载这两个组件的 4 个文件(41 测试全绿)。pnpm exec turbo run type-check --concurrency=2:81/81 successful。node scripts/check-control-bytes.mjsOK;改动文件另做了含 0x09 的越门自扫(测试里有制表符走私用例,一律写成转义序列)。check-phantom-dependencies.mjs、check-spec-symbol-derivation.mjs、三个 changeset 门:全绿。改动文件 eslint 0 error。反向验证(先书面预判,再变异,
git checkout --还原;预判全部命中)isSameOriginUrl+ 直接 assign 原串)encodeURIComponent两条值得单独记下来的:①退回裁定前消费时单元文件全绿 —— 它杀的是调用点而非模块,这就是消费语义必须钉在组件旁边的原因;②"只删拒绝 arm"这个变异就是原始的缺陷 2,只是 guard 已经变严了 —— 所以"guard 现在是 relative-only 了"本身并不构成修复。
派发模板里的第三条反向验证(撤 router 分支 → basename 用例红)在本 PR 不适用:没有 router 分支可撤。照直写出来,而不是造一个符合模板形状的证据。
Generated by Claude Code