Skip to content

fix(timeline): bind the timeline to the date axis the view declares (#3129) - #4072

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3129-timeline-startdatefield
Aug 10, 2026
Merged

fix(timeline): bind the timeline to the date axis the view declares (#3129)#4072
yinlianghui merged 1 commit into
mainfrom
claude/issue-3129-timeline-startdatefield

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #3129 — deliberately not Fixes. See "Premise verification" below: the card's literal premise no longer reproduces, and the re-run against the published console that triage asked for is still outstanding, so this should not auto-close the card.

Premise verification (the issue body is a lead, not a spec)

The card says the timeline ignores the view's declared timeline.startDateField. On current origin/main that is no longer reproducible. A probe drove ListView into the real ObjectTimeline and read the bucket headers for every authoring shape:

shape bucket
timeline: { startDateField, endDateField } (the card's shape) Later
options.timeline: { startDateField, endDateField } (what app-shell emits) Later
options.timeline: { dateField } (legacy alias) Later
timeline: { startDateField, scale: 'month' } Later
options.calendar: { startDateField }, rendered as timeline No date

The declared-key path was fixed by PR #3175 (5cb75b3, 2026-08-02) — one day after this card was filed and four days before triage. Triage on 2026-08-06 re-verified that startDateField consumers were alive, not that the defect still reproduced, and explicitly asked for a repro re-run before dispatch.

The defect that does reproduce

The last row above is a live defect, and it is exactly the shape the report isolated: "the same start_date / end_date fields render correctly in the Calendar and Gantt views". A view whose date axis is bound under calendar is offered the Timeline visualization and then buckets every record into "No date", while the calendar renders the very same field. Two read-sites disagreed about what counts as a timeline binding:

  • ListView's capability gate accepted options.calendar.startDateField as a timeline-resolvable axis. The render branch never read calendar config at all, so it fell through to its created_at last resort.
  • app-shell's object page emitted startDateField: 'due_date' into options.timeline for every object view, declared or not. Downstream that is indistinguishable from a real binding, and because it is always present it shadowed the fallback entirely — which is why the calendar-bound case cannot be fixed in ListView alone.

Treating a calendar binding as a timeline axis is not a lenient fallback added here: the capability gate has always said so, and InterfaceListPage derives a timeline's default binding from the very same defaultCalendarFromObject helper it uses for calendars. What was missing is that one of the two read-sites never honoured the promise the other made.

Change

  • packages/plugin-list/src/ListView.tsx — new resolveTimelineDateBinding(schema), the single resolution consumed by both the capability gate and the timeline render branch. Spec key before legacy alias, timeline before calendar, both nestings at each level. The resolved axis is written onto the nested config as well as the flat prop, because ObjectTimeline prefers timeline.startDateField and app-shell emits a timeline config object for every object view.
  • packages/app-shell/src/views/ObjectView.tsx — new exported timelineViewOptions(viewDef, objectDef); the object page now forwards only what the view declared, plus the object's titleField (the one thing this layer knows and ListView does not). The fabricated due_date is gone.

Answering the triage warning: Calendar, Gantt and Timeline each carry their own resolution, in three files (ListView.tsx, app-shell ObjectView.tsx, plugin-view ObjectView.tsx) — there is no shared resolver to wire into. No fourth copy was written: this collapses the two timeline read-sites into one and removes the app-shell copy outright. Calendar and Gantt were left untouched (out of scope); noted on the issue.

Both directions pinned

  • declared timeline.startDateField still wins over a calendar one;
  • a view declaring no date axis anywhere keeps today's created_at fallback, and nothing is invented from the object.

Reverse verification (direction predicted before running, both observed)

  • Drop calendar from the resolver's source list: 5 red, including the end-to-end bucket assertion expected [ 'No date' ] to deeply equal [ 'Later' ].
  • Restore app-shell's startDateField: … || 'due_date': exactly the two "invents NO date field" cases go red (expected 'due_date' to be undefined); everything else stays green, because the fabricated value is only observable when the view declared nothing.

Tests

  • packages/plugin-list + packages/plugin-timeline: 29 files, 424 passed.
  • packages/app-shell: 314 files, 2929 passed, 1 skipped.
  • type-check: 31 tasks successful. lint: 0 errors.

Changeset: .changeset/timeline-binds-to-the-calendar-date-axis.md (minor — records move out of "No date" into real date buckets).


Generated by Claude Code

…3129)

A view whose date axis is bound under `calendar` was OFFERED the Timeline
visualization and then bucketed every record into "No date" — while the
calendar rendered the very same field correctly. Two read-sites disagreed
about what counts as a timeline binding:

- ListView's capability gate accepted `options.calendar.startDateField` as a
  timeline-resolvable axis; the render branch never read calendar config at
  all, so it fell through to its `created_at` last resort.
- app-shell's object page emitted `startDateField: 'due_date'` into
  `options.timeline` for EVERY object view, declared or not — downstream
  indistinguishable from a real binding, and always present, so it shadowed
  the fallback entirely.

ListView now resolves the axis once (`resolveTimelineDateBinding`, consumed by
both the gate and the render branch, spec key before legacy alias and
`timeline` before `calendar` in both nestings), and the object page forwards
only what the view declared plus the object's titleField.

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

vercel Bot commented Aug 10, 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 10, 2026 4:06am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
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) 483.72KB 106.71KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 228.51KB 56.69KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.84KB 10.80KB
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.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.49KB 17.48KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.50KB 30.66KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 237.80KB 59.48KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 113.81KB 27.53KB
plugin-gantt (index.js) 162.79KB 39.67KB
plugin-grid (index.js) 187.97KB 49.79KB
plugin-kanban (index.js) 48.53KB 13.38KB
plugin-list (index.js) 109.96KB 26.64KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 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.71KB 7.95KB
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 (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) 2.71KB 1.34KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 10, 2026 04:17
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit bd863fe Aug 10, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3129-timeline-startdatefield branch August 10, 2026 04:17
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.

2 participants