Skip to content

console: 列表「新建」成功后的去向只由对象字段数决定,form view 的 submitBehavior 在这条路径上解析通过却静默无效 #16381

Description

@baozhoutao

现象

控制台列表页点「新建」→ 填完表单点「创建」后,浏览器一定会离开列表:要么在列表上盖一层记录抽屉(?recordId=…),要么整页跳到记录页(/<object>/record/<id>)。作者无法用元数据改变这个去向 —— 想让操作员留在列表连续录入(队列式登记场景:一次登记多条加减分 / 多条调整申请)就没有办法。

在 form view 上声明 submitBehavior: { kind: 'continue' }(spec ui/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)

  1. 建一个字段数 < 12 的对象(下例 demo_note,5 个字段),给它一个列表视图和一个 form view;
  2. form view 上显式声明 submitBehavior: { kind: 'continue' };
// *.view.ts
export const NoteViews = defineView({
  list: { label: '便签', type: 'grid', data: { provider: 'object', object: 'demo_note' }, columns: [{ field: 'title' }] },
  formViews: {
    form: {
      type: 'simple',
      data: { provider: 'object', object: 'demo_note' },
      submitBehavior: { kind: 'continue' },   // ← 声明了,validate 通过
      sections: [{ name: 'main', label: '便签', columns: 2, fields: [{ field: 'title', required: true }, { field: 'body' }] }],
    },
  },
});
  1. os validate → 通过,无告警;
  2. 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' },行为一个字没变。

期望能力

三选一都能解决,按我们的偏好排序:

  1. 让列表「新建」这条路径读 submitBehavior:外壳的 onSuccess 只在表单没声明 submitBehavior 时兜底,声明了就让表单自己的行为优先(与 /_console/forms/:name 路径一致)。kind: 'continue' 就正好表达「留在列表继续录入」。
  2. 或者在列表视图上给一个显式键表达「新建成功后的去向」(留在列表 / 开抽屉 / 跳记录页),让 recordSurface 的字段数启发式只作为默认值。
  3. 至少让 submitBehavior 在这条路径上声明即报错,而不是解析通过却静默无效 —— 现在的表现符合「parse-clean no-op」,作者要读产物才知道自己写的键没人读。

平台版本

@objectstack/spec / @objectstack/console / @objectstack/cli 均为 17.2.0;运行方式 objectstack dev,SQLite(better-sqlite3)驱动,单租户。

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊: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 是 spec ui/view.zod.ts FormViewSchema 上已声明的键,文档写明 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_bonus 10 字段 → 抽屉,kpi_adjustment 15 字段 → 记录页),声明 submitBehavior 后「行为一个字没变」。
    • ⭐ 更值钱的一条:「新建之后去哪儿」当刻唯一的输入是对象的字段个数。 一个纯呈现层的启发式(RECORD_SURFACE_PAGE_THRESHOLD,上报测得阈值 12)在决定一个业务流程的形状,而作者加/删一个字段就会翻转它 —— 且没有任何声明能覆盖。
    • 不到 p1:无数据损坏、无鉴权面、无租户面;记录确实创建成功了,只是去向不对。

    交给认领席(三条建议里,本席对确定性的判断)

    上报按偏好排了三条,本席按是否需要裁决重排:

    1. ⭐ 建议 1(让这条路径读 submitBehavior,宿主 onSuccess 退为兜底)—— 确定,推荐先做这条。 它只对「显式声明了一个今天什么都不做的键」的作者改变行为;没声明的一律走原路。⇒ 只拒绝/改变已经坏掉的东西,零回归面。而且它让这条路径与 /_console/forms/:name 路径一致,消除的是两条路径两套语义。
    2. 建议 2(列表视图上给一个显式去向键)—— 扩大公开面(Feature),⛔ 出本卡范围。 若做,需要新 spec 键 + 契约评审;且做完建议 1 之后是否还需要它,要重新评估 —— kind: 'continue' 已经表达了「留在列表继续录入」。⛔ 不要在本卡里顺手加 spec 键。
    3. 建议 3(声明即报错)—— 是建议 1 的替代品,不是补充。 一旦建议 1 落地,submitBehavior 在这条路径上就生效了,也就无需报错。⇒ 只有在维护者决定这条路径永远不读 submitBehavior 时,建议 3 才成立;那时它属 domain:spec 的 lint,另立卡。

    ⇒ 本卡范围 = 建议 1。若认领席读完认为建议 1 不可行(例如 onSuccess 承载了 SDUI/内联字段采集器的写入语义,见 DrawerForm.tsx:394 那条注释「the inline-fields collector, whose onSuccess IS 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions