Skip to content

[scheduler] Fix the event dialog moving untouched dates across timezones - #23462

Open
rita-codes wants to merge 18 commits into
mui:masterfrom
rita-codes:scheduler-all-day-data-timezone
Open

[scheduler] Fix the event dialog moving untouched dates across timezones#23462
rita-codes wants to merge 18 commits into
mui:masterfrom
rita-codes:scheduler-all-day-data-timezone

Conversation

@rita-codes

@rita-codes rita-codes commented Aug 31, 2026

Copy link
Copy Markdown
Member

Saving an all-day event from a display timezone different from its data timezone corrupted its stored bounds — even a plain rename. The dialog seeds its date fields from the display-timezone bounds (the days the user actually sees) and resent start/end on every save, so the persisted event was re-anchored to the display zone: a UTC "July 4th" renamed from New York moved to a different day and stretched across two data-zone days.

The fix: the submit now leaves start/end out of the update when none of the range fields was edited, reusing the form store's dirty tracking that custom fields already rely on. Only the keys the submitted range actually reads count: startDate/endDate/allDay for an all-day save (a time typed while all-day was momentarily toggled off must not re-arm the resend), all five for a timed one. An untouched range can no longer move an event in any timezone combination, and dates the user does edit keep their existing display-timezone interpretation — consistent with how the grid renders them. The recurring path benefits for free: an untouched display-zone start no longer enters the pattern-based recurring update, where it could shift a weekly series' BYDAY.

An earlier iteration of this PR anchored the all-day computeRange bounds in the event's data timezone instead; that could not work, because the form is seeded with display-zone day strings — the anchor faithfully persisted the wrong day. That approach is fully reverted. RANGE_FORM_KEYS now lives next to computeRange as the single copy of the key list (the placeholder keys derive from it), and getDirtyValues drops its now-unused excludeKeys parameter.

Covered by dialog-level tests: rename-only saves keep the stored dates byte-identical for all-day (both timezone directions, single- and multi-day) and timed events, the orphaned-time toggle case stays untouched, a genuine day edit still applies in the display timezone, a partial range edit submits the whole displayed range, and a rename of a cross-timezone weekly series completes the scope flow end-to-end without moving its dates or BYDAY. Found while testing the auto-scheduling engine on #23439, which documents the pre-fix behavior — its mirrored tests can be simplified once this lands.

A second review pass surfaced a sibling bug on the recurring scope paths: the occurrence handed to updateRecurringEvent/deleteRecurringEvent was identified by its display-timezone start, which for a cross-timezone all-day occurrence is day-normalized to a different instant — so "Only this event" excluded the wrong day and detached the occurrence onto it. All call sites (dialog, toolbar, context menu, drag) now pass the occurrence's data-timezone start, the identity the recurrence expansion keys on. Covered by a scope-flow test that pins the exception date and the detached event on the event's own day.

A third review pass hardened the identity change itself: the armed-occurrence snapshot (setEditingOccurrenceTimes, repointEditingOccurrence) now keeps the data-timezone bounds in sync (a follow-up edit or delete of a resized/moved armed occurrence no longer targets the pre-change occurrence), a rename-only scope change still re-points the armed occurrence onto the moved-to event, and the grid event primitives thread dataStart/dataEnd into the drag data so the drag path carries the real occurrence identity instead of one fabricated from the display segment bounds. The occurrenceStart parameters now document the data-timezone contract, and the four 'dataTimezone' in occurrence checks share an isEventOccurrence guard. All of it is pinned red/green: store-level identity-sync and rename-repoint tests, cross-timezone delete tests for the dialog, context menu, and toolbar, a scope-all edited-day pin, and generateOccurrenceFromEvent units.

A fourth pass closed the last corner of the armed-state sync: the rename fallback fed display bounds into the identity sync, relabeling a cross-timezone all-day armed occurrence onto its display day. setEditingOccurrenceTimes/repointEditingOccurrence now accept explicit data bounds — a rename keeps the occurrence's own identity, a genuine time change relabels the new instants. Pinned red/green at the store level (cross-timezone rename repoint) and end-to-end with a cross-timezone timeline drag test that discriminates the dataStart threading through the grid primitives.

The dialog built all-day bounds with the display timezone, so saving an
all-day event viewed from another timezone — even a plain rename —
shifted its stored day boundaries and stretched it across two data-zone
days. computeRange now takes the all-day anchor timezone and the submit
path passes the event's data timezone for edits; validation and the
placeholder keep the display timezone, where ordering and rendering
happen. Creations still anchor to the display timezone (the event has no
timezone of its own yet).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
@rita-codes rita-codes added type: bug It doesn't behave as expected. scope: scheduler Changes related to the scheduler. labels Aug 31, 2026
@rita-codes rita-codes self-assigned this Aug 31, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Aug 31, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-23462--material-ui-x.netlify.app/
QR code for https://deploy-preview-23462--material-ui-x.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 🔺+713B(+0.18%) 🔺+291B(+0.27%)
@mui/x-scheduler-premium 🔺+865B(+0.16%) 🔺+338B(+0.22%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

rita-codes and others added 4 commits August 31, 2026 12:23
The submit read the creation placeholder twice, and the data-timezone
lookup inlined the processed-event walk; a `dataTimezone` selector now
carries the lookup and the 'default' fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
The form seeds its date fields from the display-timezone bounds, so resending
them untouched re-anchors the event to the display zone: viewed from another
timezone, an all-day event moved to a different day on a mere rename. Anchoring
the submit in the data timezone (previous approach) could not fix this — it
faithfully persisted the display day the form was seeded with.

Instead, leave start/end out of the update when none of the range fields is
dirty. This also keeps an untouched range out of the pattern-based recurring
update, where a display-zone start could shift a weekly series' BYDAY.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
@rita-codes rita-codes changed the title [scheduler] Anchor all-day dialog bounds to the event's own timezone [scheduler] Fix the event dialog moving untouched dates across timezones Aug 31, 2026
rita-codes and others added 2 commits August 31, 2026 13:29
…e2e pin

A time typed while all-day was toggled off stayed dirty after toggling it back
on, re-arming the range resend and reintroducing the cross-timezone day shift.
Only the keys the submitted range actually reads count now.

Also: RANGE_FORM_KEYS moves next to computeRange as the single copy of the list
(the placeholder keys derive from it), getDirtyValues drops its now-unused
excludeKeys parameter, the recurring rename test completes the scope flow on a
cross-timezone weekly series, the touch-resize test pins the seeded end time,
and a test pins the whole-displayed-range semantics of a partial edit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
The recurring scope paths (update, delete, drag) identified the occurrence by
its display-timezone start. For an all-day occurrence viewed from another
timezone those bounds are day-normalized to a different instant, so the
exception date and the detached event landed on the wrong data-zone day. The
occurrence's own data-timezone start is the identity the expansion keys on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
rita-codes and others added 2 commits August 31, 2026 14:41
The data-timezone bounds are now the occurrence identity for recurring scope
operations, so every holder must maintain them:

- setEditingOccurrenceTimes and repointEditingOccurrence sync dataTimezone
  alongside the display bounds, so a follow-up edit or delete of the armed
  occurrence targets the post-resize/post-move occurrence.
- A rename-only scope change (no start/end in the changes) still re-points the
  armed occurrence onto the moved-to event, keeping its current bounds.
- The grid event primitives take dataStart/dataEnd so drag data carries the real
  occurrence identity instead of one fabricated from the display segment bounds.
- The 'dataTimezone' in occurrence checks share an isEventOccurrence guard, the
  RangeFormKey type derives from RANGE_FORM_KEYS, and the occurrenceStart
  parameters document the data-timezone contract.

New coverage: store-level identity-sync and rename-repoint tests, cross-timezone
delete pins for the dialog, context menu, and toolbar, a scope-'all' edited-day
pin, the all-day-to-timed conversion, and generateOccurrenceFromEvent units.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
The rename fallback fed the display bounds into the data-timezone sync, so a
rename-only scope change on a cross-timezone all-day armed occurrence relabeled
its identity onto the display day — the repoint keyed off the wrong day and a
follow-up delete excluded it. The fallback now threads the occurrence's own
data bounds through setEditingOccurrenceTimes and repointEditingOccurrence,
which accept explicit data bounds and only fall back to the instant-preserving
relabel for genuine time changes.

Also: an end-to-end cross-timezone drag test discriminates the dataStart
threading through the grid primitives, the scope-'all' pin asserts the end day
and the BYDAY, the exDate asserts parse through the adapter, the wrappers
compute the data bounds once, and the scope-sync comments say 'change' instead
of 'resize' now that renames flow through too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDEgka4iDRFrenWeWFhunq
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

…-data-timezone

# Conflicts:
#	packages/x-scheduler/src/internals/components/event-dialog/EventDialog.test.tsx
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 31, 2026
rita-codes and others added 6 commits August 31, 2026 18:10
…he pattern math

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC6LfHbZXD549R8qtmQojJ
…-'all' update

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC6LfHbZXD549R8qtmQojJ
…e drag, and the pending-validation untouched range

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC6LfHbZXD549R8qtmQojJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: scheduler Changes related to the scheduler. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant