Uh oh!
There was an error while loading. Please reload this page.
fix(fields): 编辑弹窗 datetime/date 字段回显存量值 - #3565
Merged
Merged
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub. |
2 tasks
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
baozhoutao
marked this pull request as ready for review
August 7, 2026 13:27
Uh oh!
There was an error while loading. Please reload this page.
`<input type="datetime-local">` accepts only a zone-less local wall clock (`YYYY-MM-DDTHH:mm`). An ISO instant like `2026-06-17T14:30:00.000Z` — the shape the API returns — is not coerced or warned about: the browser silently rejects it. The attribute still lands in the DOM, so the value looks present to anyone inspecting markup, while `input.value` reads back `""` and the control paints its empty placeholder. Every datetime field in an edit dialog rendered blank while holding a real value, and users read that as "the date was lost" and retyped it. Convert on the way in and back out as a pair (`toDateTimeInputValue` / `fromDateTimeInputValue`). Converting only on read would be the worse bug: the control would then be read as UTC but written back as a naive local string, so a user in UTC+8 who picked 08:33 would store 08:33Z and see 16:33 on the detail page. Read basis and write basis must be the same one. `DateField` had the same gap for ISO-shaped `date` values. It passes a leading `YYYY-MM-DD` through verbatim rather than re-parsing, since a date-only string is parsed as UTC midnight and reading local components back out of it shifts the day everywhere west of Greenwich. Refs objectui#3127. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…get's basis `DateTimeField` became ISO-canonical on both sides in the previous commit — it has to be, because a record's stored value arrives as an ISO instant that the native control silently rejects, so read and write must share one basis or a UTC+8 user picking 08:33 would store `08:33Z`. `ActionParamDialog` renders that same widget (ADR-0059), so the change also moved what a `datetime` action param POSTs, from the control's zone-less local wall clock to an ISO instant — exactly the silent onChange-driven contract drift the render proof added in #2714 exists to catch, and it caught it. Action params have no stored value to read back, so the widget-level basis is invisible there; only the emitted shape matters. Convert at `serializeParamValues` — the dialog's existing widget-value → endpoint-value boundary, already doing this for `file`/`image` fileIds — so the widget stays coherent AND the wire shape stays byte-identical. Weakening the widget instead would put the record form back on two bases; moving action params onto ISO is a contract change of its own and does not belong in a display-bug fix. `toDateTimeInputValue` is exported from `@object-ui/fields` rather than duplicated, so the two surfaces cannot drift on what an instant's local wall clock is. Refs objectui#3127, objectui#2714. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
akarma-synetal pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 10, 2026
…i#3930) * fix(app-shell): POST a zoned ISO instant for datetime action params An action declaring a `type: 'datetime'` param was unusable from the Console: no value a user could pick could pass validation. The dialog renders the param as `datetime-local` (zone-less by nature) and `serializeParamValues` then converted the value back to that control's naive wall clock — the one shape the platform's `datetime` value contract rejects. Since 17.0 the dispatcher validates a params bag against the action's declaration before the handler runs (ADR-0104 D2, `validateActionParams` -> `InstantValueSchema`), so every UI submission earned a 400: "expected an ISO-8601 instant with explicit zone". Renderer and validator wanted disjoint shapes and the app author had no seam between them. The fix is a removal, not a conversion. `DateTimeField` has been ISO-canonical on both sides since objectui#3127/objectstack-ai#3565 — it takes the record's ISO instant in and hands an ISO instant back out, seconds, milliseconds and zone included — so the widget's value already satisfies the contract. objectstack-ai#3565 added the back-conversion to keep the wire shape byte-identical while fixing a display bug and named the follow-up in its own commit message: moving action params onto ISO is a contract change of its own. This is that change. Dropping the branch also makes the boundary idempotent for an already-zoned value (a `+08:00` offset survives byte-for-byte instead of being re-derived and re-cut to the minute) and leaves an empty or unfilled param alone. Deliberately NOT normalized: an authored `defaultValue` written as a zone-less wall clock. That is ambiguous metadata (whose zone?), and coercing it in the renderer would make it "work" in the UI while the identical literal kept 400ing from REST/MCP. It stays loud until the spec validates a param default against the param's own value contract — filed as objectstack#6970, which is not datetime-specific (the same hole lets a `number` param default to 'abc'). The render proof that pinned the old shape was replaced rather than re-spelled: it asserted `2026-07-20T14:30` and was green while the feature was 100% broken, because the shape it pinned is the one shape nothing accepts. It now drives the real widget and asks the real `validateActionParams` — the exact function that produced the 400 — whether the resolved bag is acceptable. Reverse-verified: restoring the deleted branch turns three of the new assertions red with that same 400 message. The datetime assertions hold in every timezone (run under Asia/Shanghai, UTC and America/Los_Angeles), since a zone-shaped test that only holds in UTC goes green on CI while the defect is live for every user east or west of it. The `paramValueShape` sample row is re-spelled but noted as documentation, not a guard: both the old and new strings classify to the same `'string'` tag, so that row could not have caught this. Fixesobjectstack-ai/objectstack#5061 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt * test(app-shell): assert the rejected-shape verdict structurally, not by spec's sentence The "zone-less authored default is still rejected" case pinned `validateActionParams`' full message string, which is owned by `@objectstack/spec` and reachable here only through a versioned dependency. A reword upstream would turn this repo's CI red for a reason that has nothing to do with the renderer. Assert the verdict structurally (`param` + `code`) plus the one phrase that identifies the rule, so the test still says which contract rejected the value without locking a cross-repo string. Refs objectstack-ai/objectstack#5061 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#3127(EHR #864)
缺陷
<input type="datetime-local">只接受不带时区后缀的本地墙钟YYYY-MM-DDTHH:mm。记录从 API 回来是完整 ISO-8601(2026-06-17T14:30:00.000Z)——正好是被拒的形状之一,而浏览器静默拒收:属性照样落进 DOM,input.value读回空串,控件画出空占位。所以每个 datetime 字段在编辑弹窗里都是空框却握着真实值,用户当成「日期丢了」去重新输入 —— 这才是显示 bug 变成数据改动的唯一路径。
DateField在拿到 ISO 形状的date值时有同一个缺口。改法
新增
packages/fields/src/widgets/nativeDateValue.ts,成对转换:toDateTimeInputValue/fromDateTimeInputValue—— 读写必须同基准。只做「读」那一半会更糟:控件按 UTC 读、却按裸本地串写,UTC+8 用户选 08:33 会存成08:33Z,详情页显示 16:33。toDateInputValue—— 开头是YYYY-MM-DD就原样透传,不重新 parse:date-only 串被new Date()按 UTC 午夜解析,再读本地日期分量会让格林威治以西的所有时区整体差一天。表单状态里的形状不变:
datetime走 ISO(API 给的、显示/格式化代码已假定的那个),date保持 plainYYYY-MM-DD(与data-table.tsx内联单元格编辑器、plugin-detail的InlineFieldInput一致)。验证
真机:framework showcase 后端 + 本分支 console dev,对象
showcase_field_zoo/ 记录Specimen — Full(种子f_datetime = 2026-06-17T14:30:00Z)。浏览器时区America/Los_Angeles。attr2026-06-17T07:302026-06-17T14:30:00.000Z.value2026-06-17T07:30✅""← 复现缺陷对照组里
f_date是正常的,只有 datetime 空 —— 说明测法本身有效,差异确实来自本次改动。写回:弹窗里改成本地
2026-06-18 09:15保存 → PATCH bodyf_datetime = "2026-06-18T16:15:00.000Z"(带 Z 的 ISO,非裸本地串),落库一致,详情页显示2026/6/18 上午9:15,无漂移。f_date全程保持 plain2026-06-17。单测:
nativeDateValue.test.ts+datetime-widgets.test.tsx共 23/23 绿,TZ取Asia/Shanghai/America/Los_Angeles/UTC三次都绿;把两个 widget 还原回 HEAD 时 5 条转红(测试确实抓得住这个缺陷)。说明
packages/fields/src/widgets/GridField.tsx:763子表 date 列是value={val != null ? String(val) : ''}配type="date",ISO 形状的值会同样空掉。另立 issue 跟。🤖 Generated with Claude Code