-
Notifications
You must be signed in to change notification settings - Fork 411
fix(calendar): respect configured timezone #2427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9cf011c
258a71e
5948a44
8f36f38
2c3a6eb
1b27138
ecf0bdc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,10 +2,13 @@ import { defineAction } from "@agent-native/core"; | |
| import { readAppState } from "@agent-native/core/application-state"; | ||
| import { getRequestUserEmail } from "@agent-native/core/server"; | ||
| import { accessFilter } from "@agent-native/core/sharing"; | ||
| import { addDays, parseISO, startOfWeek } from "date-fns"; | ||
| import { fromZonedTime, toZonedTime } from "date-fns-tz"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { getDb, schema } from "../server/db/index.js"; | ||
| import { rowToBookingLink } from "../server/lib/booking-link-utils.js"; | ||
| import { getCalendarTimezone } from "../server/lib/calendar-settings.js"; | ||
| import type { CalendarEvent, CalendarEventDraft } from "../shared/api.js"; | ||
| import { | ||
| CALENDAR_VIEW_PREFERENCES_KEY, | ||
|
|
@@ -67,18 +70,18 @@ export default defineAction({ | |
| const nav = navigation as any; | ||
|
|
||
| if (nav?.view === "calendar" || !nav?.view) { | ||
| const now = new Date(); | ||
| const viewDate = nav?.date ? new Date(nav.date) : now; | ||
|
|
||
| const from = new Date(viewDate); | ||
| from.setDate(from.getDate() - from.getDay()); | ||
| from.setHours(0, 0, 0, 0); | ||
| const to = new Date(from); | ||
| to.setDate(to.getDate() + 7); | ||
| const email = getRequestUserEmail(); | ||
| if (!email) throw new Error("no authenticated user"); | ||
| const timezone = await getCalendarTimezone(email); | ||
| const viewDate = nav?.date | ||
| ? parseISO(nav.date) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Parse navigation dates in the configured timezone
|
||
| : toZonedTime(new Date(), timezone); | ||
| const from = startOfWeek(viewDate); | ||
| const to = addDays(from, 7); | ||
|
|
||
| const eventResult = await fetchEventsForRange( | ||
| from.toISOString(), | ||
| to.toISOString(), | ||
| fromZonedTime(from, timezone).toISOString(), | ||
| fromZonedTime(to, timezone).toISOString(), | ||
| ); | ||
| const { events } = eventResult; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ import { | |
| addDays, | ||
| min, | ||
| } from "date-fns"; | ||
| import { toZonedTime } from "date-fns-tz"; | ||
| import { useState, useEffect, useRef, useMemo, useCallback, memo } from "react"; | ||
|
|
||
| import { useCalendarSetters } from "@/components/layout/AppLayout"; | ||
|
|
@@ -49,6 +50,7 @@ import { OutOfOfficeEvent } from "./OutOfOfficeEvent"; | |
|
|
||
| interface DayViewProps { | ||
| events: CalendarEvent[]; | ||
| timezone: string; | ||
| date: Date; | ||
| onDeleteEvent: (eventId: string) => void; | ||
| onEventTimeChange?: (eventId: string, newStart: Date, newEnd: Date) => void; | ||
|
|
@@ -182,9 +184,13 @@ function computeLayout(dayEvents: CalendarEvent[]): Map<string, LayoutInfo> { | |
| return result; | ||
| } | ||
|
|
||
| function getEventStyleForDate(event: CalendarEvent, date: Date) { | ||
| const start = parseISO(event.start); | ||
| const end = parseISO(event.end); | ||
| function getEventStyleForDate( | ||
| event: CalendarEvent, | ||
| date: Date, | ||
| timezone: string, | ||
| ) { | ||
| const start = toZonedTime(event.start, timezone); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Use one timezone context for day event positioningThe event start/end are projected with |
||
| const end = toZonedTime(event.end, timezone); | ||
| const dayStart = set(startOfDay(date), { hours: START_HOUR }); | ||
| const dayEnd = addDays(startOfDay(date), 1); | ||
| const cappedEnd = min([end, dayEnd]); | ||
|
|
@@ -202,6 +208,7 @@ function getEventStyleForDate(event: CalendarEvent, date: Date) { | |
|
|
||
| interface DayEventCardProps { | ||
| event: CalendarEvent; | ||
| timezone: string; | ||
| date: Date; | ||
| layout: Map<string, LayoutInfo>; | ||
| now: Date; | ||
|
|
@@ -243,6 +250,7 @@ interface DayEventCardProps { | |
| */ | ||
| const DayEventCard = memo(function DayEventCard({ | ||
| event, | ||
| timezone, | ||
| date, | ||
| layout, | ||
| now, | ||
|
|
@@ -284,7 +292,7 @@ const DayEventCard = memo(function DayEventCard({ | |
| top: `${overrides.top}px`, | ||
| height: `${overrides.height}px`, | ||
| } | ||
| : getEventStyleForDate(event, date); | ||
| : getEventStyleForDate(event, date, timezone); | ||
| const color = getEventDisplayColor(event, prefs); | ||
| const evStart = parseISO(event.start); | ||
| const rawEnd = parseISO(event.end); | ||
|
|
@@ -529,6 +537,7 @@ const DayCreateGhost = memo(function DayCreateGhost({ | |
|
|
||
| export const DayView = memo(function DayView({ | ||
| events, | ||
| timezone, | ||
| date, | ||
| onDeleteEvent, | ||
| onEventTimeChange, | ||
|
|
@@ -618,7 +627,10 @@ export const DayView = memo(function DayView({ | |
| function nameForToken(token: "shortGeneric" | "longGeneric" | "short") { | ||
| try { | ||
| return ( | ||
| new Intl.DateTimeFormat("en-US", { timeZoneName: token }) | ||
| new Intl.DateTimeFormat("en-US", { | ||
| timeZone: timezone, | ||
| timeZoneName: token, | ||
| }) | ||
| .formatToParts(now) | ||
| .find((p) => p.type === "timeZoneName")?.value ?? "" | ||
| ); | ||
|
|
@@ -629,7 +641,7 @@ export const DayView = memo(function DayView({ | |
|
|
||
| let iana = ""; | ||
| try { | ||
| iana = Intl.DateTimeFormat().resolvedOptions().timeZone ?? ""; | ||
| iana = timezone; | ||
| } catch {} | ||
|
|
||
| const longGeneric = nameForToken("longGeneric"); | ||
|
|
@@ -648,10 +660,12 @@ export const DayView = memo(function DayView({ | |
| tzLong: longGeneric || iana, | ||
| tzIana: iana, | ||
| }; | ||
| }, []); | ||
| }, [now, timezone]); | ||
|
|
||
| const calendarNow = toZonedTime(now, timezone); | ||
| const today = isToday(date); | ||
| const nowMinutes = (now.getHours() - START_HOUR) * 60 + now.getMinutes(); | ||
| const nowMinutes = | ||
| (calendarNow.getHours() - START_HOUR) * 60 + calendarNow.getMinutes(); | ||
| const nowTop = (nowMinutes / 60) * HOUR_HEIGHT; | ||
| const showNowIndicator = | ||
| today && nowMinutes >= 0 && nowMinutes <= (END_HOUR - START_HOUR) * 60; | ||
|
|
@@ -676,6 +690,7 @@ export const DayView = memo(function DayView({ | |
| scrollContainerRef, | ||
| onEventTimeChange: handleEventTimeChange, | ||
| events, | ||
| timezone, | ||
| }); | ||
|
|
||
| const canDrag = !!onEventTimeChange; | ||
|
|
@@ -1109,6 +1124,7 @@ export const DayView = memo(function DayView({ | |
| <DayEventCard | ||
| key={event._tempId ?? event.id} | ||
| event={event} | ||
| timezone={timezone} | ||
| date={date} | ||
| layout={layout} | ||
| now={now} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Use the saved timezone when validating inventory cursors
The first inventory page resolves its range with the saved calendar timezone, but the cursor preflight later calls
resolveCalendarEventRangewithouttimezone. For date-only or omitted bounds, page two can compute a different query key using the request timezone and reject the cursor as query-bound. Resolve and pass the same saved timezone in the cursor path.