[#565] added move for delegated events, deletes old and put new (#578)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2026-02-25 15:51:13 +01:00
committed by GitHub
parent 67898fa218
commit 6874b9ee70
7 changed files with 835 additions and 499 deletions
+24 -64
View File
@@ -6,7 +6,6 @@ import { addDays } from "@/components/Event/utils/dateRules";
import { formatDateTimeInTimezone } from "@/components/Event/utils/dateTimeFormatters";
import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers";
import {
moveEventAsync,
putEventAsync,
updateEventInstanceAsync,
updateSeriesAsync,
@@ -45,6 +44,7 @@ import { userAttendee } from "../User/models/attendee";
import { deleteEvent, getEvent, putEvent } from "./EventApi";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { detectRecurringEventChanges } from "./eventUtils";
import { moveEventBetweenCalendars } from "./updateEventHelpers/moveEventBetweenCalendars";
function EventUpdateModal({
eventId,
@@ -79,14 +79,12 @@ function EventUpdateModal({
// if the event's calendar is delegated then it shall be the only calendar accessible from the event update modal
const userPersonalCalendars: Calendar[] = useMemo(() => {
const allCalendars = Object.values(calList) as Calendar[];
if (calList[calId]?.delegated) {
return [calList[calId]];
}
return allCalendars.filter(
(calendar: Calendar) =>
calendar.id?.split("/")[0] === user.userData?.openpaasId
calendar.id?.split("/")[0] === user.userData?.openpaasId ||
calendar.delegated
);
}, [calList, calId, user.userData?.openpaasId]);
}, [calList, user.userData?.openpaasId]);
const timezoneList = useMemo(() => {
const zones = Object.keys(TIMEZONES.zones).sort();
@@ -947,7 +945,7 @@ function EventUpdateModal({
);
// Handle result of updateSeriesAsync
assertThunkSuccess(result);
await assertThunkSuccess(result);
// Clear cache to ensure navigation shows updated data
dispatch(clearFetchCache(calId));
@@ -969,7 +967,7 @@ function EventUpdateModal({
);
// Handle result of putEventAsync - check if rejected first
assertThunkSuccess(result);
await assertThunkSuccess(result);
// Remove old single event AFTER new recurring instances are added to store
// This prevents empty grid during the transition
@@ -980,68 +978,30 @@ function EventUpdateModal({
// Clear temp data on successful save
clearEventFormTempData("update");
} else {
// Normal non-recurring event update
// If calendar is changing, we'll handle it separately with moveEventAsync
// So only call putEventAsync if calendar is NOT changing
if (newCalId === calId) {
const result = await dispatch(
putEventAsync({ cal: targetCalendar, newEvent })
);
} else if (newCalId === calId) {
// Normal non-recurring event update (same calendar)
const result = await dispatch(
putEventAsync({ cal: targetCalendar, newEvent })
);
// Handle result of putEventAsync - check if rejected first
const typedResult = result as AsyncThunkResult;
if (typedResult.type && typedResult.type.endsWith("/rejected")) {
throw new Error(
typedResult.error?.message ||
typedResult.payload?.message ||
"API call failed"
);
}
if (typedResult && typeof typedResult.unwrap === "function") {
await typedResult.unwrap();
}
await assertThunkSuccess(result);
// Clear temp data on successful save
clearEventFormTempData("update");
}
// Clear temp data on successful save
clearEventFormTempData("update");
}
// Note: when newCalId !== calId, the move is handled below in the
// "Handle calendar change" block, so we intentionally skip putEventAsync here.
}
// Handle calendar change
// Handle calendar change (move to a different calendar)
if (newCalId !== calId) {
// Get the old calendar for updating
const oldCalendar = calList[calId];
if (!oldCalendar) {
console.error("Old calendar not found");
return;
}
// First update the event in the old calendar, then move it
const putResult = await dispatch(
putEventAsync({ cal: oldCalendar, newEvent: { ...newEvent, calId } })
);
// Handle result of putEventAsync
const typedPutResult = putResult as AsyncThunkResult;
if (typedPutResult && typeof typedPutResult.unwrap === "function") {
await typedPutResult.unwrap();
}
// Then move it to the new calendar
const moveResult = await dispatch(
moveEventAsync({
cal: targetCalendar,
newEvent,
newURL: `/calendars/${newCalId}/${extractEventBaseUuid(event.uid)}.ics`,
})
);
// Handle result of moveEventAsync
const typedMoveResult = moveResult as AsyncThunkResult;
if (typedMoveResult && typeof typedMoveResult.unwrap === "function") {
await typedMoveResult.unwrap();
}
await moveEventBetweenCalendars({
dispatch,
calList,
newEvent,
oldCalId: calId,
newCalId,
});
// Clear temp data on successful move
clearEventFormTempData("update");
@@ -0,0 +1,210 @@
import { AppDispatch } from "@/app/store";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import {
deleteEventAsync,
moveEventAsync,
putEventAsync,
} from "@/features/Calendars/services";
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
import { userAttendee } from "@/features/User/models/attendee";
import { userOrganiser } from "@/features/User/userDataTypes";
import { assertThunkSuccess } from "@/utils/assertThunkSuccess";
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
import { makeDisplayName } from "@/utils/makeDisplayName";
import { CalendarEvent } from "../EventsTypes";
import { buildDelegatedEventURL } from "../eventUtils";
export interface MoveEventBetweenCalendarsParams {
dispatch: AppDispatch;
calList: Record<string, Calendar>;
newEvent: CalendarEvent;
oldCalId: string;
newCalId: string;
}
function resolveOrganizerForCalendar(
calendar: Calendar,
originalOrganizer: CalendarEvent["organizer"]
): CalendarEvent["organizer"] {
const ownerEmail = calendar.owner?.emails?.[0];
if (!ownerEmail) {
return originalOrganizer;
}
return {
...originalOrganizer,
cal_address: ownerEmail,
cn: makeDisplayName(calendar) ?? originalOrganizer?.cn ?? "",
};
}
function rewriteAttendeesForOrganizerChange(
attendees: userAttendee[],
oldOrganizer?: userOrganiser,
newOrganizer?: userOrganiser
): userAttendee[] {
if (!newOrganizer) {
return attendees;
}
const normalise = (addr: string | undefined) => (addr ?? "").toLowerCase();
const oldAddr = normalise(oldOrganizer?.cal_address);
const newAddr = normalise(newOrganizer.cal_address);
// Remove the old organizer from the attendee list if there is one
const filtered = attendees.filter(
(a) => normalise(a.cal_address) !== oldAddr
);
// Add the new organizer as CHAIR if they are not already listed
const alreadyPresent = filtered.some(
(a) => normalise(a.cal_address) === newAddr
);
if (!alreadyPresent && newAddr) {
filtered.push({
cal_address: newOrganizer.cal_address,
partstat: "ACCEPTED",
role: "CHAIR",
rsvp: "FALSE",
cn: newOrganizer.cn || newOrganizer.cal_address,
cutype: "INDIVIDUAL",
});
}
return filtered;
}
export async function moveEventBetweenCalendars({
dispatch,
calList,
newEvent,
oldCalId,
newCalId,
}: MoveEventBetweenCalendarsParams): Promise<void> {
const oldCalendar = calList[oldCalId];
if (!oldCalendar) {
throw new Error(`Old calendar not found: ${oldCalId}`);
}
const targetCalendar = calList[newCalId];
if (!targetCalendar) {
throw new Error(`Target calendar not found: ${newCalId}`);
}
const isDelegatedMove = oldCalendar.delegated || targetCalendar.delegated;
if (isDelegatedMove) {
await moveDelegatedEvent({
dispatch,
newEvent,
oldCalendar,
targetCalendar,
});
} else {
await moveStandardEvent({
dispatch,
newEvent,
targetCalendar,
oldCalendar,
});
}
}
interface StandardMoveParams {
dispatch: AppDispatch;
newEvent: CalendarEvent;
targetCalendar: Calendar;
oldCalendar: Calendar;
}
async function moveStandardEvent({
dispatch,
newEvent,
targetCalendar,
oldCalendar,
}: StandardMoveParams): Promise<void> {
const newCalId = targetCalendar.id;
const putResult = await dispatch(
putEventAsync({
cal: oldCalendar,
newEvent: { ...newEvent, calId: oldCalendar.id },
})
);
await assertThunkSuccess(putResult);
const newURL = `/calendars/${newCalId}/${extractEventBaseUuid(newEvent.uid)}.ics`;
const moveResult = await dispatch(
moveEventAsync({
cal: targetCalendar,
newEvent,
newURL,
})
);
await assertThunkSuccess(moveResult);
}
interface DelegatedMoveParams {
dispatch: AppDispatch;
newEvent: CalendarEvent;
oldCalendar: Calendar;
targetCalendar: Calendar;
}
async function moveDelegatedEvent({
dispatch,
newEvent,
oldCalendar,
targetCalendar,
}: DelegatedMoveParams): Promise<void> {
const newCalId = targetCalendar.id;
const newOrganizer = resolveOrganizerForCalendar(
targetCalendar,
newEvent.organizer
);
const newAttendees = rewriteAttendeesForOrganizerChange(
newEvent.attendee ?? [],
newEvent.organizer,
newOrganizer
);
const newURL = `/calendars/${newCalId}/${extractEventBaseUuid(newEvent.uid)}.ics`;
const eventForTargetCalendar: CalendarEvent = {
...newEvent,
calId: newCalId,
URL: targetCalendar.delegated
? buildDelegatedEventURL(targetCalendar, newURL)
: newURL,
organizer: newOrganizer,
attendee: newAttendees,
};
const putResult = await dispatch(
putEventAsync({ cal: targetCalendar, newEvent: eventForTargetCalendar })
);
const typedPutResult = putResult as AsyncThunkResult;
if (typedPutResult && typeof typedPutResult.unwrap === "function") {
await typedPutResult.unwrap();
} else {
await assertThunkSuccess(putResult);
}
const deleteResult = await dispatch(
deleteEventAsync({
calId: oldCalendar.id,
eventId: newEvent.uid,
eventURL: newEvent.URL,
})
);
const typedDeleteResult = deleteResult as AsyncThunkResult;
if (typedDeleteResult && typeof typedDeleteResult.unwrap === "function") {
await typedDeleteResult.unwrap();
} else {
await assertThunkSuccess(deleteResult);
}
}
+5 -2
View File
@@ -1,10 +1,13 @@
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
export async function assertThunkSuccess(result: unknown): Promise<void> {
if (result === undefined || result === null) {
return;
}
const typed = result as AsyncThunkResult;
if (typed?.type?.endsWith("/rejected")) {
if (typed.type && typed.type.endsWith("/rejected")) {
throw new Error(
typed.error?.message || typed.payload?.message || "API call failed"
typed.error?.message ?? typed.payload?.message ?? "Thunk was rejected"
);
}
if (typeof typed.unwrap === "function") {