Skip to content

fix(fields): 编辑弹窗 datetime/date 字段回显存量值 - #3565

Merged
baozhoutao merged 2 commits into
mainfrom
claude/framework-showcase-ui-test-d6c417
Aug 7, 2026
Merged

fix(fields): 编辑弹窗 datetime/date 字段回显存量值#3565
baozhoutao merged 2 commits into
mainfrom
claude/framework-showcase-ui-test-d6c417

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#3127(EHR #864

缺陷

<input type="datetime-local"> 只接受不带时区后缀的本地墙钟YYYY-MM-DDTHH:mm。记录从 API 回来是完整 ISO-8601(2026-06-17T14:30:00.000Z)——正好是被拒的形状之一,而浏览器静默拒收:属性照样落进 DOM,input.value 读回空串,控件画出空占位。

el.getAttribute('value')// "2026-06-17T14:30:00.000Z" ← 记录里的值el.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 保持 plain YYYY-MM-DD(与 data-table.tsx 内联单元格编辑器、plugin-detailInlineFieldInput 一致)。

验证

真机:framework showcase 后端 + 本分支 console dev,对象 showcase_field_zoo / 记录 Specimen — Full(种子 f_datetime = 2026-06-17T14:30:00Z)。浏览器时区 America/Los_Angeles

本分支对照组(预构建 console,无修复)
attr2026-06-17T07:302026-06-17T14:30:00.000Z
.value2026-06-17T07:30"" ← 复现缺陷

对照组里 f_date 是正常的,只有 datetime 空 —— 说明测法本身有效,差异确实来自本次改动。

写回:弹窗里改成本地 2026-06-18 09:15 保存 → PATCH body f_datetime = "2026-06-18T16:15:00.000Z"(带 Z 的 ISO,非裸本地串),落库一致,详情页显示 2026/6/18 上午9:15,无漂移。f_date 全程保持 plain 2026-06-17

单测nativeDateValue.test.ts + datetime-widgets.test.tsx23/23 绿TZAsia/Shanghai / America/Los_Angeles / UTC 三次都绿;把两个 widget 还原回 HEAD 时 5 条转红(测试确实抓得住这个缺陷)。

说明

  • 纯 bug 修复,按 AGENTS.md 不带 changeset。
  • 排查中读码确认了一处同族缺口,未并进本单packages/fields/src/widgets/GridField.tsx:763 子表 date 列是 value={val != null ? String(val) : ''}type="date",ISO 形状的值会同样空掉。另立 issue 跟。

🤖 Generated with Claude Code

@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 7, 2026 1:15pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-E_nd4JPA.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)480.72KB105.64KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)137.51KB35.11KB
fields (index.js)230.78KB56.80KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.03KB28.88KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)186.61KB49.34KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DpScGiC0.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)480.72KB105.64KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)137.51KB35.11KB
fields (index.js)230.87KB56.83KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.03KB28.88KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)186.61KB49.34KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@baozhoutao
baozhoutao marked this pull request as ready for review August 7, 2026 13:27
@baozhoutao
baozhoutao added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit b785a77Aug 7, 2026
18 checks passed
@baozhoutao
baozhoutao deleted the claude/framework-showcase-ui-test-d6c417 branch August 7, 2026 13:30
baozhoutaoand others added 2 commits August 7, 2026 08:57
`<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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant

@baozhoutao