fix: directly delete recurring events when converting to non-recurring

- Use direct API call to delete recurring events instead of going through Redux store
- Construct event URL directly for more reliable deletion
- Add error handling and small delay to ensure complete deletion
- Clean up all instances from store after deletion
This commit is contained in:
lenhanphung
2025-10-09 14:05:19 +07:00
committed by Benoit TELLIER
parent 2ba80fcdb6
commit 40f0179be6
3 changed files with 89 additions and 65 deletions
+7 -7
View File
@@ -345,13 +345,13 @@ export default function EventFormFields({
label="All day" label="All day"
/> />
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={showRepeat} checked={showRepeat}
disabled={typeOfAction === "solo"} disabled={typeOfAction === "solo"}
onChange={() => { onChange={() => {
const newShowRepeat = !showRepeat; const newShowRepeat = !showRepeat;
setShowRepeat(newShowRepeat); setShowRepeat(newShowRepeat);
if (newShowRepeat) { if (newShowRepeat) {
setRepetition({ setRepetition({
freq: "daily", freq: "daily",
+5 -5
View File
@@ -46,7 +46,7 @@ import {
import { Calendars } from "../Calendars/CalendarTypes"; import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes"; import { userAttendee } from "../User/userDataTypes";
import { getEvent } from "./EventApi"; import { getEvent } from "./EventApi";
import { formatLocalDateTime } from "./EventModal"; import { formatLocalDateTime } from "../../components/Event/EventFormFields";
import { CalendarEvent, RepetitionObject } from "./EventsTypes"; import { CalendarEvent, RepetitionObject } from "./EventsTypes";
export default function EventDisplayModal({ export default function EventDisplayModal({
@@ -128,7 +128,7 @@ export default function EventDisplayModal({
onClose({}, "backdropClick"); onClose({}, "backdropClick");
} }
setRepetition(event?.repetition ?? ({} as RepetitionObject)); setRepetition(event?.repetition ?? ({} as RepetitionObject));
}, [open, eventId, dispatch, onClose, event]); }, [open, eventId, dispatch, onClose, event, calendar]);
useEffect(() => { useEffect(() => {
const fetchMasterEvent = async () => { const fetchMasterEvent = async () => {
const masterEvent = await getEvent(event); const masterEvent = await getEvent(event);
@@ -160,8 +160,8 @@ export default function EventDisplayModal({
calId, calId,
title, title,
URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`, URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`,
start: new Date(start), start: start,
end: new Date(end), end: end,
allday, allday,
uid: event.uid ?? newEventUID, uid: event.uid ?? newEventUID,
description, description,
@@ -523,7 +523,7 @@ export default function EventDisplayModal({
{isOwn && ( {isOwn && (
<RepeatEvent <RepeatEvent
repetition={repetition} repetition={repetition}
eventStart={event.start} eventStart={new Date(event.start)}
setRepetition={setRepetition} setRepetition={setRepetition}
isOwn={isOwn && typeOfAction !== "solo"} isOwn={isOwn && typeOfAction !== "solo"}
/> />
+77 -53
View File
@@ -23,7 +23,7 @@ import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtil
import EventFormFields, { import EventFormFields, {
formatDateTimeInTimezone, formatDateTimeInTimezone,
} from "../../components/Event/EventFormFields"; } from "../../components/Event/EventFormFields";
import { getEvent } from "./EventApi"; import { getEvent, deleteEvent } from "./EventApi";
import { refreshCalendars } from "../../components/Event/utils/eventUtils"; import { refreshCalendars } from "../../components/Event/utils/eventUtils";
import { getCalendarRange } from "../../utils/dateUtils"; import { getCalendarRange } from "../../utils/dateUtils";
@@ -346,66 +346,90 @@ function EventUpdateModal({
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid })); dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
} }
// Handle recurrence instances // Handle recurrence instances
const [, recurrenceId] = event.uid.split("/"); const [, recurrenceId] = event.uid.split("/");
// Execute API calls in background based on typeOfAction // Execute API calls in background based on typeOfAction
if (recurrenceId) { if (recurrenceId) {
if (typeOfAction === "solo") { if (typeOfAction === "solo") {
// Update just this instance // Update just this instance
dispatch(
updateEventInstanceAsync({
cal: targetCalendar,
event: { ...newEvent, recurrenceId },
})
);
} else if (typeOfAction === "all") {
// Update all instances
// Special case: When converting recurring event to non-recurring
if (event.repetition?.freq && !repetition.freq) {
// For repeat -> no-repeat, create a new non-repeating event
dispatch( dispatch(
putEventAsync({ updateEventInstanceAsync({
cal: targetCalendar,
newEvent: {
...newEvent,
uid: crypto.randomUUID(), // Generate new ID for the single event
URL: `/calendars/${newCalId || calId}/${crypto.randomUUID()}.ics`,
},
})
);
// Delete the old repeating series
const baseUID = event.uid.split("/")[0];
Object.keys(targetCalendar.events).forEach((eventId) => {
if (eventId.split("/")[0] === baseUID) {
dispatch(removeEvent({ calendarUid: calId, eventUid: eventId }));
}
});
} else {
// Normal update for recurring events
dispatch(
updateSeriesAsync({
cal: targetCalendar, cal: targetCalendar,
event: { ...newEvent, recurrenceId }, event: { ...newEvent, recurrenceId },
}) })
); );
} else if (typeOfAction === "all") {
// Update all instances
// Special case: When converting recurring event to non-recurring
if (event.repetition?.freq && !repetition.freq) {
// For repeat -> no-repeat, create a new non-repeating event
dispatch(
putEventAsync({
cal: targetCalendar,
newEvent: {
...newEvent,
uid: crypto.randomUUID(), // Generate new ID for the single event
URL: `/calendars/${newCalId || calId}/${crypto.randomUUID()}.ics`,
},
})
);
// Delete the old repeating series - we need to be more direct and forceful
const baseUID = event.uid.split("/")[0];
try {
// Directly use the deleteEvent API without going through the store
const eventBaseURL = `/calendars/${calId}/${baseUID}.ics`;
await deleteEvent(eventBaseURL);
console.log(
"Deleted master event via direct API call:",
eventBaseURL
);
// Force a small delay to ensure deletion completes
await new Promise((resolve) => setTimeout(resolve, 500));
// Make sure to clean up any instances in the store
Object.keys(targetCalendar.events).forEach((eventId) => {
if (eventId.split("/")[0] === baseUID) {
dispatch(
removeEvent({ calendarUid: calId, eventUid: eventId })
);
}
});
} catch (err) {
console.error("Failed to delete recurring event:", err);
// Even if deletion fails, continue with creating the new event
}
} else {
// Normal update for recurring events
dispatch(
updateSeriesAsync({
cal: targetCalendar,
event: { ...newEvent, recurrenceId },
})
);
}
// Refresh calendars to ensure all instances are updated
const calendarRange = getCalendarRange(new Date(start));
await refreshCalendars(
dispatch,
Object.values(calendarsList),
calendarRange
);
} }
} else {
// Refresh calendars to ensure all instances are updated // Non-recurring event
const calendarRange = getCalendarRange(new Date(start)); dispatch(
await refreshCalendars(dispatch, Object.values(calendarsList), calendarRange); putEventAsync({
cal: targetCalendar,
newEvent,
})
);
} }
} else {
// Non-recurring event
dispatch(
putEventAsync({
cal: targetCalendar,
newEvent,
})
);
}
// Handle calendar change // Handle calendar change
if (newCalId !== calId) { if (newCalId !== calId) {