Implement destructive CalDAV move saga

This commit is contained in:
2026-08-02 17:30:24 +02:00
parent 56d7b2108d
commit a125b666da
16 changed files with 2384 additions and 36 deletions
+40 -6
View File
@@ -58,6 +58,7 @@ import {
} from "./CalendarCollectionDialogs";
import { CalendarEventDialog } from "./CalendarEventDialog";
import { CalendarOutboxDialog } from "./CalendarOutboxDialog";
import { CalendarMigrationDialog } from "./CalendarMigrationDialog";
import {
CalendarTimeGrid,
CalendarWeekRows,
@@ -134,6 +135,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
const [calendarDialog, setCalendarDialog] = useState<CalendarCollectionDialogState | null>(null);
const [calendarDeleteDialog, setCalendarDeleteDialog] = useState<CalendarDeleteDialogState | null>(null);
const [outboxDialog, setOutboxDialog] = useState<{ calendar: CalendarCollection; source: CalendarSyncSource } | null>(null);
const [migrationBatchId, setMigrationBatchId] = useState("");
const [syncingSourceId, setSyncingSourceId] = useState("");
const [continuousViewport, setContinuousViewport] = useState<ContinuousViewport>({ scrollTop: 0, height: 0 });
const [draggingEventId, setDraggingEventId] = useState("");
@@ -238,7 +240,7 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
return () => window.cancelAnimationFrame(frame);
}, [days, mode]);
async function loadCalendars() {
async function loadCalendars(): Promise<CalendarCollection[]> {
setLoading(true);
setError("");
try {
@@ -254,8 +256,10 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
return next.length > 0 ? next : ids;
});
setEventCalendarId((current) => current && ids.includes(current) ? current : ids[0] ?? "");
return response.calendars;
} catch (err) {
setError(errorText(err));
return [];
} finally {
setLoading(false);
}
@@ -387,11 +391,19 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
await deleteCalendar(settings, calendar.id, payload);
setCalendarDialog(null);
setCalendarDeleteDialog(null);
setCalendars((current) => current.filter((item) => item.id !== calendar.id));
setSyncSources((current) => current.filter((item) => item.calendar_id !== calendar.id));
setVisibleCalendarIds((current) => current.filter((id) => id !== calendar.id));
setEventCalendarId((current) => current === calendar.id ? "" : current);
await loadCalendars();
const remoteMove = payload.external_action === "remote_move";
if (!remoteMove) {
setCalendars((current) => current.filter((item) => item.id !== calendar.id));
setSyncSources((current) => current.filter((item) => item.calendar_id !== calendar.id));
setVisibleCalendarIds((current) => current.filter((id) => id !== calendar.id));
setEventCalendarId((current) => current === calendar.id ? "" : current);
}
const refreshedCalendars = await loadCalendars();
if (remoteMove) {
const refreshedSource = refreshedCalendars.find((item) => item.id === calendar.id);
const batchId = refreshedSource ? calendarRemoteMoveBatchId(refreshedSource) : "";
if (batchId) setMigrationBatchId(batchId);
}
if (calendars.length > 1 || payload.event_action === "move") {
await loadEvents();
} else {
@@ -1012,6 +1024,10 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
setOutboxDialog({ calendar: calendarDialog.calendar, source });
}
}}
onOpenMigration={(batchId) => {
setCalendarDialog(null);
setMigrationBatchId(batchId);
}}
onDiscover={handleCalDavDiscovery} />
}
@@ -1022,6 +1038,17 @@ export default function CalendarPage({ settings, auth }: {settings: ApiSettings;
source={outboxDialog.source}
onClose={() => setOutboxDialog(null)} />
}
{migrationBatchId &&
<CalendarMigrationDialog
settings={settings}
batchId={migrationBatchId}
onClose={() => setMigrationBatchId("")}
onChanged={() => {
void loadCalendars();
void loadEvents();
}} />
}
{calendarDeleteDialog &&
<CalendarCollectionDeleteDialog
@@ -1042,6 +1069,13 @@ function compareCalendars(left: CalendarCollection, right: CalendarCollection):
return left.name.localeCompare(right.name);
}
function calendarRemoteMoveBatchId(calendar: CalendarCollection): string {
const remoteMove = calendar.metadata?.remote_move;
if (!remoteMove || typeof remoteMove !== "object" || Array.isArray(remoteMove)) return "";
const batchId = (remoteMove as Record<string, unknown>).batch_id;
return typeof batchId === "string" ? batchId : "";
}
function calendarViewPreferences(
response: CalendarViewPreferencesResponse
): CalendarViewPreferences {