feat: implement CalDAV discovery API and enhance calendar ID handling
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
import { getStalwartCredentials } from '@/lib/stalwart/credentials';
|
||||||
|
|
||||||
|
interface DiscoveryAccountRequest {
|
||||||
|
key: string;
|
||||||
|
candidates: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiscoveryResult {
|
||||||
|
url: string | null;
|
||||||
|
resolvedAccount: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPublicUrl(serverUrl: string, path: string): string {
|
||||||
|
return new URL(path, serverUrl).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeCalendarHome(serverUrl: string, authHeader: string, accountName: string): Promise<string | null> {
|
||||||
|
const targetUrl = buildPublicUrl(serverUrl, `/dav/cal/${encodeURIComponent(accountName)}`);
|
||||||
|
const response = await fetch(targetUrl, {
|
||||||
|
method: 'PROPFIND',
|
||||||
|
headers: {
|
||||||
|
Authorization: authHeader,
|
||||||
|
Depth: '0',
|
||||||
|
'Content-Type': 'application/xml; charset=utf-8',
|
||||||
|
},
|
||||||
|
body: `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<D:propfind xmlns:D="DAV:">
|
||||||
|
<D:prop>
|
||||||
|
<D:resourcetype/>
|
||||||
|
<D:displayname/>
|
||||||
|
</D:prop>
|
||||||
|
</D:propfind>`,
|
||||||
|
redirect: 'manual',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 207) {
|
||||||
|
return targetUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status >= 300 && response.status < 400) {
|
||||||
|
const location = response.headers.get('Location');
|
||||||
|
if (location) {
|
||||||
|
return new URL(location, targetUrl).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const creds = await getStalwartCredentials(request);
|
||||||
|
if (!creds) {
|
||||||
|
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
const accounts = Array.isArray(body.accounts) ? body.accounts as DiscoveryAccountRequest[] : [];
|
||||||
|
const wellKnownUrl = buildPublicUrl(creds.serverUrl, '/.well-known/caldav');
|
||||||
|
const discovered: Record<string, DiscoveryResult> = {};
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
if (!account?.key) continue;
|
||||||
|
|
||||||
|
const candidates = Array.from(new Set(
|
||||||
|
(account.candidates || [])
|
||||||
|
.map((candidate) => candidate?.trim())
|
||||||
|
.filter((candidate): candidate is string => Boolean(candidate))
|
||||||
|
));
|
||||||
|
|
||||||
|
let url: string | null = null;
|
||||||
|
let resolvedAccount: string | null = null;
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
url = await probeCalendarHome(creds.serverUrl, creds.authHeader, candidate);
|
||||||
|
if (url) {
|
||||||
|
resolvedAccount = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('CalDAV discovery probe failed', {
|
||||||
|
accountKey: account.key,
|
||||||
|
candidate,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
discovered[account.key] = { url, resolvedAccount };
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
wellKnownUrl,
|
||||||
|
accounts: discovered,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('CalDAV discovery failed', { error: error instanceof Error ? error.message : 'Unknown' });
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { format, parseISO, isToday, isTomorrow } from "date-fns";
|
|||||||
import { Calendar as CalendarIcon, MapPin, Users } from "lucide-react";
|
import { Calendar as CalendarIcon, MapPin, Users } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { parseDuration, getEventColor } from "./event-card";
|
import { parseDuration, getEventColor } from "./event-card";
|
||||||
import { getEventDayBounds } from "@/lib/calendar-utils";
|
import { getEventDayBounds, getPrimaryCalendarId } from "@/lib/calendar-utils";
|
||||||
import { getParticipantCount } from "@/lib/calendar-participants";
|
import { getParticipantCount } from "@/lib/calendar-participants";
|
||||||
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
||||||
|
|
||||||
@@ -113,8 +113,8 @@ export function CalendarAgendaView({
|
|||||||
|
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{group.events.map((ev) => {
|
{group.events.map((ev) => {
|
||||||
const calId = Object.keys(ev.calendarIds)[0];
|
const calId = getPrimaryCalendarId(ev);
|
||||||
const calendar = calendarMap.get(calId);
|
const calendar = calId ? calendarMap.get(calId) : undefined;
|
||||||
const color = getEventColor(ev, calendar);
|
const color = getEventColor(ev, calendar);
|
||||||
const start = parseISO(ev.start);
|
const start = parseISO(ev.start);
|
||||||
const durMin = parseDuration(ev.duration);
|
const durMin = parseDuration(ev.duration);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { format, isToday, parseISO } from "date-fns";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EventCard, parseDuration } from "./event-card";
|
import { EventCard, parseDuration } from "./event-card";
|
||||||
import { QuickEventInput } from "./quick-event-input";
|
import { QuickEventInput } from "./quick-event-input";
|
||||||
import { getEventDayBounds, layoutOverlappingEvents, formatSnapTime } from "@/lib/calendar-utils";
|
import { formatSnapTime, getEventDayBounds, getPrimaryCalendarId, layoutOverlappingEvents } from "@/lib/calendar-utils";
|
||||||
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
||||||
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
|
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
|
||||||
|
|
||||||
@@ -127,12 +127,12 @@ export function CalendarDayView({
|
|||||||
<div className="text-[10px] text-muted-foreground mb-1">{t("events.all_day")}</div>
|
<div className="text-[10px] text-muted-foreground mb-1">{t("events.all_day")}</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{allDayEvents.map((ev) => {
|
{allDayEvents.map((ev) => {
|
||||||
const calId = Object.keys(ev.calendarIds)[0];
|
const calId = getPrimaryCalendarId(ev);
|
||||||
return (
|
return (
|
||||||
<EventCard
|
<EventCard
|
||||||
key={ev.id}
|
key={ev.id}
|
||||||
event={ev}
|
event={ev}
|
||||||
calendar={calendarMap.get(calId)}
|
calendar={calId ? calendarMap.get(calId) : undefined}
|
||||||
variant="chip"
|
variant="chip"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
@@ -192,7 +192,7 @@ export function CalendarDayView({
|
|||||||
const top = (startMin / 60) * HOUR_HEIGHT;
|
const top = (startMin / 60) * HOUR_HEIGHT;
|
||||||
const baseHeight = Math.max(24, (durMin / 60) * HOUR_HEIGHT);
|
const baseHeight = Math.max(24, (durMin / 60) * HOUR_HEIGHT);
|
||||||
const height = resizeVisual?.eventId === ev.id ? resizeVisual.heightPx : baseHeight;
|
const height = resizeVisual?.eventId === ev.id ? resizeVisual.heightPx : baseHeight;
|
||||||
const calId = Object.keys(ev.calendarIds)[0];
|
const calId = getPrimaryCalendarId(ev);
|
||||||
const leftPct = (column / totalColumns) * 100;
|
const leftPct = (column / totalColumns) * 100;
|
||||||
const widthPct = (1 / totalColumns) * 100;
|
const widthPct = (1 / totalColumns) * 100;
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ export function CalendarDayView({
|
|||||||
>
|
>
|
||||||
<EventCard
|
<EventCard
|
||||||
event={ev}
|
event={ev}
|
||||||
calendar={calendarMap.get(calId)}
|
calendar={calId ? calendarMap.get(calId) : undefined}
|
||||||
variant="block"
|
variant="block"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EventCard } from "./event-card";
|
import { EventCard } from "./event-card";
|
||||||
import { buildWeekSegments, getEventDayBounds } from "@/lib/calendar-utils";
|
import { buildWeekSegments, getEventDayBounds, getPrimaryCalendarId } from "@/lib/calendar-utils";
|
||||||
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
||||||
import { useAuthStore } from "@/stores/auth-store";
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
import { useCalendarStore } from "@/stores/calendar-store";
|
import { useCalendarStore } from "@/stores/calendar-store";
|
||||||
@@ -197,8 +197,8 @@ export function CalendarMonthView({
|
|||||||
dayEvents.length > 0 && (
|
dayEvents.length > 0 && (
|
||||||
<div className="flex items-center justify-center gap-0.5 flex-wrap">
|
<div className="flex items-center justify-center gap-0.5 flex-wrap">
|
||||||
{dayEvents.slice(0, 3).map((ev) => {
|
{dayEvents.slice(0, 3).map((ev) => {
|
||||||
const calId = Object.keys(ev.calendarIds)[0];
|
const calId = getPrimaryCalendarId(ev);
|
||||||
const cal = calendarMap.get(calId);
|
const cal = calId ? calendarMap.get(calId) : undefined;
|
||||||
const evColor = ev.color || cal?.color || "#3b82f6";
|
const evColor = ev.color || cal?.color || "#3b82f6";
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -222,7 +222,7 @@ export function CalendarMonthView({
|
|||||||
{!isMobile && segments.length > 0 && (
|
{!isMobile && segments.length > 0 && (
|
||||||
<div className="absolute inset-x-0 pointer-events-none" style={{ top: 30 }}>
|
<div className="absolute inset-x-0 pointer-events-none" style={{ top: 30 }}>
|
||||||
{segments.map((segment) => {
|
{segments.map((segment) => {
|
||||||
const calId = Object.keys(segment.event.calendarIds)[0];
|
const calId = getPrimaryCalendarId(segment.event);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${segment.event.id}-${segment.startIndex}-${segment.row}`}
|
key={`${segment.event.id}-${segment.startIndex}-${segment.row}`}
|
||||||
@@ -236,7 +236,7 @@ export function CalendarMonthView({
|
|||||||
>
|
>
|
||||||
<EventCard
|
<EventCard
|
||||||
event={segment.event}
|
event={segment.event}
|
||||||
calendar={calendarMap.get(calId)}
|
calendar={calId ? calendarMap.get(calId) : undefined}
|
||||||
variant="span"
|
variant="span"
|
||||||
continuesBefore={segment.continuesBefore}
|
continuesBefore={segment.continuesBefore}
|
||||||
continuesAfter={segment.continuesAfter}
|
continuesAfter={segment.continuesAfter}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function CalendarSidebarPanel({
|
|||||||
const shared = calendars.filter(c => c.isShared);
|
const shared = calendars.filter(c => c.isShared);
|
||||||
const groups = new Map<string, { accountName: string; calendars: Calendar[] }>();
|
const groups = new Map<string, { accountName: string; calendars: Calendar[] }>();
|
||||||
for (const cal of shared) {
|
for (const cal of shared) {
|
||||||
const key = cal.accountId!;
|
const key = cal.accountId || cal.accountName || cal.id;
|
||||||
if (!groups.has(key)) {
|
if (!groups.has(key)) {
|
||||||
groups.set(key, { accountName: cal.accountName || key, calendars: [] });
|
groups.set(key, { accountName: cal.accountName || key, calendars: [] });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export function CalendarToolbar({
|
|||||||
const shared = calendars.filter(c => c.isShared);
|
const shared = calendars.filter(c => c.isShared);
|
||||||
const groups = new Map<string, { accountName: string; cals: typeof shared }>();
|
const groups = new Map<string, { accountName: string; cals: typeof shared }>();
|
||||||
for (const c of shared) {
|
for (const c of shared) {
|
||||||
const key = c.accountId!;
|
const key = c.accountId || c.accountName || c.id;
|
||||||
if (!groups.has(key)) groups.set(key, { accountName: c.accountName || key, cals: [] });
|
if (!groups.has(key)) groups.set(key, { accountName: c.accountName || key, cals: [] });
|
||||||
groups.get(key)!.cals.push(c);
|
groups.get(key)!.cals.push(c);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EventCard, parseDuration } from "./event-card";
|
import { EventCard, parseDuration } from "./event-card";
|
||||||
import { QuickEventInput } from "./quick-event-input";
|
import { QuickEventInput } from "./quick-event-input";
|
||||||
import { buildWeekSegments, getEventDayBounds, layoutOverlappingEvents, formatSnapTime } from "@/lib/calendar-utils";
|
import { buildWeekSegments, formatSnapTime, getEventDayBounds, getPrimaryCalendarId, layoutOverlappingEvents } from "@/lib/calendar-utils";
|
||||||
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
import type { CalendarEvent, Calendar } from "@/lib/jmap/types";
|
||||||
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
|
import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ export function CalendarWeekView({
|
|||||||
|
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
{allDaySegments.map((segment) => {
|
{allDaySegments.map((segment) => {
|
||||||
const calId = Object.keys(segment.event.calendarIds)[0];
|
const calId = getPrimaryCalendarId(segment.event);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${segment.event.id}-${segment.startIndex}-${segment.row}`}
|
key={`${segment.event.id}-${segment.startIndex}-${segment.row}`}
|
||||||
@@ -176,7 +176,7 @@ export function CalendarWeekView({
|
|||||||
>
|
>
|
||||||
<EventCard
|
<EventCard
|
||||||
event={segment.event}
|
event={segment.event}
|
||||||
calendar={calendarMap.get(calId)}
|
calendar={calId ? calendarMap.get(calId) : undefined}
|
||||||
variant="span"
|
variant="span"
|
||||||
continuesBefore={segment.continuesBefore}
|
continuesBefore={segment.continuesBefore}
|
||||||
continuesAfter={segment.continuesAfter}
|
continuesAfter={segment.continuesAfter}
|
||||||
@@ -284,7 +284,7 @@ export function CalendarWeekView({
|
|||||||
const top = (startMin / 60) * HOUR_HEIGHT;
|
const top = (startMin / 60) * HOUR_HEIGHT;
|
||||||
const baseHeight = Math.max(20, (durMin / 60) * HOUR_HEIGHT);
|
const baseHeight = Math.max(20, (durMin / 60) * HOUR_HEIGHT);
|
||||||
const height = resizeVisual?.eventId === ev.id ? resizeVisual.heightPx : baseHeight;
|
const height = resizeVisual?.eventId === ev.id ? resizeVisual.heightPx : baseHeight;
|
||||||
const calId = Object.keys(ev.calendarIds)[0];
|
const calId = getPrimaryCalendarId(ev);
|
||||||
const leftPct = (column / totalColumns) * 100;
|
const leftPct = (column / totalColumns) * 100;
|
||||||
const widthPct = (1 / totalColumns) * 100;
|
const widthPct = (1 / totalColumns) * 100;
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ export function CalendarWeekView({
|
|||||||
>
|
>
|
||||||
<EventCard
|
<EventCard
|
||||||
event={ev}
|
event={ev}
|
||||||
calendar={calendarMap.get(calId)}
|
calendar={calId ? calendarMap.get(calId) : undefined}
|
||||||
variant="block"
|
variant="block"
|
||||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||||
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Vid
|
|||||||
import { format, parseISO, addHours, addDays } from "date-fns";
|
import { format, parseISO, addHours, addDays } from "date-fns";
|
||||||
import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types";
|
import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types";
|
||||||
import { parseDuration, getEventColor } from "./event-card";
|
import { parseDuration, getEventColor } from "./event-card";
|
||||||
import { buildAllDayDuration, getEventDisplayEndDate } from "@/lib/calendar-utils";
|
import { buildAllDayDuration, getEventDisplayEndDate, getPrimaryCalendarId } from "@/lib/calendar-utils";
|
||||||
import { ParticipantInput } from "./participant-input";
|
import { ParticipantInput } from "./participant-input";
|
||||||
import {
|
import {
|
||||||
isOrganizer,
|
isOrganizer,
|
||||||
@@ -184,7 +184,7 @@ export function EventModal({
|
|||||||
const [endTime, setEndTime] = useState(formatTimeInput(getInitialEnd()));
|
const [endTime, setEndTime] = useState(formatTimeInput(getInitialEnd()));
|
||||||
const [allDay, setAllDay] = useState(event?.showWithoutTime || false);
|
const [allDay, setAllDay] = useState(event?.showWithoutTime || false);
|
||||||
const [calendarId, setCalendarId] = useState<string>(() => {
|
const [calendarId, setCalendarId] = useState<string>(() => {
|
||||||
if (event?.calendarIds) return Object.keys(event.calendarIds)[0] || calendars[0]?.id || "";
|
if (event?.calendarIds) return getPrimaryCalendarId(event) || calendars[0]?.id || "";
|
||||||
const defaultCal = calendars.find(c => c.isDefault);
|
const defaultCal = calendars.find(c => c.isDefault);
|
||||||
return defaultCal?.id || calendars[0]?.id || "";
|
return defaultCal?.id || calendars[0]?.id || "";
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ export function CalendarManagementSettings() {
|
|||||||
const { client, serverUrl, username } = useAuthStore();
|
const { client, serverUrl, username } = useAuthStore();
|
||||||
const { calendars, updateCalendar, createCalendar, removeCalendar, clearCalendarEvents, fetchCalendars, icalSubscriptions, removeICalSubscription, refreshICalSubscription, isSubscriptionCalendar } = useCalendarStore();
|
const { calendars, updateCalendar, createCalendar, removeCalendar, clearCalendarEvents, fetchCalendars, icalSubscriptions, removeICalSubscription, refreshICalSubscription, isSubscriptionCalendar } = useCalendarStore();
|
||||||
|
|
||||||
|
const [discoveredCalDavUrls, setDiscoveredCalDavUrls] = useState<Record<string, string | null>>({});
|
||||||
|
const [wellKnownCalDavUrl, setWellKnownCalDavUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
@@ -175,6 +178,60 @@ export function CalendarManagementSettings() {
|
|||||||
}
|
}
|
||||||
}, [client, calendars.length, fetchCalendars]);
|
}, [client, calendars.length, fetchCalendars]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!client || !serverUrl || !username) {
|
||||||
|
setDiscoveredCalDavUrls({});
|
||||||
|
setWellKnownCalDavUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryKey = username;
|
||||||
|
const accounts = new Map<string, string[]>();
|
||||||
|
accounts.set(primaryKey, [username]);
|
||||||
|
|
||||||
|
for (const calendar of calendars) {
|
||||||
|
if (!calendar.isShared) continue;
|
||||||
|
const key = calendar.accountId || calendar.accountName || calendar.id;
|
||||||
|
const candidates = accounts.get(key) || [];
|
||||||
|
if (calendar.accountId) candidates.push(calendar.accountId);
|
||||||
|
if (calendar.accountName) candidates.push(calendar.accountName);
|
||||||
|
accounts.set(key, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
fetch('/api/caldav/discover', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
accounts: Array.from(accounts.entries()).map(([key, candidates]) => ({ key, candidates })),
|
||||||
|
}),
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) throw new Error(`CalDAV discovery failed: ${response.status}`);
|
||||||
|
return response.json() as Promise<{
|
||||||
|
wellKnownUrl?: string;
|
||||||
|
accounts?: Record<string, { url: string | null }>;
|
||||||
|
}>;
|
||||||
|
})
|
||||||
|
.then((payload) => {
|
||||||
|
setWellKnownCalDavUrl(payload.wellKnownUrl || null);
|
||||||
|
const next: Record<string, string | null> = {};
|
||||||
|
for (const [key, value] of Object.entries(payload.accounts || {})) {
|
||||||
|
next[key] = value?.url || null;
|
||||||
|
}
|
||||||
|
setDiscoveredCalDavUrls(next);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
const fallbackWellKnown = new URL('/.well-known/caldav', serverUrl).toString();
|
||||||
|
setDiscoveredCalDavUrls({});
|
||||||
|
setWellKnownCalDavUrl(fallbackWellKnown);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [client, calendars, serverUrl, username]);
|
||||||
|
|
||||||
const handleRefreshSubscription = async (subId: string) => {
|
const handleRefreshSubscription = async (subId: string) => {
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
setRefreshingSubId(subId);
|
setRefreshingSubId(subId);
|
||||||
@@ -295,8 +352,10 @@ export function CalendarManagementSettings() {
|
|||||||
|
|
||||||
const buildCalDavUrl = (calendarId: string) => {
|
const buildCalDavUrl = (calendarId: string) => {
|
||||||
if (!serverUrl || !username) return null;
|
if (!serverUrl || !username) return null;
|
||||||
const base = serverUrl.replace(/\/$/, '');
|
const calendar = calendars.find((entry) => entry.id === calendarId);
|
||||||
return `${base}/dav/cal/${encodeURIComponent(username)}/${encodeURIComponent(calendarId)}/`;
|
if (!calendar) return null;
|
||||||
|
const accountKey = calendar.isShared ? (calendar.accountId || calendar.accountName || calendar.id) : username;
|
||||||
|
return discoveredCalDavUrls[accountKey] || wellKnownCalDavUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyUrl = async (url: string) => {
|
const handleCopyUrl = async (url: string) => {
|
||||||
|
|||||||
@@ -142,3 +142,7 @@ export function formatSnapTime(minutes: number, timeFormat: "12h" | "24h"): stri
|
|||||||
}
|
}
|
||||||
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPrimaryCalendarId(event: Pick<CalendarEvent, 'calendarIds'>): string | undefined {
|
||||||
|
return Object.keys(event.calendarIds || {})[0];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user