Skip to content

type: 'form' action fired from a record opens an empty CREATE form — /forms/:name ignores the ?recordId= ActionRunner forwards #4278

Description

@yinlianghui

Found while implementing #4109 (which touches the same route and the same submit path). Filed unassigned for triage; deliberately not fixed in that PR — it is a separate defect with a separate acceptance test, and #4109's scope is the shell nesting plus the post-submit default.

What happens

ActionRunner.executeForm forwards the current record id to the internal form route:

packages/core/src/actions/ActionRunner.ts:1325-1329

const recordId =
  (this.context.record && (this.context.record as { id?: unknown }).id) ??
  (this.context.data && (this.context.data as { id?: unknown }).id);
const base = `/forms/${name}`;
const to = recordId != null ? `${base}?recordId=${encodeURIComponent(String(recordId))}` : base;

apps/console/src/components/FormPage.tsx never reads that param. The only query params it consumes are the prefill_ ones, in readPrefill. So in internal mode the route:

  1. loads the FormView metadata and renders empty inputs — the record's current values are never fetched;
  2. submits through submitInternal, which is POST /api/v1/data/:object — an INSERT, unconditionally.

An "edit this record" action therefore renders a blank form and, on submit, creates a second record instead of updating the one the user was looking at.

Reproduction (showcase)

showcase_log_time in examples/app-showcase/src/ui/actions/index.ts is exactly this shape — type: 'form', target: 'showcase_task.edit', locations: ['record_header', 'record_related', 'record_section']. Open any Task, click Log Time, fill the form, Submit: a NEW task is created and the original is untouched.

Why it is worth grading now

The forwarded ?recordId= says the producer's intent is an edit, and the target view is literally named edit, so the metadata is not at fault — the renderer simply never implemented the read side. Until #4109 the outcome was invisible: the post-submit default was the anonymous thank-you panel, so the user saw "Your submission has been received" and no record. After #4109 an internal submit lands on the record it created, which means the duplicate becomes visible immediately (arguably an improvement — it surfaces the bug instead of hiding it — but it is still a duplicate).

What a fix would need to decide

Not obvious enough to guess, which is part of why it is filed rather than fixed:

  • whether recordId should make the route load the record and switch the submit to PATCH /api/v1/data/:object/:id, or whether internal type: 'form' should route to the existing app-shell record-form surface instead of this standalone renderer;
  • what the route should do when recordId names a record the user cannot read, or one belonging to a different object than the view's target;
  • whether recordId deserves a place in app-shell/src/urlParams.ts (it is adjacent to the reserved recordId drawer param already registered there, and the collision is worth ruling on explicitly).

Measured on

origin/main at 4cb0562b5, by reading the two files above; not browser-verified.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 11, 2026
  2. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    CollaboratorAuthor

    TRIAGE + CLAIM + 代决 — session session_017Qqyix2QcnpUC9XeYVDzx3(objectui whole-repo PM seat)。定级 bug(今日可撞:编辑动作产出重复记录)。Branch: claude/issue-4278-form-edit-recordid,gated on PR #4279 落地(同 FormPage 面,dev 先以 git 确认 #4279 的 squash 已在 origin/main 再动第一笔)。

    三个先决点的裁决(以产品长期稳定角度代决,维护者可否决):

    1. fix 形态:本路由实现读侧。?recordId= 存在时:加载记录、以现值预填、提交走 PATCH /api/v1/data/:object/:id。不改道 app-shell record-form 面——那会为一种模式分叉表单渲染路径,且改导航目的地是 core/actions 编辑([P3] objectui ActionDef's [key: string]: any absorbs every retired and misspelled action key — removing a spec key produces no compile error #4046 在飞持有)。Default list view synthesized from highlightFields omits the nameField — the showcase Account list shows 14 rows with no name column #7245 裁决保留了这条路由,编辑深链就应在这条路由上完整成立;producer 转发的 recordId + 名为 edit 的目标视图已声明意图,缺的只是读侧。

    2. 不可读 / 对象不符:fail closed。记录加载失败(403/404)或视图目标对象与记录对象不符时,渲染表单错误面板,绝不静默降级为 create 模式——静默降级正是本缺陷的伤害原型(空表单 + INSERT)。

    3. recordId 与 drawer 保留参数的撞名:显式裁定而非回避。dev 实测两个读者的路由域是否真正相交;至少以测试钉住"内部表单路由的 recordId 读取只在该路由生效"。若需要在 app-shell/src/urlParams.ts 登记,允许单行登记(与 REGRESSION: the console header bell panel is dead again — "No notifications" under both Unread and All while /api/v1/notifications returns 10 rows (#4110 / #4156 back on console 09987b68) #4230 持有的 layout/hooks 不同区域),在 PR 正文说明与 drawer 参数的关系。


    Generated by Claude Code

  3. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    CollaboratorAuthor

    ACCEPT — PM review of record (session session_017Qqyix2QcnpUC9XeYVDzx3)。

    PR #4293 按三点裁决落地,验收要点:

    • 读侧完整:?recordId= ⇒ GET 加载、现值预填、PATCH /data/:object/:id 保存、落回被更新的记录。更新动词是测出来的(spec 路由表 + 全工作区四个更新客户端同拼写),不是选的。
    • 单一事实源的克制:edit 路径故意不复用 readCreatedRecordId——id 已由 URL 知晓,第二个读者只可能因 server 缺陷而分歧、渲染器还会跟着错;用"响应报不同 id"的 pin 钉住该决定。正确。
    • fail-closed 三路全钉(403/404、对象不符、空参),预填优先级裁定合理(prefill_ 点名字段胜出、记录填其余、存储的 null 压过 create 默认值——编辑表单永不静默提出未做的修改)。
    • drawer 参数判定:实测不相交(React Router 单叶渲染 + 两读者路由域分离),app-shell 侧零功能行(注册表行 + 常量注释),console 字面量与注册表拼写用测试钉等——比注册一个新常量更对。
    • 反向验证 11 红 / 2 绿与文档块预言逐一吻合,头条红就是缺陷原文(期望预填值、收到空白)。CI 一次收敛 18+2。
    • 流程瑕疵如实披露并已自救:反向验证时先 revert 后 commit,源改动一度丢失,靠 transcript 重放恢复并复跑 69/69。正确顺序是 commit-then-revert——下不为例,披露加分。

    #4292 定级采纳(bug 入队):?recordId= 不携带对象名,跨对象 id 碰撞时本修复把伤害从重复插入升级为定向写错记录。producer 半场在 core ActionRunner(#4285 落地后面即释放),将双闸门派发(候 #4285 + #4293 落地)。

    Flipping ready + auto-merge。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions