fix: preserve recurrence exceptions when RSVP to recurring events (#234)

When an attendee accepts/rejects/tentative a recurring event with
exceptions, only update the PARTSTAT without removing exception events.

Add removeOverrides: false parameter to updateSeriesAsync call in
handleRSVP function to preserve all recurrence exceptions.

Fixes: Recurrence exceptions being deleted when accepting/rejecting
all events in a recurring series

* fix: update PARTSTAT on all VEVENTs when RSVP to recurring events

When an attendee accepts/rejects/tentative a recurring event with exceptions,
update PARTSTAT on ALL VEVENTs (master + exceptions) to reflect the correct
attendance status across the entire series.

Changes:
- Add updateSeriesPartstat() in EventApi.ts to update PARTSTAT on all VEVENTs
- Update handleRSVP() to use new function instead of updateSeriesAsync
- Preserve exception times while updating PARTSTAT consistently
- Update test to verify new function is called correctly

Fixes: Exception events not accepting when user chooses 'Accept all events'
This commit is contained in:
lenhanphung
2025-10-23 10:36:34 +07:00
committed by GitHub
parent 1c8826facc
commit 8681e88390
3 changed files with 58 additions and 27 deletions
@@ -8,7 +8,10 @@ import {
deleteEventAsync,
} from "../../../features/Calendars/CalendarSlice";
import { Calendars } from "../../../features/Calendars/CalendarTypes";
import { getEvent } from "../../../features/Events/EventApi";
import {
getEvent,
updateSeriesPartstat,
} from "../../../features/Events/EventApi";
import { CalendarEvent } from "../../../features/Events/EventsTypes";
import { userData } from "../../../features/User/userDataTypes";
import { getCalendarRange } from "../../../utils/dateUtils";
@@ -33,20 +36,11 @@ export async function handleRSVP(
if (typeOfAction === "solo") {
dispatch(updateEventInstanceAsync({ cal: calendar, event: newEvent }));
} else if (typeOfAction === "all") {
const master = await getEvent(newEvent, true);
const calendarRange = getCalendarRange(new Date(event.start));
dispatch(
updateSeriesAsync({
cal: calendar,
event: {
...master,
attendee: event.attendee?.map((a) =>
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
),
},
})
);
// Update PARTSTAT on ALL VEVENTs (master + exceptions)
await updateSeriesPartstat(event, user.userData.email, rsvp);
if (calendars) {
await refreshCalendars(dispatch, calendars, calendarRange);
}
+44
View File
@@ -126,6 +126,50 @@ export const deleteEventInstance = async (
return putEvent(seriesEvent, calOwnerEmail);
};
export const updateSeriesPartstat = async (
event: CalendarEvent,
attendeeEmail: string,
partstat: string
) => {
const vevents = await getAllRecurrentEvent(event);
// Update PARTSTAT in ALL VEVENTs (master + exceptions)
const updatedVevents = vevents.map((vevent: any[]) => {
const properties = vevent[1];
const updatedProperties = properties.map((prop: any[]) => {
// Find ATTENDEE properties
if (prop[0] === "attendee") {
const calAddress = prop[3];
// Check if this is the target attendee
if (calAddress.toLowerCase().includes(attendeeEmail.toLowerCase())) {
// Update PARTSTAT parameter
const params = { ...prop[1], partstat: partstat };
return [prop[0], params, prop[2], prop[3]];
}
}
return prop;
});
return [vevent[0], updatedProperties, vevent[2]];
});
const timezoneData = TIMEZONES.zones[event.timezone];
const vtimezone = makeTimezone(timezoneData, event);
const newJCal = [
"vcalendar",
[],
[...updatedVevents, vtimezone.component.jCal],
];
return api(`dav${event.URL}`, {
method: "PUT",
body: JSON.stringify(newJCal),
headers: {
"content-type": "text/calendar; charset=utf-8",
},
});
};
export const updateSeries = async (
event: CalendarEvent,
calOwnerEmail?: string,