@agent-native/core 0.159.5 → 0.160.1
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 +68 -31
- package/dist/agent/run-manager.js +30 -0
- package/dist/agent/thread-data-builder.js +4 -0
- package/dist/agent/tool-approval-migrations.d.ts +1 -0
- package/dist/agent/tool-approval-migrations.js +10 -0
- package/dist/agent/tool-approval-store.d.ts +5 -3
- package/dist/agent/tool-approval-store.js +11 -6
- package/dist/agent/types.d.ts +9 -0
- package/dist/cli/code-agent-connector.js +79 -7
- package/dist/cli/portal-transfer.d.ts +43 -0
- package/dist/cli/portal-transfer.js +201 -0
- package/dist/client/agent-chat-adapter.js +46 -15
- 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/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/jobs/background-automation-runner.js +9 -15
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/register-framework-secrets.js +25 -0
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat-plugin.js +9 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- 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 +1 -1
- package/dist/server/index.js +6 -1
- 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/transcription/builder-transcription.js +8 -4
- package/package.json +3 -3
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { WorkingLocationEditor } from "@/components/calendar/WorkingLocationEditor";
|
|
52
52
|
import { useCalendarContext } from "@/components/layout/AppLayout";
|
|
53
53
|
import { Button } from "@/components/ui/button";
|
|
54
|
+
import { Label } from "@/components/ui/label";
|
|
54
55
|
import {
|
|
55
56
|
Popover,
|
|
56
57
|
PopoverTrigger,
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
SelectValue,
|
|
65
66
|
} from "@/components/ui/select";
|
|
66
67
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
68
|
+
import { Switch } from "@/components/ui/switch";
|
|
67
69
|
import {
|
|
68
70
|
Tooltip,
|
|
69
71
|
TooltipContent,
|
|
@@ -73,6 +75,11 @@ import {
|
|
|
73
75
|
import { useEvent, useUpdateEvent } from "@/hooks/use-events";
|
|
74
76
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
75
77
|
import { useConnectZoom, useZoomStatus } from "@/hooks/use-zoom-auth";
|
|
78
|
+
import { addCalendarDays } from "@/lib/calendar-timezone";
|
|
79
|
+
import {
|
|
80
|
+
getDateKeyInTimezone,
|
|
81
|
+
getDateTimePartsInTimezone,
|
|
82
|
+
} from "@/lib/calendar-timezone";
|
|
76
83
|
import {
|
|
77
84
|
attachmentsToDrafts,
|
|
78
85
|
buildRecurrenceRules,
|
|
@@ -83,7 +90,6 @@ import {
|
|
|
83
90
|
getEventEndValidationMessage,
|
|
84
91
|
getLocalTimezone,
|
|
85
92
|
getRecurrencePreset,
|
|
86
|
-
normalizeAllDayEditEndDate,
|
|
87
93
|
remindersToDraftState,
|
|
88
94
|
resolveTimeEditScope,
|
|
89
95
|
type AttachmentDraft,
|
|
@@ -103,6 +109,7 @@ import {
|
|
|
103
109
|
buildWorkingLocationUpdate,
|
|
104
110
|
createWorkingLocationDisplayLabels,
|
|
105
111
|
getWorkingLocationTitle,
|
|
112
|
+
isWorkingLocationDraftReadyToCreate,
|
|
106
113
|
isWorkingLocationEvent,
|
|
107
114
|
type WorkingLocationSelection,
|
|
108
115
|
} from "@/lib/working-location";
|
|
@@ -395,19 +402,45 @@ function isUrl(str: string): boolean {
|
|
|
395
402
|
return /^https?:\/\//i.test(str.trim());
|
|
396
403
|
}
|
|
397
404
|
|
|
398
|
-
/** Convert
|
|
399
|
-
function toDateInputValue(iso: string): string {
|
|
405
|
+
/** Convert an event date to the calendar's date input value. */
|
|
406
|
+
function toDateInputValue(iso: string, timezone?: string): string {
|
|
407
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(iso)) return iso;
|
|
408
|
+
const zonedDate = timezone ? getDateKeyInTimezone(iso, timezone) : null;
|
|
409
|
+
if (zonedDate) return zonedDate;
|
|
400
410
|
const d = parseISO(iso);
|
|
401
411
|
return format(d, "yyyy-MM-dd");
|
|
402
412
|
}
|
|
403
413
|
|
|
404
|
-
function toAllDayEndDateInputValue(iso: string): string {
|
|
414
|
+
function toAllDayEndDateInputValue(iso: string, timezone?: string): string {
|
|
415
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(iso)) return addCalendarDays(iso, -1);
|
|
416
|
+
const zonedDate = timezone ? getDateKeyInTimezone(iso, timezone) : null;
|
|
417
|
+
if (zonedDate) return addCalendarDays(zonedDate, -1);
|
|
405
418
|
const d = parseISO(iso);
|
|
406
419
|
return format(new Date(d.getTime() - 1), "yyyy-MM-dd");
|
|
407
420
|
}
|
|
408
421
|
|
|
409
|
-
/**
|
|
410
|
-
function
|
|
422
|
+
/** Timed events ending at 00:00 already store the next date; don't add another day. */
|
|
423
|
+
function inclusiveEndDateForAllDayConversion(
|
|
424
|
+
startDate: string,
|
|
425
|
+
endDate: string,
|
|
426
|
+
endTime: string,
|
|
427
|
+
): string {
|
|
428
|
+
const bounded = endDate < startDate ? startDate : endDate;
|
|
429
|
+
if (endTime === "00:00" && bounded > startDate) {
|
|
430
|
+
return addCalendarDays(bounded, -1);
|
|
431
|
+
}
|
|
432
|
+
return bounded;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Convert an event time to the calendar's time input value. */
|
|
436
|
+
function toTimeInputValue(iso: string, timezone?: string): string {
|
|
437
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(iso)) return "00:00";
|
|
438
|
+
const zonedTime = timezone ? getDateTimePartsInTimezone(iso, timezone) : null;
|
|
439
|
+
if (zonedTime) {
|
|
440
|
+
return `${String(zonedTime.hour).padStart(2, "0")}:${String(
|
|
441
|
+
zonedTime.minute,
|
|
442
|
+
).padStart(2, "0")}`;
|
|
443
|
+
}
|
|
411
444
|
const d = parseISO(iso);
|
|
412
445
|
return format(d, "HH:mm");
|
|
413
446
|
}
|
|
@@ -426,6 +459,7 @@ interface EventDetailPopoverProps {
|
|
|
426
459
|
children: React.ReactNode;
|
|
427
460
|
onDelete: (eventId: string) => void;
|
|
428
461
|
isDraft?: boolean;
|
|
462
|
+
timezone?: string;
|
|
429
463
|
/** When true, the popover opens immediately and title is focused for editing */
|
|
430
464
|
defaultOpen?: boolean;
|
|
431
465
|
/** Called when the title is changed and should be persisted */
|
|
@@ -458,6 +492,7 @@ export function EventDetailPopover({
|
|
|
458
492
|
children,
|
|
459
493
|
onDelete,
|
|
460
494
|
isDraft = false,
|
|
495
|
+
timezone,
|
|
461
496
|
defaultOpen = false,
|
|
462
497
|
onTitleSave,
|
|
463
498
|
onDismissNew,
|
|
@@ -469,6 +504,7 @@ export function EventDetailPopover({
|
|
|
469
504
|
const t = useT();
|
|
470
505
|
const workingLocationLabels = createWorkingLocationDisplayLabels(t);
|
|
471
506
|
const isMobile = useIsMobile();
|
|
507
|
+
const eventTimezone = event.startTimeZone || timezone || getLocalTimezone();
|
|
472
508
|
const [open, setOpen] = useState(defaultOpen);
|
|
473
509
|
const [editingTitle, setEditingTitle] = useState(
|
|
474
510
|
defaultOpen ? getEditableEventTitle(event) : "",
|
|
@@ -489,7 +525,6 @@ export function EventDetailPopover({
|
|
|
489
525
|
} = useCalendarContext();
|
|
490
526
|
const isWorkingLocation = isWorkingLocationEvent(event);
|
|
491
527
|
const isOutOfOffice = isOutOfOfficeEvent(event);
|
|
492
|
-
const isSingleDayWorkingLocation = isWorkingLocation && event.allDay;
|
|
493
528
|
const editableLocationValue = event.location || "";
|
|
494
529
|
|
|
495
530
|
// Inline editing state
|
|
@@ -499,21 +534,23 @@ export function EventDetailPopover({
|
|
|
499
534
|
event.description || "",
|
|
500
535
|
);
|
|
501
536
|
const [editLocation, setEditLocation] = useState(editableLocationValue);
|
|
502
|
-
const [editDate, setEditDate] = useState(() =>
|
|
537
|
+
const [editDate, setEditDate] = useState(() =>
|
|
538
|
+
toDateInputValue(event.start, eventTimezone),
|
|
539
|
+
);
|
|
503
540
|
const [editEndDate, setEditEndDate] = useState(() =>
|
|
504
541
|
event.allDay
|
|
505
|
-
? toAllDayEndDateInputValue(event.end)
|
|
506
|
-
: toDateInputValue(event.end),
|
|
542
|
+
? toAllDayEndDateInputValue(event.end, eventTimezone)
|
|
543
|
+
: toDateInputValue(event.end, eventTimezone),
|
|
507
544
|
);
|
|
508
545
|
const [editStartTime, setEditStartTime] = useState(() =>
|
|
509
|
-
toTimeInputValue(event.start),
|
|
546
|
+
toTimeInputValue(event.start, eventTimezone),
|
|
510
547
|
);
|
|
511
548
|
const [editEndTime, setEditEndTime] = useState(() =>
|
|
512
|
-
toTimeInputValue(event.end),
|
|
513
|
-
);
|
|
514
|
-
const [editTimezone, setEditTimezone] = useState(
|
|
515
|
-
event.startTimeZone || getLocalTimezone(),
|
|
549
|
+
toTimeInputValue(event.end, eventTimezone),
|
|
516
550
|
);
|
|
551
|
+
const [editTimezone, setEditTimezone] = useState(eventTimezone);
|
|
552
|
+
const isSingleDayWorkingLocation =
|
|
553
|
+
isWorkingLocation && event.allDay && editDate === editEndDate;
|
|
517
554
|
const [editReminderMode, setEditReminderMode] = useState<ReminderMode>(
|
|
518
555
|
() => remindersToDraftState(event).mode,
|
|
519
556
|
);
|
|
@@ -615,15 +652,15 @@ export function EventDetailPopover({
|
|
|
615
652
|
setEditDescription(event.description || "");
|
|
616
653
|
if (editingField !== "location") setEditLocation(editableLocationValue);
|
|
617
654
|
if (editingField !== "time") {
|
|
618
|
-
setEditDate(toDateInputValue(event.start));
|
|
655
|
+
setEditDate(toDateInputValue(event.start, eventTimezone));
|
|
619
656
|
setEditEndDate(
|
|
620
657
|
event.allDay
|
|
621
|
-
? toAllDayEndDateInputValue(event.end)
|
|
622
|
-
: toDateInputValue(event.end),
|
|
658
|
+
? toAllDayEndDateInputValue(event.end, eventTimezone)
|
|
659
|
+
: toDateInputValue(event.end, eventTimezone),
|
|
623
660
|
);
|
|
624
|
-
setEditStartTime(toTimeInputValue(event.start));
|
|
625
|
-
setEditEndTime(toTimeInputValue(event.end));
|
|
626
|
-
setEditTimezone(
|
|
661
|
+
setEditStartTime(toTimeInputValue(event.start, eventTimezone));
|
|
662
|
+
setEditEndTime(toTimeInputValue(event.end, eventTimezone));
|
|
663
|
+
setEditTimezone(eventTimezone);
|
|
627
664
|
setEditTimeScope("single");
|
|
628
665
|
}
|
|
629
666
|
if (editingField !== "reminders") {
|
|
@@ -652,6 +689,7 @@ export function EventDetailPopover({
|
|
|
652
689
|
event.remindersUseDefault,
|
|
653
690
|
event.attachments,
|
|
654
691
|
editableLocationValue,
|
|
692
|
+
eventTimezone,
|
|
655
693
|
]);
|
|
656
694
|
|
|
657
695
|
// When defaultOpen changes to true (new event created), open the popover
|
|
@@ -659,14 +697,14 @@ export function EventDetailPopover({
|
|
|
659
697
|
if (defaultOpen) {
|
|
660
698
|
setOpen(true);
|
|
661
699
|
setIsEditingTitle(true);
|
|
662
|
-
isNewEventRef.current =
|
|
700
|
+
isNewEventRef.current = isDraft;
|
|
663
701
|
setEditingTitle((current) => {
|
|
664
702
|
const eventTitle = getEditableEventTitle(event);
|
|
665
703
|
if (current.trim() && current !== eventTitle) return current;
|
|
666
704
|
return eventTitle;
|
|
667
705
|
});
|
|
668
706
|
}
|
|
669
|
-
}, [defaultOpen, event.title, event.titleIsGenerated]);
|
|
707
|
+
}, [defaultOpen, event.title, event.titleIsGenerated, isDraft]);
|
|
670
708
|
|
|
671
709
|
// Focus title input when editing starts
|
|
672
710
|
useEffect(() => {
|
|
@@ -998,32 +1036,29 @@ export function EventDetailPopover({
|
|
|
998
1036
|
);
|
|
999
1037
|
|
|
1000
1038
|
const saveTimeValues = useCallback(
|
|
1001
|
-
(values: TimeEditValues) => {
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
values.date,
|
|
1005
|
-
values.endDate,
|
|
1006
|
-
);
|
|
1007
|
-
const allDayEnd = new Date(`${normalizedEndDate}T00:00:00`);
|
|
1008
|
-
allDayEnd.setDate(allDayEnd.getDate() + 1);
|
|
1009
|
-
const newStart = event.allDay
|
|
1010
|
-
? new Date(`${values.date}T00:00:00`).toISOString()
|
|
1039
|
+
(values: TimeEditValues, nextAllDay = event.allDay) => {
|
|
1040
|
+
const newStart = nextAllDay
|
|
1041
|
+
? values.date
|
|
1011
1042
|
: dateTimeInTimezoneToIso(
|
|
1012
1043
|
values.date,
|
|
1013
1044
|
values.startTime,
|
|
1014
1045
|
values.timezone,
|
|
1015
1046
|
);
|
|
1016
|
-
const newEnd =
|
|
1017
|
-
?
|
|
1047
|
+
const newEnd = nextAllDay
|
|
1048
|
+
? addCalendarDays(values.endDate, 1)
|
|
1018
1049
|
: dateTimeInTimezoneToIso(
|
|
1019
1050
|
values.endDate,
|
|
1020
1051
|
values.endTime,
|
|
1021
1052
|
values.timezone,
|
|
1022
1053
|
);
|
|
1023
|
-
if (
|
|
1054
|
+
if (
|
|
1055
|
+
nextAllDay
|
|
1056
|
+
? values.endDate < values.date
|
|
1057
|
+
: new Date(newEnd).getTime() <= new Date(newStart).getTime()
|
|
1058
|
+
) {
|
|
1024
1059
|
toast.error(
|
|
1025
1060
|
getEventEndValidationMessage({
|
|
1026
|
-
allDay:
|
|
1061
|
+
allDay: nextAllDay,
|
|
1027
1062
|
startDate: values.date,
|
|
1028
1063
|
endDate: values.endDate,
|
|
1029
1064
|
startTime: values.startTime,
|
|
@@ -1037,9 +1072,9 @@ export function EventDetailPopover({
|
|
|
1037
1072
|
saved = saveField({
|
|
1038
1073
|
start: newStart,
|
|
1039
1074
|
end: newEnd,
|
|
1040
|
-
allDay:
|
|
1041
|
-
startTimeZone:
|
|
1042
|
-
endTimeZone:
|
|
1075
|
+
allDay: nextAllDay,
|
|
1076
|
+
startTimeZone: nextAllDay ? undefined : values.timezone,
|
|
1077
|
+
endTimeZone: nextAllDay ? undefined : values.timezone,
|
|
1043
1078
|
scope: resolveTimeEditScope(
|
|
1044
1079
|
isRecurringEvent,
|
|
1045
1080
|
isSingleDayWorkingLocation,
|
|
@@ -1061,6 +1096,49 @@ export function EventDetailPopover({
|
|
|
1061
1096
|
],
|
|
1062
1097
|
);
|
|
1063
1098
|
|
|
1099
|
+
const handleWorkingLocationAllDayChange = useCallback(
|
|
1100
|
+
(nextAllDay: boolean) => {
|
|
1101
|
+
const nextStartTime =
|
|
1102
|
+
!nextAllDay && editStartTime === "00:00" && editEndTime === "00:00"
|
|
1103
|
+
? "09:00"
|
|
1104
|
+
: editStartTime;
|
|
1105
|
+
const nextEndTime =
|
|
1106
|
+
!nextAllDay && editStartTime === "00:00" && editEndTime === "00:00"
|
|
1107
|
+
? "17:00"
|
|
1108
|
+
: editEndTime;
|
|
1109
|
+
const nextEndDate = nextAllDay
|
|
1110
|
+
? inclusiveEndDateForAllDayConversion(
|
|
1111
|
+
editDate,
|
|
1112
|
+
editEndDate,
|
|
1113
|
+
editEndTime,
|
|
1114
|
+
)
|
|
1115
|
+
: editEndDate < editDate
|
|
1116
|
+
? editDate
|
|
1117
|
+
: editEndDate;
|
|
1118
|
+
setEditEndDate(nextEndDate);
|
|
1119
|
+
setEditStartTime(nextStartTime);
|
|
1120
|
+
setEditEndTime(nextEndTime);
|
|
1121
|
+
saveTimeValues(
|
|
1122
|
+
{
|
|
1123
|
+
date: editDate,
|
|
1124
|
+
endDate: nextEndDate,
|
|
1125
|
+
startTime: nextStartTime,
|
|
1126
|
+
endTime: nextEndTime,
|
|
1127
|
+
timezone: editTimezone,
|
|
1128
|
+
},
|
|
1129
|
+
nextAllDay,
|
|
1130
|
+
);
|
|
1131
|
+
},
|
|
1132
|
+
[
|
|
1133
|
+
editDate,
|
|
1134
|
+
editEndDate,
|
|
1135
|
+
editEndTime,
|
|
1136
|
+
editStartTime,
|
|
1137
|
+
editTimezone,
|
|
1138
|
+
saveTimeValues,
|
|
1139
|
+
],
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1064
1142
|
const handleInlineTimeChange = useCallback(
|
|
1065
1143
|
(field: "startTime" | "endTime", nextValue: string) => {
|
|
1066
1144
|
let nextDate = editDate;
|
|
@@ -1182,10 +1260,10 @@ export function EventDetailPopover({
|
|
|
1182
1260
|
|
|
1183
1261
|
const handleSelectFindTimeSlot = useCallback(
|
|
1184
1262
|
(slot: FindTimeSlot) => {
|
|
1185
|
-
setEditDate(toDateInputValue(slot.start));
|
|
1186
|
-
setEditEndDate(toDateInputValue(slot.end));
|
|
1187
|
-
setEditStartTime(toTimeInputValue(slot.start));
|
|
1188
|
-
setEditEndTime(toTimeInputValue(slot.end));
|
|
1263
|
+
setEditDate(toDateInputValue(slot.start, findTimeTimezone));
|
|
1264
|
+
setEditEndDate(toDateInputValue(slot.end, findTimeTimezone));
|
|
1265
|
+
setEditStartTime(toTimeInputValue(slot.start, findTimeTimezone));
|
|
1266
|
+
setEditEndTime(toTimeInputValue(slot.end, findTimeTimezone));
|
|
1189
1267
|
setEditTimezone(findTimeTimezone);
|
|
1190
1268
|
setEditingField(null);
|
|
1191
1269
|
setFindTimeOpen(false);
|
|
@@ -1312,6 +1390,7 @@ export function EventDetailPopover({
|
|
|
1312
1390
|
|
|
1313
1391
|
const handleCreateDraft = useCallback(() => {
|
|
1314
1392
|
if (!onDraftCreate) return;
|
|
1393
|
+
if (!isWorkingLocationDraftReadyToCreate(event)) return;
|
|
1315
1394
|
const title = editingTitle.trim();
|
|
1316
1395
|
const updates = isEditingTitle && title ? { title } : undefined;
|
|
1317
1396
|
if (updates) {
|
|
@@ -1586,11 +1665,29 @@ export function EventDetailPopover({
|
|
|
1586
1665
|
<div className="flex items-start gap-3 rounded-md py-1.5">
|
|
1587
1666
|
<IconClock className="mt-1 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
1588
1667
|
<div className="min-w-0 flex-1">
|
|
1668
|
+
{isWorkingLocation && (
|
|
1669
|
+
<div className="mb-1 flex items-center gap-2">
|
|
1670
|
+
<Switch
|
|
1671
|
+
id={`working-location-all-day-${event.id}`}
|
|
1672
|
+
checked={event.allDay}
|
|
1673
|
+
onCheckedChange={handleWorkingLocationAllDayChange}
|
|
1674
|
+
disabled={updateEvent.isPending}
|
|
1675
|
+
/>
|
|
1676
|
+
<Label
|
|
1677
|
+
htmlFor={`working-location-all-day-${event.id}`}
|
|
1678
|
+
className="text-xs"
|
|
1679
|
+
>
|
|
1680
|
+
{t("eventForm.allDay")}
|
|
1681
|
+
</Label>
|
|
1682
|
+
</div>
|
|
1683
|
+
)}
|
|
1589
1684
|
{event.allDay ? (
|
|
1590
1685
|
<div className="flex flex-wrap items-center gap-1 text-sm">
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1686
|
+
{!isWorkingLocation && (
|
|
1687
|
+
<span className="text-muted-foreground">
|
|
1688
|
+
{t("eventForm.allDay")}
|
|
1689
|
+
</span>
|
|
1690
|
+
)}
|
|
1594
1691
|
<DatePickerPopover
|
|
1595
1692
|
value={editDate}
|
|
1596
1693
|
label={t("eventForm.startDate")}
|
|
@@ -1598,7 +1695,7 @@ export function EventDetailPopover({
|
|
|
1598
1695
|
handleInlineDateChange("date", value)
|
|
1599
1696
|
}
|
|
1600
1697
|
/>
|
|
1601
|
-
{editEndDate !== editDate && (
|
|
1698
|
+
{(isWorkingLocation || editEndDate !== editDate) && (
|
|
1602
1699
|
<>
|
|
1603
1700
|
<span className="text-muted-foreground/50">→</span>
|
|
1604
1701
|
<DatePickerPopover
|
|
@@ -1715,7 +1812,9 @@ export function EventDetailPopover({
|
|
|
1715
1812
|
onOpenChange={setFindTimeOpen}
|
|
1716
1813
|
title={t("eventForm.findTime")}
|
|
1717
1814
|
subtitle={event.title}
|
|
1718
|
-
date={
|
|
1815
|
+
date={
|
|
1816
|
+
editDate || toDateInputValue(event.start, findTimeTimezone)
|
|
1817
|
+
}
|
|
1719
1818
|
timezone={findTimeTimezone}
|
|
1720
1819
|
durationMinutes={findTimeDurationMinutes}
|
|
1721
1820
|
attendees={schedulingAttendees}
|
|
@@ -1733,6 +1832,7 @@ export function EventDetailPopover({
|
|
|
1733
1832
|
<WorkingLocationEditor
|
|
1734
1833
|
event={event}
|
|
1735
1834
|
isRecurring={isRecurringEvent}
|
|
1835
|
+
isDraft={isDraft}
|
|
1736
1836
|
readOnly={isOverlay}
|
|
1737
1837
|
disabled={updateEvent.isPending}
|
|
1738
1838
|
onSave={handleSaveWorkingLocation}
|
|
@@ -2422,6 +2522,7 @@ export function EventDetailPopover({
|
|
|
2422
2522
|
<Button
|
|
2423
2523
|
size="sm"
|
|
2424
2524
|
className="ml-auto text-xs"
|
|
2525
|
+
disabled={!isWorkingLocationDraftReadyToCreate(event)}
|
|
2425
2526
|
onClick={handleCreateDraft}
|
|
2426
2527
|
>
|
|
2427
2528
|
{event.attendees?.length
|
|
@@ -1,23 +1,35 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
1
2
|
import type { CalendarEvent } from "@shared/api";
|
|
2
3
|
import { getWeekdayOrder } from "@shared/calendar-week";
|
|
4
|
+
import { IconPlus } from "@tabler/icons-react";
|
|
3
5
|
import {
|
|
4
6
|
startOfMonth,
|
|
5
7
|
endOfMonth,
|
|
6
8
|
startOfWeek,
|
|
7
9
|
endOfWeek,
|
|
8
10
|
eachDayOfInterval,
|
|
9
|
-
startOfDay,
|
|
10
|
-
addDays,
|
|
11
11
|
isSameMonth,
|
|
12
12
|
isSameDay,
|
|
13
|
-
isToday,
|
|
14
13
|
format,
|
|
15
|
-
parseISO,
|
|
16
14
|
} from "date-fns";
|
|
17
15
|
import { memo, useState, useMemo } from "react";
|
|
18
16
|
|
|
17
|
+
import {
|
|
18
|
+
Tooltip,
|
|
19
|
+
TooltipContent,
|
|
20
|
+
TooltipTrigger,
|
|
21
|
+
} from "@/components/ui/tooltip";
|
|
19
22
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
20
23
|
import { useViewPreferences } from "@/hooks/use-view-preferences";
|
|
24
|
+
import {
|
|
25
|
+
dateToCalendarDateKey,
|
|
26
|
+
addCalendarDays,
|
|
27
|
+
dateKeyToDate,
|
|
28
|
+
eventOverlapsCalendarDay,
|
|
29
|
+
getBrowserTimezone,
|
|
30
|
+
getDateKeyInTimezone,
|
|
31
|
+
getEventDateKey,
|
|
32
|
+
} from "@/lib/calendar-timezone";
|
|
21
33
|
import { getFullDayOutOfOfficeDateRange } from "@/lib/out-of-office";
|
|
22
34
|
import { shouldSuppressAfterPopoverClose } from "@/lib/popover-click-guard";
|
|
23
35
|
import { cn } from "@/lib/utils";
|
|
@@ -28,7 +40,9 @@ import { EventDetailPopover } from "./EventDetailPopover";
|
|
|
28
40
|
interface MonthViewProps {
|
|
29
41
|
events: CalendarEvent[];
|
|
30
42
|
selectedDate: Date;
|
|
43
|
+
timezone?: string;
|
|
31
44
|
onDateSelect: (date: Date) => void;
|
|
45
|
+
onCreateWorkingLocation?: (date: Date) => void;
|
|
32
46
|
onDeleteEvent?: (eventId: string) => void;
|
|
33
47
|
onEventDrop?: (eventId: string, newDate: Date) => void;
|
|
34
48
|
draftEventIds?: string[];
|
|
@@ -79,7 +93,9 @@ interface DayOccurrence {
|
|
|
79
93
|
export const MonthView = memo(function MonthView({
|
|
80
94
|
events,
|
|
81
95
|
selectedDate,
|
|
96
|
+
timezone = getBrowserTimezone(),
|
|
82
97
|
onDateSelect,
|
|
98
|
+
onCreateWorkingLocation,
|
|
83
99
|
onDeleteEvent,
|
|
84
100
|
onEventDrop,
|
|
85
101
|
draftEventIds = [],
|
|
@@ -89,6 +105,7 @@ export const MonthView = memo(function MonthView({
|
|
|
89
105
|
isLoading = false,
|
|
90
106
|
weekStartsOn = 0,
|
|
91
107
|
}: MonthViewProps) {
|
|
108
|
+
const t = useT();
|
|
92
109
|
const isMobile = useIsMobile();
|
|
93
110
|
const { prefs } = useViewPreferences();
|
|
94
111
|
const [dragOverDay, setDragOverDay] = useState<string | null>(null);
|
|
@@ -119,7 +136,7 @@ export const MonthView = memo(function MonthView({
|
|
|
119
136
|
const fullDayOutOfOfficeRange = getFullDayOutOfOfficeDateRange(e);
|
|
120
137
|
if (fullDayOutOfOfficeRange) {
|
|
121
138
|
for (const day of days) {
|
|
122
|
-
const key =
|
|
139
|
+
const key = dateToCalendarDateKey(day);
|
|
123
140
|
if (
|
|
124
141
|
key < fullDayOutOfOfficeRange.startDate ||
|
|
125
142
|
key >= fullDayOutOfOfficeRange.endDateExclusive
|
|
@@ -130,7 +147,7 @@ export const MonthView = memo(function MonthView({
|
|
|
130
147
|
event: e,
|
|
131
148
|
isStart: key === fullDayOutOfOfficeRange.startDate,
|
|
132
149
|
continuesNext:
|
|
133
|
-
|
|
150
|
+
addCalendarDays(key, 1) <
|
|
134
151
|
fullDayOutOfOfficeRange.endDateExclusive,
|
|
135
152
|
};
|
|
136
153
|
const list = map.get(key);
|
|
@@ -139,17 +156,17 @@ export const MonthView = memo(function MonthView({
|
|
|
139
156
|
}
|
|
140
157
|
continue;
|
|
141
158
|
}
|
|
142
|
-
const evStart = parseISO(e.start);
|
|
143
|
-
const evEnd = e.end ? parseISO(e.end) : addDays(evStart, 1);
|
|
144
159
|
for (const day of days) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (evStart < dayEnd && evEnd > dayStart) {
|
|
148
|
-
const key = format(day, "yyyy-MM-dd");
|
|
160
|
+
if (eventOverlapsCalendarDay(e, day, timezone)) {
|
|
161
|
+
const key = dateToCalendarDateKey(day);
|
|
149
162
|
const occurrence: DayOccurrence = {
|
|
150
163
|
event: e,
|
|
151
|
-
isStart:
|
|
152
|
-
continuesNext:
|
|
164
|
+
isStart: getEventDateKey(e, timezone) === key,
|
|
165
|
+
continuesNext: eventOverlapsCalendarDay(
|
|
166
|
+
e,
|
|
167
|
+
dateKeyToDate(addCalendarDays(key, 1)),
|
|
168
|
+
timezone,
|
|
169
|
+
),
|
|
153
170
|
};
|
|
154
171
|
const list = map.get(key);
|
|
155
172
|
if (list) list.push(occurrence);
|
|
@@ -158,7 +175,7 @@ export const MonthView = memo(function MonthView({
|
|
|
158
175
|
}
|
|
159
176
|
}
|
|
160
177
|
return map;
|
|
161
|
-
}, [events,
|
|
178
|
+
}, [days, events, timezone]);
|
|
162
179
|
|
|
163
180
|
function handleDragOver(e: React.DragEvent, dayKey: string) {
|
|
164
181
|
e.preventDefault();
|
|
@@ -200,11 +217,13 @@ export const MonthView = memo(function MonthView({
|
|
|
200
217
|
>
|
|
201
218
|
{days.map((day) => {
|
|
202
219
|
const dayOccurrences =
|
|
203
|
-
eventsByDay.get(
|
|
220
|
+
eventsByDay.get(dateToCalendarDateKey(day)) ?? [];
|
|
204
221
|
const inMonth = isSameMonth(day, selectedDate);
|
|
205
|
-
const today =
|
|
222
|
+
const today =
|
|
223
|
+
dateToCalendarDateKey(day) ===
|
|
224
|
+
getDateKeyInTimezone(new Date(), timezone);
|
|
206
225
|
const selected = isSameDay(day, selectedDate);
|
|
207
|
-
const dayKey = day
|
|
226
|
+
const dayKey = dateToCalendarDateKey(day);
|
|
208
227
|
const isDragTarget = dragOverDay === dayKey;
|
|
209
228
|
|
|
210
229
|
return (
|
|
@@ -249,11 +268,25 @@ export const MonthView = memo(function MonthView({
|
|
|
249
268
|
{format(day, "d")}
|
|
250
269
|
</span>
|
|
251
270
|
|
|
252
|
-
{
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
271
|
+
{inMonth && onCreateWorkingLocation && (
|
|
272
|
+
<Tooltip>
|
|
273
|
+
<TooltipTrigger asChild>
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
aria-label={t("calendarView.addWorkingLocation")}
|
|
277
|
+
className="mr-0.5 flex size-5 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover:opacity-100"
|
|
278
|
+
onClick={(event) => {
|
|
279
|
+
event.stopPropagation();
|
|
280
|
+
onCreateWorkingLocation(day);
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<IconPlus aria-hidden="true" className="size-3.5" />
|
|
284
|
+
</button>
|
|
285
|
+
</TooltipTrigger>
|
|
286
|
+
<TooltipContent side="top">
|
|
287
|
+
{t("calendarView.addWorkingLocation")}
|
|
288
|
+
</TooltipContent>
|
|
289
|
+
</Tooltip>
|
|
257
290
|
)}
|
|
258
291
|
</div>
|
|
259
292
|
|
|
@@ -274,6 +307,7 @@ export const MonthView = memo(function MonthView({
|
|
|
274
307
|
<EventDetailPopover
|
|
275
308
|
key={event.id}
|
|
276
309
|
event={event}
|
|
310
|
+
timezone={timezone}
|
|
277
311
|
onDelete={onDeleteEvent ?? (() => {})}
|
|
278
312
|
isDraft={draftEventIds.includes(event.id)}
|
|
279
313
|
onDraftUpdate={onDraftUpdate}
|
|
@@ -8,6 +8,7 @@ import { EventDetailPopover } from "./EventDetailPopover";
|
|
|
8
8
|
interface OutOfOfficeEventProps {
|
|
9
9
|
event: CalendarEvent;
|
|
10
10
|
day: Date;
|
|
11
|
+
timezone?: string;
|
|
11
12
|
hourHeight: number;
|
|
12
13
|
color: string;
|
|
13
14
|
label: string;
|
|
@@ -51,6 +52,7 @@ interface OutOfOfficeEventProps {
|
|
|
51
52
|
export function OutOfOfficeEvent({
|
|
52
53
|
event,
|
|
53
54
|
day,
|
|
55
|
+
timezone,
|
|
54
56
|
hourHeight,
|
|
55
57
|
color,
|
|
56
58
|
label,
|
|
@@ -76,7 +78,7 @@ export function OutOfOfficeEvent({
|
|
|
76
78
|
onDraftDiscard,
|
|
77
79
|
onOpenChange,
|
|
78
80
|
}: OutOfOfficeEventProps) {
|
|
79
|
-
const segment = getOutOfOfficeSegment(event, day);
|
|
81
|
+
const segment = getOutOfOfficeSegment(event, day, timezone);
|
|
80
82
|
const hasDragOverride =
|
|
81
83
|
isBeingDragged &&
|
|
82
84
|
isDragTargetDay &&
|
|
@@ -114,6 +116,7 @@ export function OutOfOfficeEvent({
|
|
|
114
116
|
</div>
|
|
115
117
|
<EventDetailPopover
|
|
116
118
|
event={event}
|
|
119
|
+
timezone={timezone}
|
|
117
120
|
onDelete={onDelete}
|
|
118
121
|
isDraft={isDraft}
|
|
119
122
|
defaultOpen={defaultOpen}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogAction,
|
|
6
|
+
AlertDialogCancel,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
} from "@/components/ui/alert-dialog";
|
|
13
|
+
|
|
14
|
+
interface TimezoneSwitchDialogProps {
|
|
15
|
+
open: boolean;
|
|
16
|
+
savedTimezone: string;
|
|
17
|
+
browserTimezone: string;
|
|
18
|
+
isSwitching?: boolean;
|
|
19
|
+
onKeep: () => void;
|
|
20
|
+
onSwitch: () => void;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TimezoneSwitchDialog({
|
|
25
|
+
open,
|
|
26
|
+
savedTimezone,
|
|
27
|
+
browserTimezone,
|
|
28
|
+
isSwitching = false,
|
|
29
|
+
onKeep,
|
|
30
|
+
onSwitch,
|
|
31
|
+
onOpenChange,
|
|
32
|
+
}: TimezoneSwitchDialogProps) {
|
|
33
|
+
const t = useT();
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
37
|
+
<AlertDialogContent>
|
|
38
|
+
<AlertDialogHeader>
|
|
39
|
+
<AlertDialogTitle>
|
|
40
|
+
{t("calendarView.timezoneSwitchTitle")}
|
|
41
|
+
</AlertDialogTitle>
|
|
42
|
+
<AlertDialogDescription>
|
|
43
|
+
{t("calendarView.timezoneSwitchDescription", {
|
|
44
|
+
savedTimezone,
|
|
45
|
+
browserTimezone,
|
|
46
|
+
})}
|
|
47
|
+
</AlertDialogDescription>
|
|
48
|
+
</AlertDialogHeader>
|
|
49
|
+
<AlertDialogFooter>
|
|
50
|
+
<AlertDialogCancel onClick={onKeep}>
|
|
51
|
+
{t("calendarView.timezoneSwitchKeep", {
|
|
52
|
+
timezone: savedTimezone,
|
|
53
|
+
})}
|
|
54
|
+
</AlertDialogCancel>
|
|
55
|
+
<AlertDialogAction
|
|
56
|
+
disabled={isSwitching}
|
|
57
|
+
onClick={(event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
onSwitch();
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{t("calendarView.timezoneSwitchSwitch", {
|
|
63
|
+
timezone: browserTimezone,
|
|
64
|
+
})}
|
|
65
|
+
</AlertDialogAction>
|
|
66
|
+
</AlertDialogFooter>
|
|
67
|
+
</AlertDialogContent>
|
|
68
|
+
</AlertDialog>
|
|
69
|
+
);
|
|
70
|
+
}
|