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.

[fields,plugin-form] 内嵌子表把 datetime/time 列折叠成 date 控件:时间分量在编辑时被静默丢弃 #3569

Closed

Description

@baozhoutao

现象(读码 + 真机确认接口形状)

packages/plugin-form/src/deriveMasterDetail.ts 的 fieldTypeToColumnType 把三种字段类型
折叠成同一个网格列类型:

case 'date':
case 'datetime':
case 'time':
  return 'date';

而 GridField 对 type === 'date' 只会渲染 input[type=date](GridField.tsx:764)。于是
一个 datetime 子表字段在内嵌网格里:

  1. 读:控件只能显示日历日,时间分量在界面上不存在;
  2. 写:onChange 交出的是控件自己的 YYYY-MM-DD,时间被静默丢弃 —— 用户只是碰了一下
    日期,记录里的 14:30 就没了;
  3. time 类型同理,被喂进一个日期控件。

真机取证(framework showcase,本地 objectstack dev):date 字段接口回 "2026-08-04",
datetime 字段接口回 "2026-08-07T16:39:20.389Z" —— 即 datetime 列拿到的一定是完整 ISO。

为什么现在才浮出来

#3566 修的是 date 列把 ISO 形状喂进 input[type=date] 导致单元格空白。那条修复顺带让
datetime 列从「空白」变成「显示日历日」,不再更差,但也没变对 —— 真正的缺口是类型折叠本身。

预期

datetime 列应渲染 input[type=datetime-local],读写各用 toDateTimeInputValue /
fromDateTimeInputValue(packages/fields/src/widgets/nativeDateValue.ts,#3127 已落地并配对
使用),与 DateTimeField 的约定一致;time 列同理需要自己的形态,而不是继续借用日期控件。

建议改法(跨两个包,故未并进 #3566)

  • packages/fields/src/widgets/GridField.tsx:GridColumn['type'] 联合类型加 'datetime',
    单元格按类型选择控件与读写适配器。
  • packages/plugin-form/src/deriveMasterDetail.ts:fieldTypeToColumnType 停止把 datetime
    折叠到 date。
  • 只读展示面同一处缺口:GridField.tsx 的 displayText()(:277 的 String(value) 兜底)和
    只读表格 :632 的 String(row[c.field]),对 date/datetime 列都会把原样 ISO 字符串印到界面上
    (例如 2026-06-17T00:00:00.000Z)。修 datetime 列时应一并处理 —— 之所以不能在 [fields] GridField 子表 date 列不回显 ISO 形状的值:String(val) 直喂 input[type=date],同 #3127 的同族缺口 #3566 里单独
    改,正是因为在类型折叠没解开之前,GridField 无从分辨该显示「日」还是「日 + 时间」,按日渲染
    会把 datetime 的时间分量从展示里抹掉。

出处

#3566(GridField date 列不回显 ISO 值)实施时按范围纪律排除的同族面。

Activity

  1. self-assigned this
    on Aug 8, 2026
  2. baozhoutao commented on Aug 8, 2026

    @baozhoutao
    ContributorAuthor

    Claim: PM loop round 2
    Branch (objectui): claude/issue-3569-grid-datetime-column
    Branch (framework): claude/issue-3569-showcase-datetime-fixture
    Worktrees: objectui-issue-3569 / framework-issue-3569

    与 #3566 的关系:栈式,不并行

    本单要改的正是 #3566 刚改过的那一行(GridField.tsx 的单元格 value=),两者文件不相交是不成立的。#3566 的 PR #3718 已开(draft,待合并),因此本单分支基于 claude/issue-3566-gridfield-date-input 开,diff 只含本单改动;#3718 并入 main 后 GitHub 自动 retarget。开发 agent 只读不写那条分支。

    跨仓:objectui 修复 + framework 夹具

    已核实 —— showcase 里没有任何内嵌网格子对象带 datetime/time 字段:

    • showcase_expense_line(inlineEdit: 'grid'):merchant / category / amount / billable / status / incurred_on(date)
    • showcase_invoice_line(inlineEdit: 'grid'):position / product / description / quantity / unit_price / receipt / amount

    也就是说本缺陷当前在 showcase 上无法真机复现,而本仓约定要求夹具长期留在 examples/app-showcase 里。所以本单落两个仓:objectui 出修复,framework 出一条永久夹具(给某个 grid 子对象加 datetime 字段并种入真实 ISO 值)。

    好消息是这条不需要 #3566 那种改写代理:已实测该后端不规整 datetime,接口原样回 2026-08-07T16:39:20.389Z,夹具建好即可直接复现。相关但不构成阻塞:objectstack-ai/objectstack#6328(那条讲的是 date 字段读写两侧都被规整)。

    判据

    datetime 子表列必须渲染 input[type=datetime-local],读写成对走 toDateTimeInputValue / fromDateTimeInputValue;可证伪点是时间分量:改一次日期后,记录里的 14:30 必须还在。


    Generated by Claude Code

  3. baozhoutao commented on Aug 8, 2026

    @baozhoutao
    ContributorAuthor

    Review: PM loop round 2 — 两仓均已实现并推送,PR 待维护者确认后开。

    改动(亲核 diff)

    objectui 5 文件 / +303 −14,framework 2 文件 / +38 −13。无截图、无临时脚本混入。

    fieldTypeToColumnType 停止三合一,GridColumn['type'] 加 'datetime' / 'time',各自配控件与适配器:

    • datetime → input[type=datetime-local],读走 toDateTimeInputValue、写在 coerce() 里走 fromDateTimeInputValue —— 成对使用,读写同基准,与 DateTimeField 同约定;
    • time → input[type=time],HH:mm[:ss] 原样往返;
    • date 不变,仍保持 YYYY-MM-DD 原样透传(差一天的防线没被动)。

    只读面按 issue 要求一并修了:新 temporalText() 撑起 displayText(),只读表的 String(row[c.field]) 对三种时间类型改走它,其余列类型路径不动。

    一个值得点名的连带修正:TYPE_FILL_PRIORITY 也补了 datetime: 3 / time: 3。不补的话两个新类型会静默掉进 unknown 档(5),被挤出内联列预算 —— 这是拆类型时很容易漏的次生效应。

    changeset:minor,理由成立

    @object-ui/fields + @object-ui/plugin-form 各 minor。虽是修复,但 GridColumn['type'] 是导出联合类型,作者显式写 columns 时现在可以声明 type: 'datetime' / 'time' —— 以前是类型错误,属新增可授权面,所以不是纯 patch。未声明 major(check-changeset-no-major.mjs 已跑过)。framework 侧不写:@objectstack/example-showcase 是 private,不在 fixed 组。

    证据

    单测:packages/fields/ packages/plugin-form/ → 97 files / 1304 tests passed;type-check 13/13;lint 0 errors。新增 8 条 GridField 测 + 1 条 fieldTypeToColumnType 用例,并删掉了那条已变假的 expect(fieldTypeToColumnType('datetime')).toBe('date')(而不是留着)。墙钟期望值在运行时由 Date 现算、日期位移按本地分量做,所以不吃时区/夏令时。

    framework:pnpm validate → ✓ Validation passed,pnpm typecheck 干净,app-showcase 13 files / 146 tests passed。

    真机(后端 :4310 --fresh + console :5310,注入 API token 登录、未键入密码;EXP-2003,本地 PDT = UTC−7):

    控件 显示 改「日」后落库
    BEFORE(还原到基底) input[type=date] 2026-08-05,14:30 在界面上不存在 2026-08-06T00:00:00.000Z ← 时间被抹掉
    AFTER input[type=datetime-local] 2026-08-05T07:30 2026-08-06T14:30:00.000Z ← 时间保留
    • 落库是 r.incurred_at === new Date(r.incurred_at).toISOString() → true,即 fromDateTimeInputValue 的 ISO,不是裸本地串。
    • 时区漂移显式排除:14:30Z→07:30 本地、08:05Z→01:05 本地,双向都恰好 UTC−7;往返只移动了刻意的那一天。
    • 共存取证:从列选择器调出 incurred_on 后,同一网格里 Incurred At 是 datetime-local、Incurred On 是 date,两者并存正确。全页 innerText 未出现任何裸 ISO。

    一处如实说明:CDP 合成击键进不了原生日期控件的内部分段编辑器,所以单元格改值是用原生 HTMLInputElement value setter + 冒泡 input/change 事件驱动的(与真实击键走同一条 React onChange)。点「编辑」、开列选择器、点「更新」、网络往返都是真实交互,且以上断言全部回读自 API/DB 而非 DOM。

    需要维护者知悉的两点

    1. time 控件有单测、无真机夹具 —— 已报 [showcase] 内嵌子表 time 列没有真机夹具:两个 inlineEdit 网格都无自然落脚点,且都已顶到 6 列预算 objectstack#6533(未认领、未加任何 pm:* 标签)。原因是成本而非省事:两个 inlineEdit: 'grid' 子对象都已顶到 6 列默认预算,加 time 列会把 receipt([fields] Field.file 在 inline grid 退化为文本框、无法上传,且被自动子表列排除 #2360 的 file-in-grid 夹具)挤进列选择器。控件本身仍随本单实现,因为留一半会让后续单重写同几行。
    2. showcase 可见变化:expense_line 现有 7 个可编辑字段对 6 列预算,incurred_on 按声明顺序落入列选择器(实测是「可选列」里唯一一条)。刻意让 datetime 占住可见位,已在对象注释里写明。

    Generated by Claude Code

  4. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Release board: target:v17 applied — class ①, the embedded sub-table folds datetime/time columns into a date control, so touching the date on an existing row silently discards the stored time component on save: user-visible data loss on a shipped editing surface. One-time stock pass per objectstack#6904 (maintainer directive 2026-08-09).


    Generated by Claude Code

  5. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    Contributor

    Delegated GO — PRs opened (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3).

    Authorization: maintainer directive (2026-08-10, this seat's session) —「还是优先v17,需要人决策的你先看看能不能站在产品长远稳定角度代为决策」. The open decision on this card was exactly the PR go-ahead the round-2 review left gated, so it falls squarely under the delegation. Veto window: open until merge — say the word and both PRs close, branches kept.

    Decision basis, from the product-stability lens: class ① user-visible data loss on a shipped editing surface; both halves implemented and real-browser verified two days ago with timezone drift explicitly excluded (UTC−7 both directions), 1304 unit tests green, falsification-first test discipline; the stacking base #3718 merged to main on 08-08 so the branch is now a pure single-card delta. Holding a verified data-loss fix out of the release window has no stability upside.

    The known-and-filed gap (time control has unit tests but no live fixture — objectstack#6533) stays open and non-blocking, per the round-2 review.


    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

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions