From f826d05c607a4ce51332be6a490be7cb49a669e6 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sun, 22 Feb 2026 01:08:00 +0800 Subject: [PATCH] Fix recurrence edit/delete scope and simplify weekly UI - Weekly recurrence no longer requires manual weekday selection; auto-derives from event start date - EventForm now receives and forwards editScope prop to API (edit_scope in PUT body, scope query param in DELETE) - CalendarPage passes scope through proper prop instead of _editScope hack - Backend this_and_future: inherits parent's recurrence_rule when child has none, properly regenerates children after edit - Backend: parent-level edits now delete+regenerate all children Co-Authored-By: Claude Opus 4.6 --- backend/app/routers/events.py | 27 +++++++++++++++-- .../src/components/calendar/CalendarPage.tsx | 7 +++-- .../src/components/calendar/EventForm.tsx | 29 +++++++++---------- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/backend/app/routers/events.py b/backend/app/routers/events.py index acdc73e..7b0b501 100644 --- a/backend/app/routers/events.py +++ b/backend/app/routers/events.py @@ -304,6 +304,13 @@ async def update_event( this_original_start = event.original_start or event.start_datetime if parent_id is not None: + # Fetch the parent's recurrence_rule before deleting siblings + parent_result = await db.execute( + select(CalendarEvent).where(CalendarEvent.id == parent_id) + ) + parent_event = parent_result.scalar_one_or_none() + parent_rule = parent_event.recurrence_rule if parent_event else None + await db.execute( delete(CalendarEvent).where( CalendarEvent.parent_event_id == parent_id, @@ -318,17 +325,33 @@ async def update_event( event.is_recurring = True event.original_start = None - # If a new recurrence_rule was provided, regenerate children from this point + # Inherit parent's recurrence_rule if none was provided in update + if not event.recurrence_rule and parent_rule: + event.recurrence_rule = parent_rule + + # Regenerate children from this point if event.recurrence_rule: await db.flush() children = generate_occurrences(event) for child in children: db.add(child) else: - # Not part of a series — plain update + # This IS a parent — update it and regenerate all children for key, value in update_data.items(): setattr(event, key, value) + # Delete all existing children and regenerate + if event.recurrence_rule: + await db.execute( + delete(CalendarEvent).where( + CalendarEvent.parent_event_id == event.id + ) + ) + await db.flush() + children = generate_occurrences(event) + for child in children: + db.add(child) + await db.commit() else: diff --git a/frontend/src/components/calendar/CalendarPage.tsx b/frontend/src/components/calendar/CalendarPage.tsx index 244ede9..dd0f0a4 100644 --- a/frontend/src/components/calendar/CalendarPage.tsx +++ b/frontend/src/components/calendar/CalendarPage.tsx @@ -46,6 +46,7 @@ export default function CalendarPage() { const [scopeDialogOpen, setScopeDialogOpen] = useState(false); const [scopeAction, setScopeAction] = useState('edit'); const [scopeEvent, setScopeEvent] = useState(null); + const [activeEditScope, setActiveEditScope] = useState<'this' | 'this_and_future' | null>(null); const { data: calendars = [] } = useCalendars(); @@ -177,8 +178,8 @@ export default function CalendarPage() { const handleScopeChoice = (scope: 'this' | 'this_and_future') => { if (!scopeEvent) return; if (scopeAction === 'edit') { - // For edits, open form — the form will send scope on save - setEditingEvent({ ...scopeEvent, _editScope: scope } as any); + setEditingEvent(scopeEvent); + setActiveEditScope(scope); setShowForm(true); setScopeDialogOpen(false); } else if (scopeAction === 'delete') { @@ -235,6 +236,7 @@ export default function CalendarPage() { calendarRef.current?.getApi().unselect(); setShowForm(false); setEditingEvent(null); + setActiveEditScope(null); setSelectedStart(null); setSelectedEnd(null); setSelectedAllDay(false); @@ -323,6 +325,7 @@ export default function CalendarPage() { initialStart={selectedStart} initialEnd={selectedEnd} initialAllDay={selectedAllDay} + editScope={activeEditScope} onClose={handleCloseForm} /> )} diff --git a/frontend/src/components/calendar/EventForm.tsx b/frontend/src/components/calendar/EventForm.tsx index 26f5466..ea99c8e 100644 --- a/frontend/src/components/calendar/EventForm.tsx +++ b/frontend/src/components/calendar/EventForm.tsx @@ -25,6 +25,7 @@ interface EventFormProps { initialStart?: string | null; initialEnd?: string | null; initialAllDay?: boolean; + editScope?: 'this' | 'this_and_future' | null; onClose: () => void; } @@ -74,7 +75,7 @@ function parseRecurrenceRule(raw?: string): RecurrenceRule | null { } } -export default function EventForm({ event, initialStart, initialEnd, initialAllDay, onClose }: EventFormProps) { +export default function EventForm({ event, initialStart, initialEnd, initialAllDay, editScope, onClose }: EventFormProps) { const queryClient = useQueryClient(); const { data: calendars = [] } = useCalendars(); const isAllDay = event?.all_day ?? initialAllDay ?? false; @@ -125,7 +126,8 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD case 'every_n_days': return { type: 'every_n_days', interval: recurrenceInterval }; case 'weekly': - return { type: 'weekly', weekday: recurrenceWeekday }; + // No weekday needed — backend derives it from the event's start date + return { type: 'weekly' }; case 'monthly_nth_weekday': return { type: 'monthly_nth_weekday', week: recurrenceWeek, weekday: recurrenceWeekday }; case 'monthly_date': @@ -145,7 +147,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD endDt = adjustAllDayEndForSave(endDt); } - const payload = { + const payload: Record = { title: data.title, description: data.description, start_datetime: data.start_datetime, @@ -158,6 +160,9 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD }; if (event) { + if (editScope) { + payload.edit_scope = editScope; + } const response = await api.put(`/events/${event.id}`, payload); return response.data; } else { @@ -179,7 +184,8 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD const deleteMutation = useMutation({ mutationFn: async () => { - await api.delete(`/events/${event?.id}`); + const params = editScope ? `?scope=${editScope}` : ''; + await api.delete(`/events/${event?.id}${params}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['calendar-events'] }); @@ -346,18 +352,9 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD )} {recurrenceType === 'weekly' && ( -
- - -
+

+ Repeats every week on the same day as the start date. +

)} {recurrenceType === 'monthly_nth_weekday' && (