Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

TextAreaField 读取的 mobileFullscreen prop 全仓无人生产,注释却说"宿主表单传入" #3232

Closed

Description

@os-zhuang

在 objectui#3221(给 FieldWidgetComponentProps 去掉 [key: string]: any)时发现,不在该 PR 修。

现象

packages/fields/src/widgets/TextAreaField.tsx 的文档注释声称:

Mobile UX (round 3): when the host form passes mobileFullscreen (or the field schema sets mobile_fullscreen: true), an "expand" affordance opens a fullscreen edit dialog

实现读了四条来源:

const showFullscreenButton = Boolean(
  (props as any).mobileFullscreen ??      // ① camelCase prop
  textareaField?.mobile_fullscreen ??     // ② 字段元数据
  (props as any).mobile_fullscreen ??     // ③ snake_case prop
  (props as any).schema?.mobile_fullscreen, // ④ schema 载体
);

全仓 grep 之后:

  • ①mobileFullscreen(camelCase)没有任何生产者。 唯一出现该拼写的地方就是 TextAreaField 自己(读取处 + 第 57 行的解构剥离)。注释里的"宿主表单传入"从未成立。
  • ③mobile_fullscreen 作为 prop 也到不了:form.tsx 的 stripRegisteredFieldProps() 明确把 mobile_fullscreen 和 fullscreen 从转发给已注册 widget 的 props 里剔除(第 266-267 行)。
  • 真正活着的只有 ②(以及 ④,在表单路径下与 ② 指向同一个对象)。生产者是 packages/plugin-form/src/ObjectForm.tsx:1049:{ ...f, mobile_fullscreen: true },写在字段元数据上。

后果

不是运行时 bug(功能经 ② 正常工作),而是三个可维护性问题:

  1. 注释描述了一条不存在的契约,下一个作者会照着它去传一个被静默忽略的 prop;
  2. 三条死读取路径,其中 ③ 被上游明确剥离 —— 属于「消费者侧宽容」正在固化第二套事实契约(AGENTS.md #0.1);
  3. mobileFullscreen / mobile_fullscreen 两种拼写并存,恰好是主令 [WIP] Enhance UI components for forms and layouts #12 说的那种应该在生产者收敛、而不是在消费者用 ?? 累积的情况。

建议

保留 ②(元数据是唯一真实生产者),删除 ①③④,并把注释改成描述真实来源。如果确实需要「宿主强制全屏」的 prop 覆盖能力,那就明确声明一个键并让某个宿主真的传它 —— 而不是三个拼写都读、都没人写。

为什么现在才能看见

FieldWidgetComponentProps 当时以 [key: string]: any 结尾,(props as any).mobileFullscreen 是完全合法的读取,类型 any,运行时恒 undefined。objectui#3221 收紧类型后,这类「读了但没人写」的键才可能被追问。

关联:objectui#3221、objectui#3161。

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    认领:PM 循环第 1 轮
    会话:session_01NVPjPzmmAJ2Ngtvgg5MSRa(objectui 分片 PM,账号 xuyushun441-sys,已在 objectstack#4604 登记)
    分支:claude/issue-3232-textarea-mobilefullscreen
    Worktree:objectui-issue-3232

    复核记录:四条读取路径与「只有 ② 活着」实测属实

    origin/main @ ca58e45 全仓 grep,原单的判断逐条成立:

    路径 现场 生产者
    ① mobileFullscreen (camelCase) TextAreaField.tsx:47 读、:57 解构剥离 无 —— 全仓仅此文件出现该拼写
    ② field.mobile_fullscreen TextAreaField.tsx:48 ✅ plugin-form/src/ObjectForm.tsx:1049
    ③ props.mobile_fullscreen TextAreaField.tsx:49 到不了 —— components/src/renderers/form/form.tsx:266 明确剥离
    ④ props.schema.mobile_fullscreen TextAreaField.tsx:50 表单路径下与 ② 同一对象

    决策裁定(PM,不升级维护者,可否决)

    原单给的建议(留 ②,删 ①③④,改注释)直接采纳,不问维护者。依据 SKILL.md 的升级门槛:这是「declared ≠ enforced / 消费端宽容」的恢复不变量类修复,属于明确的非升级类 —— 仓规(AGENTS.md #0.1、主令 #12)已经把答案写死了:分歧在生产者收敛,不在消费者用 ?? 累积。

    按两条评估轴:

    • 长远合理性:①③④ 是三条永远为 undefined 的读取,留着就是在文档和代码里同时维持一份不存在的契约,下一个作者照注释传 prop 会被静默忽略 —— 删除是唯一与主令 [WIP] Enhance UI components for forms and layouts #12 一致的走法;
    • 防 AI 写错:?? 链恰恰是 AI 批量犯错被掩盖的温床(读四个拼写、都不报错、都没人写)。收敛成单一来源后,写错的拼写会没有任何读取路径接住,错误变响亮。

    ⚠️ 关于原单末尾「如果确实需要宿主强制全屏的 prop 覆盖能力」:本单不新增这个能力。今天没有任何宿主需要它,凭空声明一个键正是主令 #12 反对的。真有宿主需要时单独立单、明确声明一个键、并让宿主真的传。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    PM 验收:ACCEPT — PR #3244,已入合并队列

    按 GitHub 实况核对:

    项 结果
    PR targets main、Fixes objectstack-ai/objectui#3232 ✅
    改动范围 ✅ 3 个文件:TextAreaField.tsx、新增测试、changeset。未动 form.tsx(scope fence 被遵守)
    content/docs/releases/ 零改动 ✅
    CI ✅ 14 绿 + 2 skipped,零失败(含 Changeset Bump Policy 与 Changeset Fixed Group Check)
    测试证据 ✅ 含 mutation 验证

    四路 ?? 链收敛成 Boolean(textareaField?.mobile_fullscreen),①③④ 连同第 57 行那条只为挡 DOM spread 而存在的解构一并删除,注释改写为描述真实生产者。没有新增宿主覆盖 prop —— 我在派发时定的非目标被遵守了,并且把「真需要时的正确做法」写进了注释而不是直接实现。

    护栏是真的,不是装饰

    关键一条:测试不只断言死路径消失,还钉住活路径仍然工作(元数据 flag → 展开按钮 → 开对话框 → 改草稿 → 点 Done 才回调)。mutation 验证:把实现临时改回 Boolean((props as any).mobileFullscreen),6 个用例里 4 个立刻转红(含全部三个活路径用例),恢复后全绿。这正是我在派发时要求的「不能只证明死路径没了」。

    退役拼写用 @ts-expect-error 在编译期钉住(而 @ts-expect-error 本身不能空转 —— 未使用的指令是 TS2578 错误,所以 type-check 干净通过同时证明了这两条确实各压住一个真实报错),运行时也钉住了「无类型宿主硬塞进来入口也不复活」。

    ⚠️ 越界发现 #3245 更要紧,已标 needs-user-decision

    这单最有价值的产出可能不是 PR 本身,而是它顺链路查出来的:ObjectForm 的 mobile.fullscreenLongText 对自动生成字段根本到不了 TextAreaField。

    链条断在两处相互配合的地方:flag 被盖在 FormField 自己身上(ObjectForm.tsx:1049),而原始元数据挂在 f.field 槽上(:584);form.tsx:1336 转发的是 field.field || field,于是带 flag 的那个对象被丢掉;唯一还带着 flag 的载体(prop)又在 form.tsx:266 被 strip。结果:只有手写 customFields 能用,自动生成字段不行 —— 而自动生成是绝大多数表单的路径。

    三点必须说清楚:

    1. 不是本 PR 造成的,也不是本 PR 能修的。refactor(fields): TextAreaField reads mobile_fullscreen from one source (#3232) #3244 之前 ① 恒 undefined、③ 被剥离、④ 与 ② 同对象,那时同样是断的;本 PR 只删死读取,行为零变化。修点在 packages/components / packages/plugin-form,在我给的 scope fence 之外。
    2. 证据等级被如实标注:静态代码推导 + 精确行号,未做浏览器复现。agent 没有把推导包装成实测,这是对的。
    3. 它需要维护者拍板,所以我标了 needs-user-decision 而不是塞回队列:两条修法都动公共契约,A(生产者把 flag 盖到元数据载体)需要先回答「mobile_fullscreen 是不是字段元数据的一等属性」——那是 spec 层的决定,按分片规则也归主 backlog PM。

    顺带一提,这个特性至今没有任何集成测试,这正是它能一直断着没被发现的原因;#3245 建议落地修之前先补一个跑通 ObjectForm → form.tsx → widget 的用例把现状钉住。


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions