Repository navigation
console: 列表「新建」成功后的去向只由对象字段数决定,form view 的 submitBehavior 在这条路径上解析通过却静默无效 #16381
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 8, 2026 分诊:
repo:objectui/Bug/priority:p2/pm:queue域 —— 读代码判定:三处相关代码全在 objectui,本仓无一处:
objectui packages/plugin-form/src/DrawerForm.tsx:399 if (schema.onSuccess) { :400 await schema.onSuccess(data); :445 if (schema.onSuccess) { :446 await schema.onSuccess(result); objectui packages/plugin-view/src/recordSurface.ts (deriveRecordSurface / RECORD_SURFACE_PAGE_THRESHOLD) objectui packages/plugin-view/src/index.tsx:21 export { deriveRecordSurface, deriveRecordFlowSurface, RECORD_SURFACE_PAGE_THRESHOLD, … }本仓这边只有
packages/console那个 4 文件的发布壳(其package.json自陈「Source of truth: @object-ui/console … objectui」)。⇒repo:objectui。⭐ 复核推翻了「这个能力不存在」的读法 ——
submitBehavior是实现了的,被短路的只是这一条路径上报读产物得出「
submitBehavior在这条路径上永远读不到」,这是对的;但很容易被后来者读成「console 还没做submitBehavior」。不是:objectui packages/plugin-form/src/ObjectForm.tsx:513 // Terminal state for `submitBehavior: { kind: 'thank-you' | 'next-record' }` objectui packages/plugin-form/src/ObjectForm.submitBehavior.test.tsx:40 describe('ObjectForm — submitBehavior', …) :55 submitBehavior: { kind: 'redirect', url: '/apps/x/done' } :73 submitBehavior: { kind: 'thank-you', message: 'All set!' } :111 submitBehavior: { kind: 'continue' } ← 上报用的正是这个 kind⇒ ⭐
kind: 'continue'在ObjectForm上有专门的钉子测试,是通的。 缺陷是优先级:列表「新建」这条路径由外壳注入onSuccess,而DrawerForm.tsx:399/:445的写法是if (schema.onSuccess) { await … }—— 宿主回调无条件先行,作者声明的submitBehavior连读都没读。这把本卡从「请实现一个能力」(Feature)变成「一条路径吞掉了已实现的声明」(Bug),也大幅缩小了修复面:不是造功能,是让一个已经存在且已测的分支在这条路径上有机会执行。
类型
Bug按类型判据:
submitBehavior是 specui/view.zod.tsFormViewSchema上已声明的键,文档写明continue是「reset the form so another response can be entered」,os validate放行。而这条路径上它静默无效。⇒ 已声明契约未兑现(class b),同时是典型的 parse-clean no-op 陷阱(class c:作者写了一个没人读的键,且必须去读产物才知道)。⛔ 不是 Feature。等级
p2- 高于 p3:受影响的是队列式连续录入这一整类场景(上报的 KPI 应用:一次登记多条加减分 / 多条调整申请),而作者没有任何元数据出口 —— 上报已实测两个对象(
kpi_bonus10 字段 → 抽屉,kpi_adjustment15 字段 → 记录页),声明submitBehavior后「行为一个字没变」。 - ⭐ 更值钱的一条:「新建之后去哪儿」当刻唯一的输入是对象的字段个数。 一个纯呈现层的启发式(
RECORD_SURFACE_PAGE_THRESHOLD,上报测得阈值 12)在决定一个业务流程的形状,而作者加/删一个字段就会翻转它 —— 且没有任何声明能覆盖。 - 不到 p1:无数据损坏、无鉴权面、无租户面;记录确实创建成功了,只是去向不对。
交给认领席(三条建议里,本席对确定性的判断)
上报按偏好排了三条,本席按是否需要裁决重排:
- ⭐ 建议 1(让这条路径读
submitBehavior,宿主onSuccess退为兜底)—— 确定,推荐先做这条。 它只对「显式声明了一个今天什么都不做的键」的作者改变行为;没声明的一律走原路。⇒ 只拒绝/改变已经坏掉的东西,零回归面。而且它让这条路径与/_console/forms/:name路径一致,消除的是两条路径两套语义。 - 建议 2(列表视图上给一个显式去向键)—— 扩大公开面(Feature),⛔ 出本卡范围。 若做,需要新 spec 键 + 契约评审;且做完建议 1 之后是否还需要它,要重新评估 ——
kind: 'continue'已经表达了「留在列表继续录入」。⛔ 不要在本卡里顺手加 spec 键。 - 建议 3(声明即报错)—— 是建议 1 的替代品,不是补充。 一旦建议 1 落地,
submitBehavior在这条路径上就生效了,也就无需报错。⇒ 只有在维护者决定这条路径永远不读submitBehavior时,建议 3 才成立;那时它属domain:spec的 lint,另立卡。
⇒ 本卡范围 = 建议 1。若认领席读完认为建议 1 不可行(例如
onSuccess承载了 SDUI/内联字段采集器的写入语义,见DrawerForm.tsx:394那条注释「the inline-fields collector, whoseonSuccessIS the write」),停下回报 —— 那条注释确实标出了一个不能一刀切退为兜底的onSuccess用法,是本卡最需要小心的地方。⚠️ 其余边界:- 上报测于 17.2.0;本席的读数取自 objectui
origin/main当刻,两者可能已分叉,认领时重读。 ⚠️ 本席没有复现浏览器行为,也没有验证阈值 12 这个数 —— 本席确认的是RECORD_SURFACE_PAGE_THRESHOLD是一个具名导出常量(plugin-view/src/index.tsx:21),不是散落在代码里的魔数。⇒ 认领时按符号读它的值,⛔ 不要继承卡面的 12。
分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。
Generated by Claude Code
- 高于 p3:受影响的是队列式连续录入这一整类场景(上报的 KPI 应用:一次登记多条加减分 / 多条调整申请),而作者没有任何元数据出口 —— 上报已实测两个对象(
现象
控制台列表页点「新建」→ 填完表单点「创建」后,浏览器一定会离开列表:要么在列表上盖一层记录抽屉(
?recordId=…),要么整页跳到记录页(/<object>/record/<id>)。作者无法用元数据改变这个去向 —— 想让操作员留在列表连续录入(队列式登记场景:一次登记多条加减分 / 多条调整申请)就没有办法。在 form view 上声明
submitBehavior: { kind: 'continue' }(specui/view.zod.ts的FormViewSchema.submitBehavior,文档说「reset the form so another response can be entered」)通过了os validate,但对这条路径完全无效,创建后照样弹记录抽屉。读了一下
@objectstack/console@17.2.0的产物,去向是这样定的:formType: 'modal'的object-form,并给它传了onSuccess回调;plugin-form里的提交处理是if (e.onSuccess) await e.onSuccess(n); else if (e.submitBehavior) {…}——onSuccess短路了submitBehavior,所以表单视图上声明的submitBehavior在这条路径上永远读不到;onSuccess的去向由recordSurface(objectDef)算出来:可见字段数 >= 12 → 记录页,否则 → 记录抽屉。既不看submitBehavior,也不看列表视图的navigation。也就是说:「新建之后去哪儿」这件事目前唯一的输入是对象的字段个数,没有任何元数据开关。
最小复现(17.2.0)
demo_note,5 个字段),给它一个列表视图和一个 form view;submitBehavior: { kind: 'continue' };os validate→ 通过,无告警;os dev→ 控制台打开/_console/apps/<app>/demo_note→ 点「新建」→ 填必填项 → 点「创建」。期望:表单重置,停在列表上,可以接着录下一条(
kind: 'continue'的语义)。实际:URL 变成
/_console/apps/<app>/demo_note?recordId=<新记录 id>,列表上盖出记录抽屉,「新建」按钮被挡住;submitBehavior没有任何效果。字段数 ≥ 12 的对象上同样声明
submitBehavior,实际结果是整页跳到/demo_note/record/<id>—— 同样与声明无关。我们这边的实测(KPI 应用,17.2.0):
kpi_bonus(10 个字段)→ 抽屉;kpi_adjustment(15 个字段)→ 记录页;两者都在 form view 上试过submitBehavior: { kind: 'continue' },行为一个字没变。期望能力
三选一都能解决,按我们的偏好排序:
submitBehavior:外壳的onSuccess只在表单没声明submitBehavior时兜底,声明了就让表单自己的行为优先(与/_console/forms/:name路径一致)。kind: 'continue'就正好表达「留在列表继续录入」。recordSurface的字段数启发式只作为默认值。submitBehavior在这条路径上声明即报错,而不是解析通过却静默无效 —— 现在的表现符合「parse-clean no-op」,作者要读产物才知道自己写的键没人读。平台版本
@objectstack/spec/@objectstack/console/@objectstack/cli均为 17.2.0;运行方式objectstack dev,SQLite(better-sqlite3)驱动,单租户。