diff --git a/src/lib/__tests__/calendar-utils.test.ts b/src/lib/__tests__/calendar-utils.test.ts index eb52c049..5c1b0a66 100644 --- a/src/lib/__tests__/calendar-utils.test.ts +++ b/src/lib/__tests__/calendar-utils.test.ts @@ -13,6 +13,7 @@ import { timePattern, layoutOverlappingEvents, CALENDAR_COLOR_PALETTE, + reopenedEdit, } from '../calendar-utils'; import type { CalendarEvent } from '../../api/types'; import { useSettingsStore } from '../../stores/settings-store'; @@ -278,3 +279,18 @@ describe('layoutOverlappingEvents', () => { expect(layoutOverlappingEvents(events, day)).toHaveLength(0); }); }); + +describe('reopenedEdit', () => { + it('puts the unsaved edits over the event and drops cleared fields', () => { + const event = { + id: 'e1', title: 'Team sync', start: '2026-10-19T10:00:00', duration: 'PT30M', + locations: { l: { '@type': 'Location', name: 'Room 2' } }, + } as unknown as CalendarEvent; + const reopened = reopenedEdit(event, { title: 'Team sync (moved)', start: '2026-10-19T11:00:00', locations: null }); + expect(reopened.id).toBe('e1'); + expect(reopened.title).toBe('Team sync (moved)'); + expect(reopened.start).toBe('2026-10-19T11:00:00'); + expect(reopened.duration).toBe('PT30M'); + expect('locations' in reopened).toBe(false); + }); +}); diff --git a/src/lib/calendar-utils.ts b/src/lib/calendar-utils.ts index e03b8874..d1cf1c94 100644 --- a/src/lib/calendar-utils.ts +++ b/src/lib/calendar-utils.ts @@ -604,3 +604,17 @@ export function getPrimaryCalendarId( ): string | undefined { return Object.keys(event.calendarIds || {})[0]; } + +/** + * The event as the editor last had it: the stored event with the unsaved + * edits on top, a field cleared in the editor (sent as `null`) removed. Used + * to reopen the editor when the recurrence-scope question is cancelled. + */ +export function reopenedEdit(event: CalendarEvent, updates: Partial): CalendarEvent { + const merged: Record = { ...event }; + for (const [key, value] of Object.entries(updates)) { + if (value === null) delete merged[key]; + else if (value !== undefined) merged[key] = value; + } + return merged as unknown as CalendarEvent; +} diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 287e83f5..71584a0c 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -86,6 +86,7 @@ import { getTaskDueDate, pickUnusedCalendarColor, sharedCalendarColorKey, + reopenedEdit, } from '../lib/calendar-utils'; import { buildReplyTo } from '../lib/calendar-invitation'; import { @@ -825,6 +826,16 @@ export default function CalendarScreen() { [modalEvent, createEvent, updateEvent], ); + // Cancelling "which occurrences?" after editing a recurring event goes back + // to the editor with the edits still in it; they used to be thrown away. + const cancelScope = React.useCallback(() => { + const pending = pendingAction; + setPendingAction(null); + if (pending?.kind !== 'edit') return; + setModalEvent(reopenedEdit(pending.event, pending.updates)); + setModalVisible(true); + }, [pendingAction]); + const handleDeleteFromModal = React.useCallback( async (event: CalendarEvent) => { setModalVisible(false); @@ -1232,7 +1243,7 @@ export default function CalendarScreen() { visible={!!pendingAction} actionType={pendingAction?.kind ?? 'edit'} onSelect={handleScopeSelect} - onClose={() => setPendingAction(null)} + onClose={cancelScope} />