From 06784581aad2bb655d20171f24134a6ee66dc0fc Mon Sep 17 00:00:00 2001 From: Stefan Hildebrandt <695494+hildebrandttk@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:22:26 +0200 Subject: [PATCH] fix(calendar): exclude subscription/read-only calendars from event creation (#762) The rights-first resolver already made EXISTING events in subscription / read-only calendars read-only, but the create path still offered them: event-modal's calendar dropdown listed every calendar. iCal subscriptions are real Stalwart calendars (client-side feed written into a created calendar) with writable myRights, so they appeared as selectable create destinations. - canCreateEventsIn(calendar, isSubscriptionCalendar): writable AND not a subscription. Exported from lib/calendar-editability. - event-modal: the calendar picker now lists only creatable calendars (plus the event's own calendar when editing), and new events never default into a subscription / read-only calendar. +4 unit tests. tsc + eslint clean; calendar-editability suite 30 green. --- components/calendar/event-modal.tsx | 18 ++++++++++++++---- lib/__tests__/calendar-editability.test.ts | 19 +++++++++++++++++++ lib/calendar-editability.ts | 14 ++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index 3ad964dcc..b7f947ca5 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -19,7 +19,7 @@ import { getStatusCounts, buildParticipantMap, } from "@/lib/calendar-participants"; -import { getEventEditability } from "@/lib/calendar-editability"; +import { getEventEditability, canCreateEventsIn } from "@/lib/calendar-editability"; import { PluginSlot } from "@/components/plugins/plugin-slot"; import { useSettingsStore } from "@/stores/settings-store"; import { generateUUID } from "@/lib/utils"; @@ -272,8 +272,18 @@ export function EventModal({ if (event?.calendarIds) return getPrimaryCalendarId(event) || calendars[0]?.id || ""; if (defaultCalendarId && calendars.some(c => c.id === defaultCalendarId)) return defaultCalendarId; const defaultCal = calendars.find(c => c.isDefault); - return defaultCal?.id || calendars[0]?.id || ""; + // Never default new events into a subscription / read-only calendar (#762). + return defaultCal?.id + || calendars.find(c => canCreateEventsIn(c, isSubscriptionCalendar))?.id + || calendars[0]?.id || ""; }); + + // Calendars offered as create/move targets: writable, non-subscription, plus + // the event's current calendar so an in-place edit never blanks the select. + const selectableCalendars = useMemo( + () => calendars.filter(c => canCreateEventsIn(c, isSubscriptionCalendar) || c.id === calendarId), + [calendars, isSubscriptionCalendar, calendarId] + ); const [recurrence, setRecurrence] = useState(() => { if (!event?.recurrenceRules?.length) return "none"; const rule = event.recurrenceRules[0]; @@ -1165,7 +1175,7 @@ export function EventModal({ )} - {calendars.length > 1 && ( + {selectableCalendars.length > 1 && (