@agent-native/core 0.159.3 → 0.160.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/assets/server/lib/generation.ts +10 -6
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
- package/corpus/templates/calendar/actions/create-event.ts +4 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
- package/corpus/templates/calendar/actions/update-event.ts +1 -0
- package/corpus/templates/calendar/agent-native.config.ts +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
- package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
- package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
- package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
- package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
- package/corpus/templates/calendar/app/i18n-data.ts +60 -0
- package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
- package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
- package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
- package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
- package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
- package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
- package/dist/agent/engine/builder-engine.d.ts +16 -2
- package/dist/agent/engine/builder-engine.js +178 -115
- package/dist/agent/engine/builtin.js +13 -4
- package/dist/agent/engine/credential-errors.d.ts +23 -0
- package/dist/agent/engine/credential-errors.js +19 -0
- package/dist/agent/engine/error-detail.d.ts +15 -0
- package/dist/agent/engine/error-detail.js +29 -0
- package/dist/agent/engine/registry.d.ts +14 -3
- package/dist/agent/engine/registry.js +106 -61
- package/dist/agent/engine/types.d.ts +16 -0
- package/dist/agent/engine/types.js +9 -0
- package/dist/agent/production-agent.d.ts +7 -0
- package/dist/agent/production-agent.js +58 -20
- package/dist/agent/run-manager.js +30 -0
- package/dist/agent/thread-data-builder.js +4 -0
- package/dist/agent/types.d.ts +9 -0
- package/dist/client/agent-chat-adapter.js +46 -15
- package/dist/client/chat-first/app-pane.js +1 -1
- package/dist/client/chat-first/copy.js +1 -0
- package/dist/client/error-format.js +25 -2
- package/dist/client/sse-event-processor.d.ts +2 -0
- package/dist/client/sse-event-processor.js +12 -1
- package/dist/feature-flags/index.d.ts +0 -2
- package/dist/feature-flags/index.js +3 -2
- package/dist/feature-flags/server.d.ts +2 -0
- package/dist/feature-flags/server.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/jobs/background-automation-runner.js +9 -15
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/register-framework-secrets.js +25 -0
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat-plugin.js +9 -2
- package/dist/server/auth.js +10 -0
- package/dist/server/credential-provider.d.ts +80 -0
- package/dist/server/credential-provider.js +168 -1
- package/dist/server/google-realtime-session.d.ts +1 -1
- package/dist/server/google-realtime-session.js +6 -6
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +7 -2
- package/dist/server/onboarding-html.js +16 -257
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/realtime-voice.js +12 -6
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/transcribe-voice.js +18 -16
- package/dist/shared/auth-copy.d.ts +34 -0
- package/dist/shared/auth-copy.js +293 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/transcription/builder-transcription.js +8 -4
- package/docs/content/actions-advanced.mdx +1 -1
- package/docs/content/portal.mdx +21 -8
- package/package.json +3 -1
|
@@ -18,8 +18,6 @@ import {
|
|
|
18
18
|
import { useQueryClient } from "@tanstack/react-query";
|
|
19
19
|
import {
|
|
20
20
|
format,
|
|
21
|
-
startOfMonth,
|
|
22
|
-
endOfMonth,
|
|
23
21
|
startOfWeek,
|
|
24
22
|
endOfWeek,
|
|
25
23
|
addMonths,
|
|
@@ -28,8 +26,6 @@ import {
|
|
|
28
26
|
subWeeks,
|
|
29
27
|
addDays,
|
|
30
28
|
subDays,
|
|
31
|
-
parseISO,
|
|
32
|
-
startOfDay,
|
|
33
29
|
} from "date-fns";
|
|
34
30
|
import { useState, useMemo, useEffect, useCallback, useRef } from "react";
|
|
35
31
|
import { Link } from "react-router";
|
|
@@ -48,6 +44,7 @@ import {
|
|
|
48
44
|
} from "@/components/calendar/GuestNotificationDialog";
|
|
49
45
|
import { MonthView } from "@/components/calendar/MonthView";
|
|
50
46
|
import { PeopleSearchDialog } from "@/components/calendar/PeopleSearchDialog";
|
|
47
|
+
import { TimezoneSwitchDialog } from "@/components/calendar/TimezoneSwitchDialog";
|
|
51
48
|
import { WeekView } from "@/components/calendar/WeekView";
|
|
52
49
|
import { useCalendarContext } from "@/components/layout/AppLayout";
|
|
53
50
|
import type { ViewMode } from "@/components/layout/AppLayout";
|
|
@@ -81,9 +78,26 @@ import { useGoogleAuthStatus } from "@/hooks/use-google-auth";
|
|
|
81
78
|
import { useMeetingStartNotifications } from "@/hooks/use-meeting-start-notifications";
|
|
82
79
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
83
80
|
import { useOverlayPeople } from "@/hooks/use-overlay-people";
|
|
84
|
-
import { useSettings } from "@/hooks/use-settings";
|
|
81
|
+
import { useSettings, useUpdateSettings } from "@/hooks/use-settings";
|
|
85
82
|
import { setUndoAction, runUndo } from "@/hooks/use-undo";
|
|
86
83
|
import { useViewPreferences } from "@/hooks/use-view-preferences";
|
|
84
|
+
import {
|
|
85
|
+
buildWorkingLocationDraft,
|
|
86
|
+
resolveDraftWorkingLocation,
|
|
87
|
+
} from "@/lib/calendar-drafts";
|
|
88
|
+
import {
|
|
89
|
+
addCalendarDays,
|
|
90
|
+
dateKeyToDate,
|
|
91
|
+
dateKeyToTimezoneIso,
|
|
92
|
+
dateToCalendarDateKey,
|
|
93
|
+
eventOverlapsCalendarDay,
|
|
94
|
+
getBrowserTimezone,
|
|
95
|
+
getDateKeyInTimezone,
|
|
96
|
+
getEventDateKey,
|
|
97
|
+
getViewDateRange,
|
|
98
|
+
moveEventToCalendarDate,
|
|
99
|
+
normalizeTimezone,
|
|
100
|
+
} from "@/lib/calendar-timezone";
|
|
87
101
|
import { resolveEventAccountEmail } from "@/lib/event-account-selection";
|
|
88
102
|
import { getGoogleEventColorHex } from "@/lib/event-colors";
|
|
89
103
|
import {
|
|
@@ -97,8 +111,19 @@ import { buildDeleteEventMutationInput } from "@/lib/event-mutation-inputs";
|
|
|
97
111
|
import { getLocationSuggestions } from "@/lib/location-suggestions";
|
|
98
112
|
import { isMcpEmbedSurface } from "@/lib/mcp-embed";
|
|
99
113
|
import { cn } from "@/lib/utils";
|
|
114
|
+
import {
|
|
115
|
+
buildWorkingLocationProperties,
|
|
116
|
+
findOwnedWorkingLocationForDay,
|
|
117
|
+
} from "@/lib/working-location";
|
|
100
118
|
|
|
101
119
|
const CALENDAR_DRAFT_EVENT_PREFIX = "calendar-draft-event:";
|
|
120
|
+
const TIMEZONE_DISMISSAL_PREFIX = "calendar.timezone.dismissed.";
|
|
121
|
+
|
|
122
|
+
function timezoneDismissalKey(savedTimezone: string, browserTimezone: string) {
|
|
123
|
+
return `${TIMEZONE_DISMISSAL_PREFIX}${encodeURIComponent(
|
|
124
|
+
savedTimezone,
|
|
125
|
+
)}:${encodeURIComponent(browserTimezone)}`;
|
|
126
|
+
}
|
|
102
127
|
|
|
103
128
|
type DraftEventPatch = Partial<CalendarEvent> & {
|
|
104
129
|
fullDay?: boolean;
|
|
@@ -176,6 +201,14 @@ function draftRange(draft: CalendarEventDraft, fallbackDate: Date) {
|
|
|
176
201
|
const fallback = fallbackDraftRange(fallbackDate);
|
|
177
202
|
const fullDayTimezone = draft.startTimeZone ?? draft.endTimeZone;
|
|
178
203
|
const fullDayDatePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
204
|
+
const dateOnlyAllDay =
|
|
205
|
+
draft.allDay === true &&
|
|
206
|
+
Boolean(
|
|
207
|
+
draft.start &&
|
|
208
|
+
draft.end &&
|
|
209
|
+
fullDayDatePattern.test(draft.start) &&
|
|
210
|
+
fullDayDatePattern.test(draft.end),
|
|
211
|
+
);
|
|
179
212
|
const semanticFullDay =
|
|
180
213
|
draft.eventType === "outOfOffice" &&
|
|
181
214
|
draft.fullDay &&
|
|
@@ -184,18 +217,24 @@ function draftRange(draft: CalendarEventDraft, fallbackDate: Date) {
|
|
|
184
217
|
draft.end &&
|
|
185
218
|
fullDayDatePattern.test(draft.start) &&
|
|
186
219
|
fullDayDatePattern.test(draft.end);
|
|
187
|
-
const start =
|
|
188
|
-
?
|
|
189
|
-
:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
const start = dateOnlyAllDay
|
|
221
|
+
? dateKeyToDate(draft.start!)
|
|
222
|
+
: semanticFullDay
|
|
223
|
+
? new Date(
|
|
224
|
+
dateTimeInTimezoneToIso(draft.start!, "00:00", fullDayTimezone!),
|
|
225
|
+
)
|
|
226
|
+
: (parseValidDate(draft.start) ?? fallback.start);
|
|
227
|
+
const parsedEnd = dateOnlyAllDay
|
|
228
|
+
? dateKeyToDate(draft.end!)
|
|
229
|
+
: semanticFullDay
|
|
230
|
+
? new Date(
|
|
231
|
+
dateTimeInTimezoneToIso(
|
|
232
|
+
addCalendarDays(draft.end!, 1),
|
|
233
|
+
"00:00",
|
|
234
|
+
fullDayTimezone!,
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
: parseValidDate(draft.end);
|
|
199
238
|
const end =
|
|
200
239
|
parsedEnd && parsedEnd.getTime() > start.getTime()
|
|
201
240
|
? parsedEnd
|
|
@@ -209,24 +248,37 @@ function draftToCalendarEvent(
|
|
|
209
248
|
): CalendarEvent {
|
|
210
249
|
const { start, end } = draftRange(draft, fallbackDate);
|
|
211
250
|
const editableTitle = draft.title?.trim() ?? "";
|
|
251
|
+
const allDay =
|
|
252
|
+
draft.eventType === "outOfOffice" && draft.fullDay
|
|
253
|
+
? false
|
|
254
|
+
: (draft.allDay ?? false);
|
|
255
|
+
const dateOnlyAllDay =
|
|
256
|
+
allDay &&
|
|
257
|
+
Boolean(
|
|
258
|
+
draft.start &&
|
|
259
|
+
draft.end &&
|
|
260
|
+
/^\d{4}-\d{2}-\d{2}$/.test(draft.start) &&
|
|
261
|
+
/^\d{4}-\d{2}-\d{2}$/.test(draft.end),
|
|
262
|
+
);
|
|
263
|
+
const { workingLocationType, workingLocationLabel } =
|
|
264
|
+
resolveDraftWorkingLocation(draft);
|
|
212
265
|
return {
|
|
213
266
|
id: calendarDraftEventId(draft.id),
|
|
214
267
|
title:
|
|
215
268
|
editableTitle ||
|
|
216
269
|
(draft.eventType === "outOfOffice"
|
|
217
270
|
? "Out of office"
|
|
218
|
-
:
|
|
271
|
+
: draft.eventType === "workingLocation"
|
|
272
|
+
? "Working location"
|
|
273
|
+
: UNNAMED_EVENT_TITLE),
|
|
219
274
|
titleIsGenerated: !editableTitle,
|
|
220
275
|
description: draft.description ?? "",
|
|
221
|
-
start: start.toISOString(),
|
|
222
|
-
end: end.toISOString(),
|
|
276
|
+
start: dateOnlyAllDay ? draft.start! : start.toISOString(),
|
|
277
|
+
end: dateOnlyAllDay ? draft.end! : end.toISOString(),
|
|
223
278
|
startTimeZone: draft.startTimeZone,
|
|
224
279
|
endTimeZone: draft.endTimeZone ?? draft.startTimeZone,
|
|
225
|
-
location: draft.location
|
|
226
|
-
allDay
|
|
227
|
-
draft.eventType === "outOfOffice" && draft.fullDay
|
|
228
|
-
? false
|
|
229
|
-
: (draft.allDay ?? false),
|
|
280
|
+
location: draft.location || workingLocationLabel,
|
|
281
|
+
allDay,
|
|
230
282
|
source: "local",
|
|
231
283
|
accountEmail: draft.accountEmail,
|
|
232
284
|
colorId: draft.colorId,
|
|
@@ -234,6 +286,16 @@ function draftToCalendarEvent(
|
|
|
234
286
|
transparency: draft.transparency,
|
|
235
287
|
visibility: draft.visibility,
|
|
236
288
|
eventType: draft.eventType ?? "default",
|
|
289
|
+
workingLocationProperties:
|
|
290
|
+
draft.eventType === "workingLocation"
|
|
291
|
+
? buildWorkingLocationProperties(
|
|
292
|
+
{ workingLocationProperties: undefined },
|
|
293
|
+
{
|
|
294
|
+
type: workingLocationType,
|
|
295
|
+
label: workingLocationLabel,
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
: undefined,
|
|
237
299
|
outOfOfficeProperties: draft.outOfOfficeProperties,
|
|
238
300
|
recurrence: draft.recurrence,
|
|
239
301
|
attendees: draft.attendees,
|
|
@@ -273,6 +335,10 @@ function applyDraftPatch(
|
|
|
273
335
|
copy("location");
|
|
274
336
|
copy("allDay");
|
|
275
337
|
copy("fullDay");
|
|
338
|
+
if (patch.allDay === true) {
|
|
339
|
+
delete next.startTimeZone;
|
|
340
|
+
delete next.endTimeZone;
|
|
341
|
+
}
|
|
276
342
|
copy("eventType");
|
|
277
343
|
copy("outOfOfficeProperties");
|
|
278
344
|
copy("transparency");
|
|
@@ -361,6 +427,7 @@ export default function CalendarView() {
|
|
|
361
427
|
Record<string, string>
|
|
362
428
|
>({});
|
|
363
429
|
const openedDraftIdRef = useRef<string | null>(null);
|
|
430
|
+
const preserveDraftViewRef = useRef(false);
|
|
364
431
|
const committingDraftIdsRef = useRef<Set<string>>(new Set());
|
|
365
432
|
const discardedCommittingDraftsRef = useRef<Map<string, CalendarEventDraft>>(
|
|
366
433
|
new Map(),
|
|
@@ -375,6 +442,12 @@ export default function CalendarView() {
|
|
|
375
442
|
const settingsQuery = useSettings();
|
|
376
443
|
const { data: settings } = settingsQuery;
|
|
377
444
|
const weekStartsOn = getWeekStartsOn(settings?.weekStart);
|
|
445
|
+
const updateSettings = useUpdateSettings();
|
|
446
|
+
const displayTimezone = normalizeTimezone(settings?.timezone);
|
|
447
|
+
const [timezonePrompt, setTimezonePrompt] = useState<{
|
|
448
|
+
savedTimezone: string;
|
|
449
|
+
browserTimezone: string;
|
|
450
|
+
} | null>(null);
|
|
378
451
|
const { data: rawOverlayPeople } = useOverlayPeople();
|
|
379
452
|
const overlayPeople = Array.isArray(rawOverlayPeople) ? rawOverlayPeople : [];
|
|
380
453
|
const overlayEmails = useMemo(
|
|
@@ -393,32 +466,83 @@ export default function CalendarView() {
|
|
|
393
466
|
day: t("calendarView.day"),
|
|
394
467
|
};
|
|
395
468
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
to: endOfWeek(me, { weekStartsOn }).toISOString(),
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
case "week": {
|
|
408
|
-
return {
|
|
409
|
-
from: startOfWeek(selectedDate, { weekStartsOn }).toISOString(),
|
|
410
|
-
to: endOfWeek(selectedDate, { weekStartsOn }).toISOString(),
|
|
411
|
-
};
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
if (!settings?.timezone || typeof window === "undefined") return;
|
|
471
|
+
|
|
472
|
+
const checkBrowserTimezone = () => {
|
|
473
|
+
const browserTimezone = getBrowserTimezone();
|
|
474
|
+
if (browserTimezone === settings.timezone) {
|
|
475
|
+
setTimezonePrompt(null);
|
|
476
|
+
return;
|
|
412
477
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
478
|
+
|
|
479
|
+
const dismissalKey = timezoneDismissalKey(
|
|
480
|
+
settings.timezone,
|
|
481
|
+
browserTimezone,
|
|
482
|
+
);
|
|
483
|
+
try {
|
|
484
|
+
if (window.localStorage.getItem(dismissalKey) === "1") {
|
|
485
|
+
setTimezonePrompt(null);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.warn(
|
|
490
|
+
"Calendar timezone dismissal could not be read from local storage.",
|
|
491
|
+
error,
|
|
492
|
+
);
|
|
419
493
|
}
|
|
494
|
+
setTimezonePrompt({
|
|
495
|
+
savedTimezone: settings.timezone,
|
|
496
|
+
browserTimezone,
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
checkBrowserTimezone();
|
|
501
|
+
window.addEventListener("focus", checkBrowserTimezone);
|
|
502
|
+
document.addEventListener("visibilitychange", checkBrowserTimezone);
|
|
503
|
+
return () => {
|
|
504
|
+
window.removeEventListener("focus", checkBrowserTimezone);
|
|
505
|
+
document.removeEventListener("visibilitychange", checkBrowserTimezone);
|
|
506
|
+
};
|
|
507
|
+
}, [settings?.timezone]);
|
|
508
|
+
|
|
509
|
+
const keepSavedTimezone = useCallback(() => {
|
|
510
|
+
if (!timezonePrompt) return;
|
|
511
|
+
try {
|
|
512
|
+
window.localStorage.setItem(
|
|
513
|
+
timezoneDismissalKey(
|
|
514
|
+
timezonePrompt.savedTimezone,
|
|
515
|
+
timezonePrompt.browserTimezone,
|
|
516
|
+
),
|
|
517
|
+
"1",
|
|
518
|
+
);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.warn(
|
|
521
|
+
"Calendar timezone dismissal could not be saved to local storage.",
|
|
522
|
+
error,
|
|
523
|
+
);
|
|
420
524
|
}
|
|
421
|
-
|
|
525
|
+
setTimezonePrompt(null);
|
|
526
|
+
}, [timezonePrompt]);
|
|
527
|
+
|
|
528
|
+
const switchToBrowserTimezone = useCallback(() => {
|
|
529
|
+
if (!timezonePrompt || !settings) return;
|
|
530
|
+
updateSettings.mutate(
|
|
531
|
+
{ ...settings, timezone: timezonePrompt.browserTimezone },
|
|
532
|
+
{
|
|
533
|
+
onSuccess: () => setTimezonePrompt(null),
|
|
534
|
+
onError: () => toast.error(t("settings.saveFailed")),
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
}, [settings, t, timezonePrompt, updateSettings]);
|
|
538
|
+
|
|
539
|
+
// The saved Calendar Settings timezone owns the range boundary. The browser
|
|
540
|
+
// timezone is only a temporary fallback while settings are loading.
|
|
541
|
+
const { from, to } = useMemo(
|
|
542
|
+
() =>
|
|
543
|
+
getViewDateRange(viewMode, selectedDate, displayTimezone, weekStartsOn),
|
|
544
|
+
[displayTimezone, selectedDate, viewMode, weekStartsOn],
|
|
545
|
+
);
|
|
422
546
|
|
|
423
547
|
const {
|
|
424
548
|
data: rawEventsData,
|
|
@@ -471,31 +595,25 @@ export default function CalendarView() {
|
|
|
471
595
|
case "month": {
|
|
472
596
|
const next = addMonths(selectedDate, 1);
|
|
473
597
|
const prev = subMonths(selectedDate, 1);
|
|
474
|
-
return [next, prev].map((
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}));
|
|
598
|
+
return [next, prev].map((date) =>
|
|
599
|
+
getViewDateRange("month", date, displayTimezone, weekStartsOn),
|
|
600
|
+
);
|
|
478
601
|
}
|
|
479
602
|
case "week": {
|
|
480
603
|
// Two weeks forward so rapid `j j` stays instant, plus one back.
|
|
481
604
|
const next = addWeeks(selectedDate, 1);
|
|
482
605
|
const next2 = addWeeks(selectedDate, 2);
|
|
483
606
|
const prev = subWeeks(selectedDate, 1);
|
|
484
|
-
return [next, next2, prev].map((
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}));
|
|
607
|
+
return [next, next2, prev].map((date) =>
|
|
608
|
+
getViewDateRange("week", date, displayTimezone, weekStartsOn),
|
|
609
|
+
);
|
|
488
610
|
}
|
|
489
611
|
case "day": {
|
|
490
612
|
const next = addDays(selectedDate, 1);
|
|
491
613
|
const prev = subDays(selectedDate, 1);
|
|
492
|
-
return [next, prev].map((
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const end = new Date(d);
|
|
496
|
-
end.setHours(23, 59, 59, 999);
|
|
497
|
-
return { from: start.toISOString(), to: end.toISOString() };
|
|
498
|
-
});
|
|
614
|
+
return [next, prev].map((date) =>
|
|
615
|
+
getViewDateRange("day", date, displayTimezone, weekStartsOn),
|
|
616
|
+
);
|
|
499
617
|
}
|
|
500
618
|
}
|
|
501
619
|
})();
|
|
@@ -503,11 +621,12 @@ export default function CalendarView() {
|
|
|
503
621
|
void prefetchEvents(queryClient, range.from, range.to, overlayEmails);
|
|
504
622
|
}
|
|
505
623
|
}, [
|
|
624
|
+
displayTimezone,
|
|
506
625
|
isLoading,
|
|
507
|
-
viewMode,
|
|
508
|
-
selectedDate,
|
|
509
626
|
overlayEmails,
|
|
510
627
|
queryClient,
|
|
628
|
+
selectedDate,
|
|
629
|
+
viewMode,
|
|
511
630
|
weekStartsOn,
|
|
512
631
|
]);
|
|
513
632
|
|
|
@@ -581,31 +700,31 @@ export default function CalendarView() {
|
|
|
581
700
|
|
|
582
701
|
// Filter events for day view — use overlap check so multi-day continuation
|
|
583
702
|
// events (started on a prior day) still appear on the selected day.
|
|
584
|
-
const dayEvents = useMemo(
|
|
585
|
-
()
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const dayStart = startOfDay(selectedDate);
|
|
591
|
-
const dayEnd = addDays(dayStart, 1);
|
|
592
|
-
return evStart < dayEnd && evEnd > dayStart;
|
|
593
|
-
})
|
|
594
|
-
: events,
|
|
595
|
-
[events, viewMode, selectedDate],
|
|
596
|
-
);
|
|
703
|
+
const dayEvents = useMemo(() => {
|
|
704
|
+
if (viewMode !== "day") return events;
|
|
705
|
+
return events.filter((event) =>
|
|
706
|
+
eventOverlapsCalendarDay(event, selectedDate, displayTimezone),
|
|
707
|
+
);
|
|
708
|
+
}, [displayTimezone, events, selectedDate, viewMode]);
|
|
597
709
|
const locationSuggestions = useMemo(
|
|
598
710
|
() => getLocationSuggestions(events),
|
|
599
711
|
[events],
|
|
600
712
|
);
|
|
601
713
|
const openNotificationEvent = useCallback(
|
|
602
714
|
(event: CalendarEvent) => {
|
|
603
|
-
|
|
715
|
+
const eventDate = getEventDateKey(event, displayTimezone);
|
|
716
|
+
if (eventDate) setSelectedDate(dateKeyToDate(eventDate));
|
|
604
717
|
setViewMode("day");
|
|
605
718
|
setSidebarEvent(event);
|
|
606
719
|
setFocusedEvent(event);
|
|
607
720
|
},
|
|
608
|
-
[
|
|
721
|
+
[
|
|
722
|
+
displayTimezone,
|
|
723
|
+
setFocusedEvent,
|
|
724
|
+
setSelectedDate,
|
|
725
|
+
setSidebarEvent,
|
|
726
|
+
setViewMode,
|
|
727
|
+
],
|
|
609
728
|
);
|
|
610
729
|
useMeetingStartNotifications(events, openNotificationEvent);
|
|
611
730
|
|
|
@@ -616,10 +735,17 @@ export default function CalendarView() {
|
|
|
616
735
|
}
|
|
617
736
|
if (openedDraftIdRef.current === eventDraft.id) return;
|
|
618
737
|
openedDraftIdRef.current = eventDraft.id;
|
|
738
|
+
const preserveView = preserveDraftViewRef.current;
|
|
739
|
+
preserveDraftViewRef.current = false;
|
|
619
740
|
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
741
|
+
const draftDate = getEventDateKey(
|
|
742
|
+
draftToCalendarEvent(eventDraft, selectedDate),
|
|
743
|
+
displayTimezone,
|
|
744
|
+
);
|
|
745
|
+
if (!preserveView && draftDate) {
|
|
746
|
+
setSelectedDate(dateKeyToDate(draftDate));
|
|
747
|
+
}
|
|
748
|
+
if (!preserveView && (isMobile || viewMode === "month")) {
|
|
623
749
|
setViewMode("day");
|
|
624
750
|
}
|
|
625
751
|
setCreateDefaultStart(undefined);
|
|
@@ -630,6 +756,7 @@ export default function CalendarView() {
|
|
|
630
756
|
setQuickEditEventId(calendarDraftEventId(eventDraft.id));
|
|
631
757
|
}, [
|
|
632
758
|
eventDraft,
|
|
759
|
+
displayTimezone,
|
|
633
760
|
isMobile,
|
|
634
761
|
selectedDate,
|
|
635
762
|
setEventDetailSidebar,
|
|
@@ -670,7 +797,11 @@ export default function CalendarView() {
|
|
|
670
797
|
const eventType = draft.eventType ?? "default";
|
|
671
798
|
const title =
|
|
672
799
|
editableTitle || (eventType === "outOfOffice" ? "Out of office" : "");
|
|
673
|
-
if (
|
|
800
|
+
if (
|
|
801
|
+
!title &&
|
|
802
|
+
eventType !== "workingLocation" &&
|
|
803
|
+
!isSlotDraftId(draftId)
|
|
804
|
+
) {
|
|
674
805
|
committingDraftIdsRef.current.delete(draftId);
|
|
675
806
|
toast.error(t("calendarView.addTitleBeforeCreate"));
|
|
676
807
|
return;
|
|
@@ -683,27 +814,31 @@ export default function CalendarView() {
|
|
|
683
814
|
return;
|
|
684
815
|
}
|
|
685
816
|
|
|
686
|
-
const
|
|
817
|
+
const { workingLocationType, workingLocationLabel } =
|
|
818
|
+
resolveDraftWorkingLocation(draft);
|
|
819
|
+
const location =
|
|
820
|
+
eventType === "workingLocation"
|
|
821
|
+
? workingLocationLabel
|
|
822
|
+
: (draft.location ?? "");
|
|
687
823
|
const timezone = resolveEventTimezone(
|
|
688
|
-
draft.startTimeZone ?? draft.endTimeZone,
|
|
824
|
+
draft.startTimeZone ?? draft.endTimeZone ?? displayTimezone,
|
|
689
825
|
);
|
|
690
826
|
const semanticFullDay =
|
|
691
827
|
eventType === "outOfOffice" &&
|
|
692
828
|
draft.fullDay === true &&
|
|
693
829
|
/^\d{4}-\d{2}-\d{2}$/.test(draft.start ?? "") &&
|
|
694
830
|
/^\d{4}-\d{2}-\d{2}$/.test(draft.end ?? "");
|
|
831
|
+
const dateOnlyAllDay =
|
|
832
|
+
draft.allDay === true &&
|
|
833
|
+
/^\d{4}-\d{2}-\d{2}$/.test(draft.start ?? "") &&
|
|
834
|
+
/^\d{4}-\d{2}-\d{2}$/.test(draft.end ?? "");
|
|
695
835
|
const statusPatch =
|
|
696
836
|
eventType === "default"
|
|
697
837
|
? {}
|
|
698
838
|
: {
|
|
699
839
|
eventType,
|
|
700
|
-
workingLocationType
|
|
701
|
-
|
|
702
|
-
workingLocationLabel:
|
|
703
|
-
(draft.workingLocationType ?? "customLocation") ===
|
|
704
|
-
"customLocation"
|
|
705
|
-
? location
|
|
706
|
-
: draft.workingLocationLabel,
|
|
840
|
+
workingLocationType,
|
|
841
|
+
workingLocationLabel,
|
|
707
842
|
autoDeclineMode:
|
|
708
843
|
eventType === "outOfOffice"
|
|
709
844
|
? draft.outOfOfficeProperties?.autoDeclineMode
|
|
@@ -720,8 +855,11 @@ export default function CalendarView() {
|
|
|
720
855
|
titleIsGenerated: !editableTitle,
|
|
721
856
|
description:
|
|
722
857
|
eventType === "outOfOffice" ? "" : (draft.description ?? ""),
|
|
723
|
-
start:
|
|
724
|
-
|
|
858
|
+
start:
|
|
859
|
+
semanticFullDay || dateOnlyAllDay
|
|
860
|
+
? draft.start!
|
|
861
|
+
: start.toISOString(),
|
|
862
|
+
end: semanticFullDay || dateOnlyAllDay ? draft.end! : end.toISOString(),
|
|
725
863
|
startTimeZone: draft.allDay ? undefined : timezone,
|
|
726
864
|
endTimeZone: draft.allDay
|
|
727
865
|
? undefined
|
|
@@ -803,6 +941,7 @@ export default function CalendarView() {
|
|
|
803
941
|
defaultAccountEmail,
|
|
804
942
|
deleteEvent,
|
|
805
943
|
eventDraft,
|
|
944
|
+
displayTimezone,
|
|
806
945
|
googleStatus.data?.accounts,
|
|
807
946
|
selectedDate,
|
|
808
947
|
setEventDraft,
|
|
@@ -882,7 +1021,8 @@ export default function CalendarView() {
|
|
|
882
1021
|
}
|
|
883
1022
|
|
|
884
1023
|
function handleToday() {
|
|
885
|
-
|
|
1024
|
+
const today = getDateKeyInTimezone(new Date(), displayTimezone);
|
|
1025
|
+
if (today) setSelectedDate(dateKeyToDate(today));
|
|
886
1026
|
}
|
|
887
1027
|
|
|
888
1028
|
const handleDateSelect = useCallback(
|
|
@@ -1047,54 +1187,24 @@ export default function CalendarView() {
|
|
|
1047
1187
|
const event = events.find((e) => e.id === eventId);
|
|
1048
1188
|
if (!event) return;
|
|
1049
1189
|
|
|
1190
|
+
const moved = moveEventToCalendarDate(event, newDate, displayTimezone);
|
|
1191
|
+
if (!moved) return;
|
|
1192
|
+
|
|
1050
1193
|
if (calendarDraftIdFromEventId(eventId)) {
|
|
1051
|
-
|
|
1052
|
-
const originalEnd = parseISO(event.end);
|
|
1053
|
-
const newStart = new Date(originalStart);
|
|
1054
|
-
const newEnd = new Date(originalEnd);
|
|
1055
|
-
newStart.setFullYear(
|
|
1056
|
-
newDate.getFullYear(),
|
|
1057
|
-
newDate.getMonth(),
|
|
1058
|
-
newDate.getDate(),
|
|
1059
|
-
);
|
|
1060
|
-
newEnd.setFullYear(
|
|
1061
|
-
newDate.getFullYear(),
|
|
1062
|
-
newDate.getMonth(),
|
|
1063
|
-
newDate.getDate(),
|
|
1064
|
-
);
|
|
1065
|
-
updateDraftEvent(eventId, {
|
|
1066
|
-
start: newStart.toISOString(),
|
|
1067
|
-
end: newEnd.toISOString(),
|
|
1068
|
-
});
|
|
1194
|
+
updateDraftEvent(eventId, moved);
|
|
1069
1195
|
return;
|
|
1070
1196
|
}
|
|
1071
1197
|
|
|
1072
1198
|
const oldStartISO = event.start;
|
|
1073
1199
|
const oldEndISO = event.end;
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const newStart = new Date(originalStart);
|
|
1077
|
-
const newEnd = new Date(originalEnd);
|
|
1078
|
-
|
|
1079
|
-
newStart.setFullYear(
|
|
1080
|
-
newDate.getFullYear(),
|
|
1081
|
-
newDate.getMonth(),
|
|
1082
|
-
newDate.getDate(),
|
|
1083
|
-
);
|
|
1084
|
-
newEnd.setFullYear(
|
|
1085
|
-
newDate.getFullYear(),
|
|
1086
|
-
newDate.getMonth(),
|
|
1087
|
-
newDate.getDate(),
|
|
1088
|
-
);
|
|
1200
|
+
const newStart = new Date(moved.start);
|
|
1201
|
+
const newEnd = new Date(moved.end);
|
|
1089
1202
|
|
|
1090
1203
|
// Guard against a zero/negative duration reaching the server (e.g. a
|
|
1091
1204
|
// DST transition collapsing a short event's start/end onto each other).
|
|
1092
1205
|
if (newEnd.getTime() <= newStart.getTime()) return;
|
|
1093
1206
|
|
|
1094
|
-
const updates =
|
|
1095
|
-
start: newStart.toISOString(),
|
|
1096
|
-
end: newEnd.toISOString(),
|
|
1097
|
-
};
|
|
1207
|
+
const updates = moved;
|
|
1098
1208
|
const isRecurring = isRecurringCalendarEvent(event);
|
|
1099
1209
|
const guestNotification = await promptGuestNotification({
|
|
1100
1210
|
event,
|
|
@@ -1155,7 +1265,7 @@ export default function CalendarView() {
|
|
|
1155
1265
|
|
|
1156
1266
|
if (calendarDraftIdFromEventId(eventId)) {
|
|
1157
1267
|
const timezone = resolveEventTimezone(
|
|
1158
|
-
event.startTimeZone ?? event.endTimeZone,
|
|
1268
|
+
event.startTimeZone ?? event.endTimeZone ?? displayTimezone,
|
|
1159
1269
|
);
|
|
1160
1270
|
updateDraftEvent(eventId, {
|
|
1161
1271
|
start: newStart.toISOString(),
|
|
@@ -1167,8 +1277,8 @@ export default function CalendarView() {
|
|
|
1167
1277
|
return;
|
|
1168
1278
|
}
|
|
1169
1279
|
|
|
1170
|
-
const oldStart =
|
|
1171
|
-
const oldEnd =
|
|
1280
|
+
const oldStart = new Date(event.start).getTime();
|
|
1281
|
+
const oldEnd = new Date(event.end).getTime();
|
|
1172
1282
|
if (oldStart === newStart.getTime() && oldEnd === newEnd.getTime()) {
|
|
1173
1283
|
return;
|
|
1174
1284
|
}
|
|
@@ -1226,8 +1336,9 @@ export default function CalendarView() {
|
|
|
1226
1336
|
);
|
|
1227
1337
|
},
|
|
1228
1338
|
[
|
|
1339
|
+
displayTimezone,
|
|
1229
1340
|
events,
|
|
1230
|
-
|
|
1341
|
+
displayTimezone,
|
|
1231
1342
|
updateDraftEvent,
|
|
1232
1343
|
promptGuestNotification,
|
|
1233
1344
|
updateEvent,
|
|
@@ -1261,7 +1372,7 @@ export default function CalendarView() {
|
|
|
1261
1372
|
setCreateDefaultStart(startTime);
|
|
1262
1373
|
setCreateDialogOpen(false);
|
|
1263
1374
|
|
|
1264
|
-
const dateStr =
|
|
1375
|
+
const dateStr = dateToCalendarDateKey(clickedDate);
|
|
1265
1376
|
// A drag-to-create gesture already computed the exact dragged range;
|
|
1266
1377
|
// a plain click falls back to the user's configured default duration.
|
|
1267
1378
|
const end = options?.explicitDuration
|
|
@@ -1302,6 +1413,53 @@ export default function CalendarView() {
|
|
|
1302
1413
|
],
|
|
1303
1414
|
);
|
|
1304
1415
|
|
|
1416
|
+
const handleCreateWorkingLocation = useCallback(
|
|
1417
|
+
(date: Date) => {
|
|
1418
|
+
const existing = findOwnedWorkingLocationForDay(
|
|
1419
|
+
events,
|
|
1420
|
+
date,
|
|
1421
|
+
displayTimezone,
|
|
1422
|
+
defaultAccountEmail,
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1425
|
+
preserveDraftViewRef.current = true;
|
|
1426
|
+
setCreateDefaultStart(undefined);
|
|
1427
|
+
setCreateDefaultEnd(undefined);
|
|
1428
|
+
|
|
1429
|
+
if (existing) {
|
|
1430
|
+
const existingDraftId = calendarDraftIdFromEventId(existing.id);
|
|
1431
|
+
if (!existingDraftId && eventDraft) {
|
|
1432
|
+
discardDraftEvent(calendarDraftEventId(eventDraft.id));
|
|
1433
|
+
}
|
|
1434
|
+
setQuickEditEventId(existing.id);
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (eventDraft) {
|
|
1439
|
+
discardDraftEvent(calendarDraftEventId(eventDraft.id));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const draftId = `slot-working-location-${Date.now()}`;
|
|
1443
|
+
const draft = buildWorkingLocationDraft({
|
|
1444
|
+
id: draftId,
|
|
1445
|
+
date,
|
|
1446
|
+
accountEmail: defaultAccountEmail,
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
persistCalendarDraft(draft);
|
|
1450
|
+
setEventDraft(draft);
|
|
1451
|
+
setQuickEditEventId(calendarDraftEventId(draftId));
|
|
1452
|
+
},
|
|
1453
|
+
[
|
|
1454
|
+
defaultAccountEmail,
|
|
1455
|
+
discardDraftEvent,
|
|
1456
|
+
displayTimezone,
|
|
1457
|
+
eventDraft,
|
|
1458
|
+
events,
|
|
1459
|
+
setEventDraft,
|
|
1460
|
+
],
|
|
1461
|
+
);
|
|
1462
|
+
|
|
1305
1463
|
// Command palette natural-language quick create (e.g. "lunch with Sam
|
|
1306
1464
|
// tomorrow 12:30") — builds a prefilled draft and jumps to it, reusing the
|
|
1307
1465
|
// same draft/quick-edit flow as clicking a time slot.
|
|
@@ -1806,7 +1964,9 @@ export default function CalendarView() {
|
|
|
1806
1964
|
<MonthView
|
|
1807
1965
|
events={events}
|
|
1808
1966
|
selectedDate={selectedDate}
|
|
1967
|
+
timezone={displayTimezone}
|
|
1809
1968
|
onDateSelect={handleDateSelect}
|
|
1969
|
+
onCreateWorkingLocation={handleCreateWorkingLocation}
|
|
1810
1970
|
onDeleteEvent={handleDeleteEvent}
|
|
1811
1971
|
onEventDrop={handleEventDrop}
|
|
1812
1972
|
draftEventIds={draftEventIds}
|
|
@@ -1821,10 +1981,12 @@ export default function CalendarView() {
|
|
|
1821
1981
|
<WeekView
|
|
1822
1982
|
events={events}
|
|
1823
1983
|
selectedDate={selectedDate}
|
|
1984
|
+
timezone={displayTimezone}
|
|
1824
1985
|
onDateSelect={handleDateSelect}
|
|
1825
1986
|
onDeleteEvent={handleDeleteEvent}
|
|
1826
1987
|
onEventTimeChange={handleEventTimeChange}
|
|
1827
1988
|
onClickTimeSlot={handleClickTimeSlot}
|
|
1989
|
+
onCreateWorkingLocation={handleCreateWorkingLocation}
|
|
1828
1990
|
quickEditEventId={quickEditEventId}
|
|
1829
1991
|
onQuickEditSave={handleQuickEditSave}
|
|
1830
1992
|
onQuickEditCancel={handleQuickEditCancel}
|
|
@@ -1840,9 +2002,11 @@ export default function CalendarView() {
|
|
|
1840
2002
|
<DayView
|
|
1841
2003
|
events={dayEvents}
|
|
1842
2004
|
date={selectedDate}
|
|
2005
|
+
timezone={displayTimezone}
|
|
1843
2006
|
onDeleteEvent={handleDeleteEvent}
|
|
1844
2007
|
onEventTimeChange={handleEventTimeChange}
|
|
1845
2008
|
onClickTimeSlot={handleClickTimeSlot}
|
|
2009
|
+
onCreateWorkingLocation={handleCreateWorkingLocation}
|
|
1846
2010
|
quickEditEventId={quickEditEventId}
|
|
1847
2011
|
onQuickEditSave={handleQuickEditSave}
|
|
1848
2012
|
onQuickEditCancel={handleQuickEditCancel}
|
|
@@ -1867,6 +2031,19 @@ export default function CalendarView() {
|
|
|
1867
2031
|
)}
|
|
1868
2032
|
|
|
1869
2033
|
{/* Dialogs */}
|
|
2034
|
+
{timezonePrompt && (
|
|
2035
|
+
<TimezoneSwitchDialog
|
|
2036
|
+
open
|
|
2037
|
+
savedTimezone={timezonePrompt.savedTimezone}
|
|
2038
|
+
browserTimezone={timezonePrompt.browserTimezone}
|
|
2039
|
+
isSwitching={updateSettings.isPending}
|
|
2040
|
+
onKeep={keepSavedTimezone}
|
|
2041
|
+
onSwitch={switchToBrowserTimezone}
|
|
2042
|
+
onOpenChange={(open) => {
|
|
2043
|
+
if (!open) keepSavedTimezone();
|
|
2044
|
+
}}
|
|
2045
|
+
/>
|
|
2046
|
+
)}
|
|
1870
2047
|
<CommandPalette
|
|
1871
2048
|
open={commandPaletteOpen}
|
|
1872
2049
|
onClose={() => setCommandPaletteOpen(false)}
|
|
@@ -1874,7 +2051,8 @@ export default function CalendarView() {
|
|
|
1874
2051
|
onGoToDate={handleGoToDate}
|
|
1875
2052
|
onEventClick={(event) => {
|
|
1876
2053
|
setCommandPaletteOpen(false);
|
|
1877
|
-
|
|
2054
|
+
const eventDate = getEventDateKey(event, displayTimezone);
|
|
2055
|
+
if (eventDate) handleGoToDate(dateKeyToDate(eventDate));
|
|
1878
2056
|
}}
|
|
1879
2057
|
onCreateEvent={() => {
|
|
1880
2058
|
setCommandPaletteOpen(false);
|