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:
@@ -472,18 +472,10 @@ describe("Recurrence Event Behavior Tests", () => {
|
|||||||
expect(updatedEvent.attendee[0].partstat).toBe("ACCEPTED");
|
expect(updatedEvent.attendee[0].partstat).toBe("ACCEPTED");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls updateSeriesAsync when accepting all instances", async () => {
|
it("calls updateSeriesPartstat when accepting all instances", async () => {
|
||||||
const getEventSpy = jest.spyOn(EventApi, "getEvent").mockResolvedValue({
|
|
||||||
...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
|
|
||||||
.events["recurring-base/20250315T100000"],
|
|
||||||
uid: "recurring-base",
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const spy = jest
|
const spy = jest
|
||||||
.spyOn(eventThunks, "updateSeriesAsync")
|
.spyOn(EventApi, "updateSeriesPartstat")
|
||||||
.mockImplementation((payload) => {
|
.mockResolvedValue({} as any);
|
||||||
return () => Promise.resolve() as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
@@ -507,12 +499,13 @@ describe("Recurrence Event Behavior Tests", () => {
|
|||||||
fireEvent.click(screen.getByRole("button", { name: /Ok/i }));
|
fireEvent.click(screen.getByRole("button", { name: /Ok/i }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getEventSpy).toHaveBeenCalled();
|
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedEvent = spy.mock.calls[0][0].event;
|
const callArgs = spy.mock.calls[0];
|
||||||
expect(updatedEvent.attendee[0].partstat).toBe("ACCEPTED");
|
expect(callArgs[0].uid).toBe("recurring-base/20250315T100000");
|
||||||
|
expect(callArgs[1]).toBe("test@test.com");
|
||||||
|
expect(callArgs[2]).toBe("ACCEPTED");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
deleteEventAsync,
|
deleteEventAsync,
|
||||||
} from "../../../features/Calendars/CalendarSlice";
|
} from "../../../features/Calendars/CalendarSlice";
|
||||||
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
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 { CalendarEvent } from "../../../features/Events/EventsTypes";
|
||||||
import { userData } from "../../../features/User/userDataTypes";
|
import { userData } from "../../../features/User/userDataTypes";
|
||||||
import { getCalendarRange } from "../../../utils/dateUtils";
|
import { getCalendarRange } from "../../../utils/dateUtils";
|
||||||
@@ -33,20 +36,11 @@ export async function handleRSVP(
|
|||||||
if (typeOfAction === "solo") {
|
if (typeOfAction === "solo") {
|
||||||
dispatch(updateEventInstanceAsync({ cal: calendar, event: newEvent }));
|
dispatch(updateEventInstanceAsync({ cal: calendar, event: newEvent }));
|
||||||
} else if (typeOfAction === "all") {
|
} else if (typeOfAction === "all") {
|
||||||
const master = await getEvent(newEvent, true);
|
|
||||||
const calendarRange = getCalendarRange(new Date(event.start));
|
const calendarRange = getCalendarRange(new Date(event.start));
|
||||||
|
|
||||||
dispatch(
|
// Update PARTSTAT on ALL VEVENTs (master + exceptions)
|
||||||
updateSeriesAsync({
|
await updateSeriesPartstat(event, user.userData.email, rsvp);
|
||||||
cal: calendar,
|
|
||||||
event: {
|
|
||||||
...master,
|
|
||||||
attendee: event.attendee?.map((a) =>
|
|
||||||
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (calendars) {
|
if (calendars) {
|
||||||
await refreshCalendars(dispatch, calendars, calendarRange);
|
await refreshCalendars(dispatch, calendars, calendarRange);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,50 @@ export const deleteEventInstance = async (
|
|||||||
return putEvent(seriesEvent, calOwnerEmail);
|
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 (
|
export const updateSeries = async (
|
||||||
event: CalendarEvent,
|
event: CalendarEvent,
|
||||||
calOwnerEmail?: string,
|
calOwnerEmail?: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user