@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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { CalendarEvent } from "@shared/api";
|
|
2
|
+
import {
|
|
3
|
+
addDays,
|
|
4
|
+
endOfMonth,
|
|
5
|
+
endOfWeek,
|
|
6
|
+
format,
|
|
7
|
+
startOfMonth,
|
|
8
|
+
startOfWeek,
|
|
9
|
+
} from "date-fns";
|
|
10
|
+
|
|
11
|
+
import { dateTimeInTimezoneToIso } from "./event-form-utils";
|
|
12
|
+
|
|
13
|
+
export const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
14
|
+
|
|
15
|
+
export interface CalendarDateTimeParts {
|
|
16
|
+
date: string;
|
|
17
|
+
hour: number;
|
|
18
|
+
minute: number;
|
|
19
|
+
second: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CalendarDayBounds {
|
|
23
|
+
date: string;
|
|
24
|
+
start: Date;
|
|
25
|
+
end: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CalendarEventSegment {
|
|
29
|
+
topMinutes: number;
|
|
30
|
+
durationMinutes: number;
|
|
31
|
+
startsOnDay: boolean;
|
|
32
|
+
endsOnDay: boolean;
|
|
33
|
+
startMinutes: number;
|
|
34
|
+
endMinutes: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getBrowserTimezone(): string {
|
|
38
|
+
try {
|
|
39
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
40
|
+
} catch {
|
|
41
|
+
return "UTC";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isValidTimezone(timezone: string): boolean {
|
|
46
|
+
try {
|
|
47
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof RangeError) return false;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeTimezone(timezone?: string): string {
|
|
56
|
+
return timezone && isValidTimezone(timezone)
|
|
57
|
+
? timezone
|
|
58
|
+
: getBrowserTimezone();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Date carriers are kept at local noon so browser DST never changes their date. */
|
|
62
|
+
export function dateKeyToDate(date: string): Date {
|
|
63
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
64
|
+
return new Date(year, month - 1, day, 12, 0, 0, 0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function dateToCalendarDateKey(date: Date): string {
|
|
68
|
+
return format(date, "yyyy-MM-dd");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function addCalendarDays(date: string, amount: number): string {
|
|
72
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
73
|
+
const next = new Date(Date.UTC(year, month - 1, day + amount));
|
|
74
|
+
return next.toISOString().slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function dateTimeParts(value: Date | string, timezone: string) {
|
|
78
|
+
const parsed = value instanceof Date ? value : new Date(value);
|
|
79
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
83
|
+
timeZone: timezone,
|
|
84
|
+
hourCycle: "h23",
|
|
85
|
+
year: "numeric",
|
|
86
|
+
month: "2-digit",
|
|
87
|
+
day: "2-digit",
|
|
88
|
+
hour: "2-digit",
|
|
89
|
+
minute: "2-digit",
|
|
90
|
+
second: "2-digit",
|
|
91
|
+
}).formatToParts(parsed);
|
|
92
|
+
const valueFor = (type: Intl.DateTimeFormatPartTypes) =>
|
|
93
|
+
parts.find((part) => part.type === type)?.value;
|
|
94
|
+
const year = valueFor("year");
|
|
95
|
+
const month = valueFor("month");
|
|
96
|
+
const day = valueFor("day");
|
|
97
|
+
const hour = valueFor("hour");
|
|
98
|
+
const minute = valueFor("minute");
|
|
99
|
+
const second = valueFor("second");
|
|
100
|
+
if (!year || !month || !day || !hour || !minute || !second) return null;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
date: `${year}-${month}-${day}`,
|
|
104
|
+
hour: Number(hour),
|
|
105
|
+
minute: Number(minute),
|
|
106
|
+
second: Number(second),
|
|
107
|
+
} satisfies CalendarDateTimeParts;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof RangeError) return null;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getDateTimePartsInTimezone(
|
|
115
|
+
value: Date | string,
|
|
116
|
+
timezone: string,
|
|
117
|
+
): CalendarDateTimeParts | null {
|
|
118
|
+
return dateTimeParts(value, normalizeTimezone(timezone));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getDateKeyInTimezone(
|
|
122
|
+
value: Date | string,
|
|
123
|
+
timezone: string,
|
|
124
|
+
): string | null {
|
|
125
|
+
return dateTimeParts(value, normalizeTimezone(timezone))?.date ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function dateKeyToTimezoneIso(
|
|
129
|
+
date: string,
|
|
130
|
+
time: string,
|
|
131
|
+
timezone: string,
|
|
132
|
+
): string {
|
|
133
|
+
return dateTimeInTimezoneToIso(date, time, normalizeTimezone(timezone));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getCalendarDayBounds(
|
|
137
|
+
day: Date,
|
|
138
|
+
timezone: string,
|
|
139
|
+
): CalendarDayBounds {
|
|
140
|
+
const normalizedTimezone = normalizeTimezone(timezone);
|
|
141
|
+
const date = dateToCalendarDateKey(day);
|
|
142
|
+
return {
|
|
143
|
+
date,
|
|
144
|
+
start: new Date(dateKeyToTimezoneIso(date, "00:00", normalizedTimezone)),
|
|
145
|
+
end: new Date(
|
|
146
|
+
dateKeyToTimezoneIso(
|
|
147
|
+
addCalendarDays(date, 1),
|
|
148
|
+
"00:00",
|
|
149
|
+
normalizedTimezone,
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getViewDateRange(
|
|
156
|
+
viewMode: "month" | "week" | "day",
|
|
157
|
+
selectedDate: Date,
|
|
158
|
+
timezone: string,
|
|
159
|
+
weekStartsOn: 0 | 1 = 0,
|
|
160
|
+
): { from: string; to: string } {
|
|
161
|
+
const normalizedTimezone = normalizeTimezone(timezone);
|
|
162
|
+
const weekOptions = { weekStartsOn };
|
|
163
|
+
let rangeStart: Date;
|
|
164
|
+
let rangeEndExclusive: Date;
|
|
165
|
+
|
|
166
|
+
if (viewMode === "month") {
|
|
167
|
+
rangeStart = startOfWeek(startOfMonth(selectedDate), weekOptions);
|
|
168
|
+
rangeEndExclusive = addDays(
|
|
169
|
+
endOfWeek(endOfMonth(selectedDate), weekOptions),
|
|
170
|
+
1,
|
|
171
|
+
);
|
|
172
|
+
} else if (viewMode === "week") {
|
|
173
|
+
rangeStart = startOfWeek(selectedDate, weekOptions);
|
|
174
|
+
rangeEndExclusive = addDays(endOfWeek(selectedDate, weekOptions), 1);
|
|
175
|
+
} else {
|
|
176
|
+
rangeStart = selectedDate;
|
|
177
|
+
rangeEndExclusive = addDays(selectedDate, 1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
from: dateKeyToTimezoneIso(
|
|
182
|
+
dateToCalendarDateKey(rangeStart),
|
|
183
|
+
"00:00",
|
|
184
|
+
normalizedTimezone,
|
|
185
|
+
),
|
|
186
|
+
to: dateKeyToTimezoneIso(
|
|
187
|
+
dateToCalendarDateKey(rangeEndExclusive),
|
|
188
|
+
"00:00",
|
|
189
|
+
normalizedTimezone,
|
|
190
|
+
),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function dateOnlyPart(value: string | undefined): string | null {
|
|
195
|
+
if (!value) return null;
|
|
196
|
+
if (DATE_ONLY_PATTERN.test(value)) return value;
|
|
197
|
+
return value.slice(0, 10);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function eventDateRange(
|
|
201
|
+
event: Pick<CalendarEvent, "start" | "end" | "allDay">,
|
|
202
|
+
timezone: string,
|
|
203
|
+
) {
|
|
204
|
+
if (event.allDay) {
|
|
205
|
+
const startDate = dateOnlyPart(event.start);
|
|
206
|
+
const endDate =
|
|
207
|
+
dateOnlyPart(event.end) ??
|
|
208
|
+
(startDate ? addCalendarDays(startDate, 1) : null);
|
|
209
|
+
return startDate && endDate ? { startDate, endDate } : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const start = getDateTimePartsInTimezone(event.start, timezone);
|
|
213
|
+
const end = getDateTimePartsInTimezone(event.end, timezone);
|
|
214
|
+
return start && end ? { startDate: start.date, endDate: end.date } : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function getEventDateKey(
|
|
218
|
+
event: Pick<CalendarEvent, "start" | "end" | "allDay">,
|
|
219
|
+
timezone: string,
|
|
220
|
+
): string | null {
|
|
221
|
+
return eventDateRange(event, timezone)?.startDate ?? null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function moveEventToCalendarDate(
|
|
225
|
+
event: Pick<CalendarEvent, "start" | "end" | "allDay">,
|
|
226
|
+
targetDate: Date,
|
|
227
|
+
timezone: string,
|
|
228
|
+
): { start: string; end: string } | null {
|
|
229
|
+
const targetDateKey = dateToCalendarDateKey(targetDate);
|
|
230
|
+
const sourceStartDate = getEventDateKey(event, timezone);
|
|
231
|
+
if (!sourceStartDate) return null;
|
|
232
|
+
|
|
233
|
+
if (event.allDay) {
|
|
234
|
+
const sourceEndDate =
|
|
235
|
+
dateOnlyPart(event.end) ?? addCalendarDays(sourceStartDate, 1);
|
|
236
|
+
const spanDays = Math.max(
|
|
237
|
+
1,
|
|
238
|
+
Math.round(
|
|
239
|
+
(dateKeyToDate(sourceEndDate).getTime() -
|
|
240
|
+
dateKeyToDate(sourceStartDate).getTime()) /
|
|
241
|
+
(24 * 60 * 60 * 1000),
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
return {
|
|
245
|
+
start: targetDateKey,
|
|
246
|
+
end: addCalendarDays(targetDateKey, spanDays),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const start = getDateTimePartsInTimezone(event.start, timezone);
|
|
251
|
+
const end = getDateTimePartsInTimezone(event.end, timezone);
|
|
252
|
+
if (!start || !end) return null;
|
|
253
|
+
const spanDays = Math.max(
|
|
254
|
+
0,
|
|
255
|
+
Math.round(
|
|
256
|
+
(dateKeyToDate(end.date).getTime() -
|
|
257
|
+
dateKeyToDate(start.date).getTime()) /
|
|
258
|
+
(24 * 60 * 60 * 1000),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
const startTime = `${String(start.hour).padStart(2, "0")}:${String(
|
|
262
|
+
start.minute,
|
|
263
|
+
).padStart(2, "0")}`;
|
|
264
|
+
const endTime = `${String(end.hour).padStart(2, "0")}:${String(
|
|
265
|
+
end.minute,
|
|
266
|
+
).padStart(2, "0")}`;
|
|
267
|
+
return {
|
|
268
|
+
start: dateKeyToTimezoneIso(targetDateKey, startTime, timezone),
|
|
269
|
+
end: dateKeyToTimezoneIso(
|
|
270
|
+
addCalendarDays(targetDateKey, spanDays),
|
|
271
|
+
endTime,
|
|
272
|
+
timezone,
|
|
273
|
+
),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function eventOverlapsCalendarDay(
|
|
278
|
+
event: Pick<CalendarEvent, "start" | "end" | "allDay">,
|
|
279
|
+
day: Date,
|
|
280
|
+
timezone: string,
|
|
281
|
+
): boolean {
|
|
282
|
+
const normalizedTimezone = normalizeTimezone(timezone);
|
|
283
|
+
const dayBounds = getCalendarDayBounds(day, normalizedTimezone);
|
|
284
|
+
if (event.allDay) {
|
|
285
|
+
const range = eventDateRange(event, normalizedTimezone);
|
|
286
|
+
const dayDate = dayBounds.date;
|
|
287
|
+
return Boolean(
|
|
288
|
+
range && range.startDate <= dayDate && range.endDate > dayDate,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const start = new Date(event.start);
|
|
293
|
+
const end = new Date(event.end);
|
|
294
|
+
return (
|
|
295
|
+
!Number.isNaN(start.getTime()) &&
|
|
296
|
+
!Number.isNaN(end.getTime()) &&
|
|
297
|
+
start < dayBounds.end &&
|
|
298
|
+
end > dayBounds.start
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function clockMinutes(parts: CalendarDateTimeParts): number {
|
|
303
|
+
return parts.hour * 60 + parts.minute + parts.second / 60;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function getEventSegmentForCalendarDay(
|
|
307
|
+
event: Pick<CalendarEvent, "start" | "end">,
|
|
308
|
+
day: Date,
|
|
309
|
+
timezone: string,
|
|
310
|
+
): CalendarEventSegment | null {
|
|
311
|
+
const normalizedTimezone = normalizeTimezone(timezone);
|
|
312
|
+
const dayBounds = getCalendarDayBounds(day, normalizedTimezone);
|
|
313
|
+
const rawStart = new Date(event.start);
|
|
314
|
+
const rawEnd = new Date(event.end);
|
|
315
|
+
if (
|
|
316
|
+
Number.isNaN(rawStart.getTime()) ||
|
|
317
|
+
Number.isNaN(rawEnd.getTime()) ||
|
|
318
|
+
rawStart >= dayBounds.end ||
|
|
319
|
+
rawEnd <= dayBounds.start
|
|
320
|
+
) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const start = getDateTimePartsInTimezone(rawStart, normalizedTimezone);
|
|
325
|
+
const end = getDateTimePartsInTimezone(rawEnd, normalizedTimezone);
|
|
326
|
+
if (!start || !end) return null;
|
|
327
|
+
|
|
328
|
+
const startMinutes =
|
|
329
|
+
start.date < dayBounds.date
|
|
330
|
+
? 0
|
|
331
|
+
: start.date > dayBounds.date
|
|
332
|
+
? 24 * 60
|
|
333
|
+
: clockMinutes(start);
|
|
334
|
+
const endMinutes =
|
|
335
|
+
end.date > dayBounds.date
|
|
336
|
+
? 24 * 60
|
|
337
|
+
: end.date < dayBounds.date
|
|
338
|
+
? 0
|
|
339
|
+
: clockMinutes(end);
|
|
340
|
+
const visibleStart = Math.max(0, Math.min(24 * 60, startMinutes));
|
|
341
|
+
const visibleEnd = Math.max(visibleStart, Math.min(24 * 60, endMinutes));
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
topMinutes: visibleStart,
|
|
345
|
+
durationMinutes: Math.max(1, visibleEnd - visibleStart),
|
|
346
|
+
startsOnDay: start.date === dayBounds.date,
|
|
347
|
+
endsOnDay: end.date === dayBounds.date,
|
|
348
|
+
startMinutes: visibleStart,
|
|
349
|
+
endMinutes: visibleEnd,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { CalendarEvent } from "@shared/api";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getBrowserTimezone,
|
|
5
|
+
getEventSegmentForCalendarDay,
|
|
6
|
+
} from "@/lib/calendar-timezone";
|
|
3
7
|
|
|
4
8
|
export interface TimedEventLayout {
|
|
5
9
|
left: number;
|
|
@@ -35,21 +39,23 @@ function overlaps(a: EventBounds, b: EventBounds): boolean {
|
|
|
35
39
|
export function computeTimedEventLayout(
|
|
36
40
|
dayEvents: readonly CalendarEvent[],
|
|
37
41
|
day: Date,
|
|
42
|
+
timezone: string = getBrowserTimezone(),
|
|
38
43
|
): Map<string, TimedEventLayout> {
|
|
39
44
|
const result = new Map<string, TimedEventLayout>();
|
|
40
45
|
if (dayEvents.length === 0) return result;
|
|
41
46
|
|
|
42
|
-
const dayStartMs = startOfDay(day).getTime();
|
|
43
|
-
const dayEndMs = addDays(startOfDay(day), 1).getTime();
|
|
44
47
|
const entries = dayEvents.map<EventEntry>((event, inputOrder) => {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const start = Math.min(dayEndMs, Math.max(dayStartMs, rawStart));
|
|
48
|
+
const segment = getEventSegmentForCalendarDay(event, day, timezone);
|
|
49
|
+
const start = segment?.startMinutes ?? 0;
|
|
48
50
|
// Match the card renderer's minimum height so tiny adjacent events get
|
|
49
51
|
// collision-aware placement even when their raw times barely overlap.
|
|
50
52
|
const end = Math.min(
|
|
51
|
-
|
|
52
|
-
Math.max(
|
|
53
|
+
24 * 60,
|
|
54
|
+
Math.max(
|
|
55
|
+
start,
|
|
56
|
+
segment?.endMinutes ?? start,
|
|
57
|
+
start + MIN_VISIBLE_EVENT_MINUTES,
|
|
58
|
+
),
|
|
53
59
|
);
|
|
54
60
|
|
|
55
61
|
return { event, bounds: { start, end }, inputOrder };
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { CalendarEvent } from "@shared/api";
|
|
2
|
-
import {
|
|
3
|
-
addDays,
|
|
4
|
-
differenceInMinutes,
|
|
5
|
-
format,
|
|
6
|
-
parseISO,
|
|
7
|
-
startOfDay,
|
|
8
|
-
} from "date-fns";
|
|
9
2
|
|
|
10
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
dateKeyToTimezoneIso,
|
|
5
|
+
dateToCalendarDateKey,
|
|
6
|
+
getBrowserTimezone,
|
|
7
|
+
getDateTimePartsInTimezone,
|
|
8
|
+
getEventSegmentForCalendarDay,
|
|
9
|
+
} from "@/lib/calendar-timezone";
|
|
11
10
|
|
|
12
11
|
export interface OutOfOfficeSegment {
|
|
13
12
|
topMinutes: number;
|
|
@@ -22,34 +21,6 @@ export function isOutOfOfficeEvent(
|
|
|
22
21
|
return event.eventType === "outOfOffice";
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
function localDateTimeParts(value: string, timeZone: string) {
|
|
26
|
-
const date = parseISO(value);
|
|
27
|
-
if (Number.isNaN(date.getTime())) return null;
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
31
|
-
timeZone,
|
|
32
|
-
year: "numeric",
|
|
33
|
-
month: "2-digit",
|
|
34
|
-
day: "2-digit",
|
|
35
|
-
hour: "2-digit",
|
|
36
|
-
minute: "2-digit",
|
|
37
|
-
hourCycle: "h23",
|
|
38
|
-
}).formatToParts(date);
|
|
39
|
-
const valueFor = (type: Intl.DateTimeFormatPartTypes) =>
|
|
40
|
-
parts.find((part) => part.type === type)?.value;
|
|
41
|
-
const year = valueFor("year");
|
|
42
|
-
const month = valueFor("month");
|
|
43
|
-
const day = valueFor("day");
|
|
44
|
-
const hour = valueFor("hour");
|
|
45
|
-
const minute = valueFor("minute");
|
|
46
|
-
if (!year || !month || !day || !hour || !minute) return null;
|
|
47
|
-
return { date: `${year}-${month}-${day}`, hour, minute };
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
24
|
export function isFullDayOutOfOfficeEvent(
|
|
54
25
|
event: Pick<
|
|
55
26
|
CalendarEvent,
|
|
@@ -68,19 +39,15 @@ export function getFullDayOutOfOfficeDateRange(
|
|
|
68
39
|
if (!isOutOfOfficeEvent(event) || event.allDay) return null;
|
|
69
40
|
const timeZone = event.startTimeZone ?? event.endTimeZone;
|
|
70
41
|
if (!timeZone) return null;
|
|
71
|
-
const start =
|
|
72
|
-
const end =
|
|
42
|
+
const start = getDateTimePartsInTimezone(event.start, timeZone);
|
|
43
|
+
const end = getDateTimePartsInTimezone(event.end, timeZone);
|
|
73
44
|
const isFullDay =
|
|
74
45
|
start !== null &&
|
|
75
46
|
end !== null &&
|
|
76
47
|
new Date(event.start).getTime() ===
|
|
77
|
-
new Date(
|
|
78
|
-
dateTimeInTimezoneToIso(start.date, "00:00", timeZone),
|
|
79
|
-
).getTime() &&
|
|
48
|
+
new Date(dateKeyToTimezoneIso(start.date, "00:00", timeZone)).getTime() &&
|
|
80
49
|
new Date(event.end).getTime() ===
|
|
81
|
-
new Date(
|
|
82
|
-
dateTimeInTimezoneToIso(end.date, "00:00", timeZone),
|
|
83
|
-
).getTime() &&
|
|
50
|
+
new Date(dateKeyToTimezoneIso(end.date, "00:00", timeZone)).getTime() &&
|
|
84
51
|
end.date > start.date;
|
|
85
52
|
return isFullDay
|
|
86
53
|
? { startDate: start.date, endDateExclusive: end.date }
|
|
@@ -96,36 +63,37 @@ export function fullDayOutOfOfficeCoversDate(
|
|
|
96
63
|
): boolean {
|
|
97
64
|
const range = getFullDayOutOfOfficeDateRange(event);
|
|
98
65
|
if (!range) return false;
|
|
99
|
-
const dateString =
|
|
66
|
+
const dateString = dateToCalendarDateKey(date);
|
|
100
67
|
return dateString >= range.startDate && dateString < range.endDateExclusive;
|
|
101
68
|
}
|
|
102
69
|
|
|
103
70
|
/** Return the portion of a timed out-of-office event visible on one day. */
|
|
104
71
|
export function getOutOfOfficeSegment(
|
|
105
|
-
event: Pick<CalendarEvent, "start" | "end">,
|
|
72
|
+
event: Pick<CalendarEvent, "start" | "end" | "startTimeZone" | "endTimeZone">,
|
|
106
73
|
day: Date,
|
|
74
|
+
timezone: string = event.startTimeZone ??
|
|
75
|
+
event.endTimeZone ??
|
|
76
|
+
getBrowserTimezone(),
|
|
107
77
|
): OutOfOfficeSegment | null {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
const dayStart = startOfDay(day);
|
|
111
|
-
const dayEnd = addDays(dayStart, 1);
|
|
112
|
-
|
|
113
|
-
if (eventStart >= dayEnd || eventEnd <= dayStart) return null;
|
|
114
|
-
|
|
115
|
-
const segmentStart = eventStart > dayStart ? eventStart : dayStart;
|
|
116
|
-
const segmentEnd = eventEnd < dayEnd ? eventEnd : dayEnd;
|
|
78
|
+
const segment = getEventSegmentForCalendarDay(event, day, timezone);
|
|
79
|
+
if (!segment) return null;
|
|
117
80
|
|
|
118
81
|
return {
|
|
119
|
-
topMinutes:
|
|
120
|
-
durationMinutes:
|
|
121
|
-
startsOnDay:
|
|
122
|
-
endsOnDay:
|
|
82
|
+
topMinutes: segment.topMinutes,
|
|
83
|
+
durationMinutes: segment.durationMinutes,
|
|
84
|
+
startsOnDay: segment.startsOnDay,
|
|
85
|
+
endsOnDay: segment.endsOnDay,
|
|
123
86
|
};
|
|
124
87
|
}
|
|
125
88
|
|
|
126
89
|
export function getFirstVisibleOutOfOfficeDayIndex(
|
|
127
|
-
event: Pick<CalendarEvent, "start" | "end">,
|
|
90
|
+
event: Pick<CalendarEvent, "start" | "end" | "startTimeZone" | "endTimeZone">,
|
|
128
91
|
days: Date[],
|
|
92
|
+
timezone: string = event.startTimeZone ??
|
|
93
|
+
event.endTimeZone ??
|
|
94
|
+
getBrowserTimezone(),
|
|
129
95
|
): number {
|
|
130
|
-
return days.findIndex(
|
|
96
|
+
return days.findIndex(
|
|
97
|
+
(day) => getOutOfOfficeSegment(event, day, timezone) !== null,
|
|
98
|
+
);
|
|
131
99
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CalendarEvent, UpdateEventScope } from "@shared/api";
|
|
2
2
|
|
|
3
|
+
import { eventOverlapsCalendarDay } from "@/lib/calendar-timezone";
|
|
4
|
+
|
|
3
5
|
export type WorkingLocationKind =
|
|
4
6
|
| "homeOffice"
|
|
5
7
|
| "officeLocation"
|
|
@@ -64,14 +66,12 @@ export function getWorkingLocationLabel(
|
|
|
64
66
|
properties.officeLocation?.label ||
|
|
65
67
|
properties.officeLocation?.buildingId ||
|
|
66
68
|
event.location ||
|
|
67
|
-
event.title ||
|
|
68
69
|
labels.office
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
return (
|
|
72
73
|
properties?.customLocation?.label ||
|
|
73
74
|
event.location ||
|
|
74
|
-
event.title ||
|
|
75
75
|
labels.workingLocation
|
|
76
76
|
);
|
|
77
77
|
}
|
|
@@ -147,6 +147,38 @@ export function getWorkingLocationTitle(
|
|
|
147
147
|
: event.title;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
export function isWorkingLocationDraftReadyToCreate(
|
|
151
|
+
event: Pick<
|
|
152
|
+
CalendarEvent,
|
|
153
|
+
"eventType" | "location" | "workingLocationProperties"
|
|
154
|
+
>,
|
|
155
|
+
): boolean {
|
|
156
|
+
if (!isWorkingLocationEvent(event)) return true;
|
|
157
|
+
if (getWorkingLocationType(event) !== "customLocation") return true;
|
|
158
|
+
return getWorkingLocationEditableLabel(event).trim().length > 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function findOwnedWorkingLocationForDay(
|
|
162
|
+
events: readonly CalendarEvent[],
|
|
163
|
+
day: Date,
|
|
164
|
+
timezone: string,
|
|
165
|
+
accountEmail?: string,
|
|
166
|
+
): CalendarEvent | undefined {
|
|
167
|
+
const matches = events.filter((event) => {
|
|
168
|
+
if (!isWorkingLocationEvent(event)) return false;
|
|
169
|
+
if (event.overlayEmail) return false;
|
|
170
|
+
if (
|
|
171
|
+
accountEmail &&
|
|
172
|
+
event.accountEmail &&
|
|
173
|
+
event.accountEmail !== accountEmail
|
|
174
|
+
) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return eventOverlapsCalendarDay(event, day, timezone);
|
|
178
|
+
});
|
|
179
|
+
return matches.find((event) => event.source === "google") ?? matches[0];
|
|
180
|
+
}
|
|
181
|
+
|
|
150
182
|
export function getWorkingLocationDetail(
|
|
151
183
|
event: Pick<CalendarEvent, "workingLocationProperties">,
|
|
152
184
|
labels: WorkingLocationDisplayLabels,
|