Skip to content

fix(plugin-calendar): authored ISO currentDate is parsed to a Date instead of crashing the calendar (#4452) - #4484

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4452-calendar-currentdate-iso
Aug 12, 2026
Merged

fix(plugin-calendar): authored ISO currentDate is parsed to a Date instead of crashing the calendar (#4452)#4484
yinlianghui merged 1 commit into
mainfrom
claude/issue-4452-calendar-currentdate-iso

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Closes #4452

The defect

plugin-calendar:calendar-view declares the input { name: 'currentDate', type: 'string', label: 'Current Date', description: 'ISO date string for initial calendar date' }, while CalendarViewProps.currentDate is a Date. Nothing converted between the two. The authored string rode the renderer's trailing {...props} spread into CalendarView, became useState's initial selectedDate, and the header called selectedDate.toLocaleDateString(…) on a string:

Component "plugin-calendar:calendar-view" failed to render
selectedDate.toLocaleDateString is not a function

The one spelling the input documents was the one spelling that could not work, and there was no correct authored value at all — type: 'string' cannot express a Date.

This is the mirror image of #4433, in the same renderer: there an undocumented authored key overwrote a computed prop; here the component documents the key, its type and its format, and honouring that documentation is what took the calendar down.

The fix

Per the ruling on #4452 — authored metadata is the contract, so the renderer owes the conversion — all of it inside packages/plugin-calendar/src/calendar-view-renderer.tsx:

  1. currentDate is destructured out of the incoming props, so the spread can no longer carry the raw value (the plugin-calendar: authoring events on a calendar-view node crashes the component — the SDUI action object overwrites the computed CalendarEvent array #4433 consumed-key pattern). Both authoring channels land there: the node's own key and a props: { currentDate } container.
  2. One resolver, one answer, at the boundary:
    • a parseable string becomes the Date the prop type declares;
    • an unparseable string — or any non-string that is not already a Date — gets the same answer as an absent key: undefined, so CalendarView's own default parameter applies. An Invalid Date is never manufactured and passed on; it does not throw, it renders the literal text "Invalid Date" into the header and the date picker, which is a silent wrong answer where the default is a usable calendar;
    • a Date instance passes through untouched. That is not authored metadata (type: 'string' cannot express it) but a React host handing the widget its real declared prop type, and its behaviour is unchanged by this card, invalid instances included.
  3. The parse is memoised on the authored value. CalendarView re-seeds its selectedDate state from this prop in an effect keyed on the prop's identity, so a fresh Date per render would re-seed on every render — throwing away the user's own Previous/Next navigation and driving the effect against its own setState. A dedicated case pins this.

No change to CalendarView, to the registry declaration, or to any other package.

Red first

The tests were written and run before the source change. 5 red, 3 green, exactly the predicted split — the three green ones are the must-not-change pins:

 × renders the calendar for the card's verbatim repro node, not the error boundary
 × honours the authored ISO date instead of falling back to today
 × honours an authored ISO date written in the `props` container
 × keeps user navigation from the authored date (the parsed Date must be stable across renders)
 × treats an unparseable authored string as ABSENT — default date, no crash, no Invalid Date
 Test Files  1 failed (1)
      Tests  5 failed | 3 passed (8)

with the card's own crash verbatim in the DOM the assertion dumped:

Component "plugin-calendar:calendar-view" failed to render
selectedDate.toLocaleDateString is not a function

After the fix: Tests 8 passed (8); the whole package Tests 31 passed (31).

One honest note on the repro date

The card's verbatim repro value, 2026-08-12T08:00:00.000Z, is in the month this card was filed in, so it can pin the crash and nothing more: a fix that parsed the string and then quietly dropped it to "today" would still satisfy it. The honouring assertions therefore use a date 14 months out. 14 is more than 12, so the authored month is always in a different calendar year than today — which lets the header assertion read year digits rather than a locale-dependent month name — and the event-visibility assertion is the locale-free half of the same fact, since a month grid only renders the events of the month it is showing. Both dates are in the suite, doing their two different jobs.

Verification

  • pnpm exec vitest run packages/plugin-calendar/ — 6 files, 31 tests, green.
  • pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx — 39 tests green, unchanged, no ledger edit. The sweep plants bind / events / ariaLabel / ariaDescribedBy / zzcanary* / reference_to / props: { colorVariant, zzcanaryprop }, and no currentDate canary, so the calendar targets' measured leak set does not move: still zero leaked attributes and still no plugin-calendar row in LEAK_LEDGER. The ledger file was not touched.
  • tsc --noEmit and tsc -p tsconfig.test.json for packages/plugin-calendar — both green.
  • pnpm --filter @object-ui/plugin-calendar lint — 0 errors.
  • check-changeset-presence, check-changeset-no-major, check-control-bytes, check-type-check-coverage — green.
  • Grading evidence: the package was built before and after the change and the emitted dist/*.d.ts are byte-identical (diff -r clean). Runtime-only, so patch — the escalation rule of fix(i18n): retire the reader-less common.search key from the ten locale packs (#4392) #4403/fix(i18n): retire the orphaned report.editor.* namespace, 105 keys across ten packs (#4145) #4177 does not trigger.

Relation to #4425 / #4453

Consuming this declared input is a forward-compatible piece of the #4425 phase-2 whitelist migration tracked for this package as #4453. The key stops being an unread value that rides the spread and becomes a declared input the renderer actually reads — which is the shape phase 2 will require of every declared input, so this conversion survives that migration rather than being redone by it. This PR changes no widget contract and no whitelist; that decision stays with #4425.


Generated by Claude Code

…at the renderer boundary (#4452)

`plugin-calendar:calendar-view` declares `currentDate` as `type: 'string'`
("ISO date string for initial calendar date") while
`CalendarViewProps.currentDate` is a `Date`. Nothing converted between them:
the authored string rode the trailing `{...props}` spread into `useState`'s
initial `selectedDate`, and the header's `selectedDate.toLocaleDateString(…)`
threw `selectedDate.toLocaleDateString is not a function`. The one spelling
the input documents was the one spelling that could not work.

The renderer now owes the conversion at its own boundary: `currentDate` is
destructured out so the spread cannot carry the raw value (the #4433
consumed-key pattern), parsed once per authored value, and handed to
`CalendarView` as the `Date` its prop type declares. Off-spec input gets the
same answer as an absent key — the component default — and an `Invalid Date`
is never manufactured and passed on. A `Date` instance passes through
untouched. The parse is memoised on the authored value, because
`CalendarView` re-seeds its `selectedDate` from this prop in an effect keyed
on the prop's identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 12, 2026 7:19pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-Sw0ReJDD.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.32KB 108.45KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 153.42KB 41.19KB
fields (index.js) 228.99KB 56.82KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 45.46KB 12.52KB
plugin-charts (index.js) 62.01KB 17.63KB
plugin-chatbot (index.js) 181.17KB 43.03KB
plugin-dashboard (index.js) 120.75KB 31.38KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.03KB 59.77KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 188.13KB 50.00KB
plugin-kanban (index.js) 48.62KB 13.42KB
plugin-list (index.js) 110.20KB 26.79KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.99KB 10.74KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

ACCEPT — step-7 复核 by PM session session_017Qqyix2QcnpUC9XeYVDzx3 (focused review).

  • Red-first verbatim with the card's own crash DOM; 5 red / 3 must-not-change green exactly as predicted.
  • The deviation-by-addition is the review's highlight: the card's repro date sits in today's own month, so it cannot prove honouring — a parse-then-drop-to-today fix would have passed. The 14-months-out second date (always a different year, locale-free assertions) closes that hole. Accepted as the stronger reading of the ruling.
  • The memoised parse is a load-bearing addition correctly identified as such — an unstable Date identity would have re-seeded the selection every render (snap-back + update-depth loop), making the fix worse than the bug; it is pinned by its own red case. The props-container channel extension is the same defect through the second spread path — single destructure, correctly in scope.
  • The Invalid-Date-INSTANCE boundary reading (host's own value passes through; only authored STRINGS parse-or-abstain) is the minimal contract change and matches the dispatch's structure — accepted; one line to reverse if the maintainer wants the wider reading.
  • Sweep untouched as ruled, with the right observation surfaced: currentDate is now SAFE TO PLANT as a calendar canary (it was withheld for the same crash-class reason as events pre-plugin-calendar: authoring events on a calendar-view node crashes the component — the SDUI action object overwrites the computed CalendarEvent array #4433) — recorded for the plugin-calendar: an authored onEventClick reaches CalendarView through the spread and throws an UNCAUGHT error on click — needs a contract decision, not a strip #4453 dispatch brief, not this card.
  • .d.ts byte-identical → patch stands. CI 18/18 green. Toolchain note (check-control-bytes is the objectui spelling) absorbed into the dispatch template.

Flipping ready + arming auto-merge.


Generated by Claude Code


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-calendar: authoring currentDate as the ISO string its own registry input documents crashes calendar-view

2 participants