import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { ResponsiveDialog } from "@/components/Dialog"; import EventFormFields from "@/components/Event/EventFormFields"; import { addDays } from "@/components/Event/utils/dateRules"; import { formatDateTimeInTimezone } from "@/components/Event/utils/dateTimeFormatters"; import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers"; import { putEventAsync, updateEventInstanceAsync, updateSeriesAsync, } from "@/features/Calendars/services"; import { updateAttendeesAfterTimeChange } from "@/features/Events/updateEventHelpers/updateAttendeesAfterTimeChange"; import { assertThunkSuccess } from "@/utils/assertThunkSuccess"; import { buildEventFormTempData, clearEventFormTempData, EventFormContext, EventFormState, restoreEventFormDataFromTemp as restoreEventFormDataFromStorage, restoreFormDataFromTemp, saveEventFormDataToTemp, showErrorNotification, } from "@/utils/eventFormTempStorage"; import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid"; import { browserDefaultTimeZone, getTimezoneOffset, resolveTimezone, } from "@/utils/timezone"; import { TIMEZONES } from "@/utils/timezone-data"; import { addVideoConferenceToDescription } from "@/utils/videoConferenceUtils"; import { Box, Button } from "@linagora/twake-mui"; import AddIcon from "@mui/icons-material/Add"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useI18n } from "twake-i18n"; import { clearFetchCache, removeEvent, updateEventLocal, } from "../Calendars/CalendarSlice"; import { Calendar } from "../Calendars/CalendarTypes"; 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, calId, open, onClose, onCloseAll, eventData, typeOfAction, }: { eventId: string; calId: string; open: boolean; onClose: (event: unknown, reason: "backdropClick" | "escapeKeyDown") => void; onCloseAll?: () => void; eventData?: CalendarEvent | null; typeOfAction?: "solo" | "all"; }) { const { t } = useI18n(); const dispatch = useAppDispatch(); const calList = useAppSelector((state) => state.calendars.list); // Get event from Redux store (cached data) as fallback const cachedEvent = useAppSelector( (state) => state.calendars.list[calId]?.events[eventId] ); // Use fresh data if available, otherwise use eventData from props, otherwise use cached data const event = eventData || cachedEvent; const user = useAppSelector((state) => state.user); const userPersonalCalendars: Calendar[] = useMemo(() => { const allCalendars = Object.values(calList) as Calendar[]; return allCalendars.filter( (calendar: Calendar) => calendar.id?.split("/")[0] === user.userData?.openpaasId || (calendar.delegated && calendar.access?.write) ); }, [calList, user.userData?.openpaasId]); const timezoneList = useMemo(() => { const zones = Object.keys(TIMEZONES.zones).sort(); const browserTz = resolveTimezone(browserDefaultTimeZone); return { zones, browserTz, getTimezoneOffset }; }, []); const [showMore, setShowMore] = useState(false); const [showDescription, setShowDescription] = useState( event?.description ? true : false ); const [showRepeat, setShowRepeat] = useState( event?.repetition?.freq ? true : false ); // Form state - initialize with empty values const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [location, setLocation] = useState(""); const [start, setStart] = useState(""); const [end, setEnd] = useState(""); const [allday, setAllDay] = useState(false); const [repetition, setRepetition] = useState( {} as RepetitionObject ); const [alarm, setAlarm] = useState(""); const [busy, setBusy] = useState("OPAQUE"); const [eventClass, setEventClass] = useState("PUBLIC"); const [timezone, setTimezone] = useState( resolveTimezone(browserDefaultTimeZone) ); const [newCalId, setNewCalId] = useState(calId); const defaultCalendarId = useMemo( () => userPersonalCalendars[0]?.id ?? "", [userPersonalCalendars] ); const [calendarid, setCalendarid] = useState(calId ?? defaultCalendarId); const [attendees, setAttendees] = useState([]); const [hasVideoConference, setHasVideoConference] = useState(false); const [meetingLink, setMeetingLink] = useState(null); const [isFormValid, setIsFormValid] = useState(false); const [showValidationErrors, setShowValidationErrors] = useState(false); const [hasEndDateChanged, setHasEndDateChanged] = useState(false); const resetAllStateToDefault = useCallback(() => { setShowMore(false); setShowDescription(false); setShowRepeat(false); setTitle(""); setDescription(""); setAttendees([]); setLocation(""); setStart(""); setEnd(""); if (defaultCalendarId) { setCalendarid(defaultCalendarId); } setAllDay(false); setRepetition({} as RepetitionObject); setAlarm(""); setEventClass("PUBLIC"); setBusy("OPAQUE"); setTimezone(resolveTimezone(browserDefaultTimeZone)); setHasVideoConference(false); setMeetingLink(null); }, [defaultCalendarId]); // Prevent repeated initialization loops const initializedKeyRef = useRef(null); // Track when restoring from error to prevent other useEffects from overriding restored data const isRestoringFromErrorRef = useRef(false); // State to hold the master event when editing "all events" const [masterEvent, setMasterEvent] = useState(null); const [isLoadingMasterEvent, setIsLoadingMasterEvent] = useState(false); // Fetch master event when editing "all events" of a recurring series useEffect(() => { if (!event || !open || typeOfAction !== "all") { setMasterEvent(null); return; } const isRecurringEvent = !!event.repetition?.freq; if (!isRecurringEvent) { setMasterEvent(null); return; } // Check if this is an instance (has recurrence-id) const [baseUID, recurrenceId] = event.uid.split("/"); if (!recurrenceId) { // This is already the master event setMasterEvent(event); return; } // Fetch the master event const fetchMasterEvent = async () => { setIsLoadingMasterEvent(true); try { const masterEventToFetch = { ...event, uid: baseUID, // Use base UID to get master event }; const fetchedMasterEvent = await getEvent(masterEventToFetch, true); setMasterEvent(fetchedMasterEvent); } catch (err) { console.error("Failed to fetch master event:", err); // Fallback to using the clicked instance setMasterEvent(event); } finally { setIsLoadingMasterEvent(false); } }; fetchMasterEvent(); }, [event, open, typeOfAction]); // Initialize form state when event data is available useEffect(() => { // Skip if restoring from error - data already restored if (isRestoringFromErrorRef.current) { return; } // Skip if still loading master event if (isLoadingMasterEvent) { return; } if (event && open) { // Use master event for "all" action, otherwise use the clicked event const eventToDisplay = typeOfAction === "all" && masterEvent ? masterEvent : event; // Reset validation errors when modal opens setShowValidationErrors(false); // Editing existing event - populate fields with event data setTitle(eventToDisplay.title ?? ""); setDescription(eventToDisplay.description ?? ""); setLocation(eventToDisplay.location ?? ""); // Handle all-day events properly const isAllDay = eventToDisplay.allday ?? false; setAllDay(isAllDay); // Get event's original timezone const eventTimezone = eventToDisplay.timezone ? resolveTimezone(eventToDisplay.timezone) : resolveTimezone(browserDefaultTimeZone); // Format dates based on all-day status if (eventToDisplay.start) { if (isAllDay) { // For all-day events, use date format (YYYY-MM-DD) const startDate = new Date(eventToDisplay.start); setStart(startDate.toISOString().split("T")[0]); } else { // For timed events, format in the event's original timezone setStart( formatDateTimeInTimezone(eventToDisplay.start, eventTimezone) ); } } else { setStart(""); } if (eventToDisplay.end) { if (isAllDay) { // For all-day events, use date format (YYYY-MM-DD) const endDate = new Date(eventToDisplay.end); endDate.setDate(endDate.getDate() - 1); setEnd(endDate.toISOString().split("T")[0]); } else { // For timed events, format in the event's original timezone setEnd(formatDateTimeInTimezone(eventToDisplay.end, eventTimezone)); } } else { setEnd(""); } // Find correct calendar index setCalendarid(calId); // Handle repetition properly - check both current event and base event const baseEventId = extractEventBaseUuid(eventToDisplay.uid); const baseEvent = calList[calId]?.events[baseEventId]; const repetitionSource = eventToDisplay.repetition || baseEvent?.repetition; if (repetitionSource && repetitionSource.freq) { const repetitionData: RepetitionObject = { freq: repetitionSource.freq, interval: repetitionSource.interval || 1, occurrences: repetitionSource.occurrences, endDate: repetitionSource.endDate, byday: repetitionSource.byday || null, }; setRepetition(repetitionData); setShowRepeat(true); } else { setRepetition({} as RepetitionObject); setShowRepeat(false); } setAttendees( eventToDisplay.attendee ? eventToDisplay.attendee.filter( (a: userAttendee) => a.cal_address !== eventToDisplay.organizer?.cal_address ) : [] ); setAlarm(eventToDisplay.alarm?.trigger ?? ""); setEventClass(eventToDisplay.class ?? "PUBLIC"); setBusy(eventToDisplay.transp ?? "OPAQUE"); if (eventToDisplay.timezone) { const resolvedTimezone = resolveTimezone(eventToDisplay.timezone); setTimezone(resolvedTimezone); } else { const browserTz = resolveTimezone(browserDefaultTimeZone); setTimezone(browserTz); } setHasVideoConference( eventToDisplay.x_openpass_videoconference ? true : false ); setMeetingLink(eventToDisplay.x_openpass_videoconference || null); setNewCalId(eventToDisplay.calId || calId); // Update description to include video conference footer if exists if ( eventToDisplay.x_openpass_videoconference && eventToDisplay.description ) { const hasVideoFooter = eventToDisplay.description.includes("Visio:"); if (!hasVideoFooter) { setDescription( addVideoConferenceToDescription( eventToDisplay.description, eventToDisplay.x_openpass_videoconference ) ); } else { setDescription(eventToDisplay.description); } } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ open, event, calId, userPersonalCalendars, calList, masterEvent, isLoadingMasterEvent, ]); // Helper to close modal(s) - use onCloseAll if available to close preview modal too const closeModal = () => { if (onCloseAll) { onCloseAll(); } else { onClose({}, "backdropClick"); } }; const handleClose = () => { // Clear temp data when user manually closes modal clearEventFormTempData("update"); closeModal(); setShowValidationErrors(false); resetAllStateToDefault(); initializedKeyRef.current = null; }; // Function to save current form data to temp storage const saveCurrentFormData = useCallback(() => { const formState: EventFormState = { title, description, location, start, end, allday, repetition, attendees, alarm, busy, eventClass, timezone, calendarid, hasVideoConference, meetingLink, showMore, showDescription, showRepeat, hasEndDateChanged, }; const context: EventFormContext = { eventId, calId, typeOfAction, }; return buildEventFormTempData(formState, context); }, [ title, description, location, start, end, allday, repetition, attendees, alarm, busy, eventClass, timezone, calendarid, hasVideoConference, meetingLink, showMore, showDescription, showRepeat, hasEndDateChanged, eventId, calId, typeOfAction, ]); // Check for temp data when modal opens useEffect(() => { if (open && event) { const tempData = restoreEventFormDataFromStorage("update"); if ( tempData && tempData.fromError && tempData.eventId === eventId && tempData.calId === calId && tempData.typeOfAction === typeOfAction ) { // Mark that we're restoring from error to prevent other useEffects from overriding isRestoringFromErrorRef.current = true; // Restore form data from previous failed save restoreFormDataFromTemp(tempData, { setTitle, setDescription, setLocation, setStart, setEnd, setAllDay, setRepetition, setAttendees, setAlarm, setBusy, setEventClass, setTimezone, setCalendarid, setHasVideoConference, setMeetingLink, setShowMore, setShowDescription, setShowRepeat, setHasEndDateChanged, }); // Clear the error flag but keep data until successful save const updatedTempData = { ...tempData, fromError: false }; saveEventFormDataToTemp("update", updatedTempData); // Reset flag after restore completes (use setTimeout to ensure other useEffects have checked the flag) setTimeout(() => { isRestoringFromErrorRef.current = false; }, 0); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, eventId, calId, typeOfAction]); const handleSave = async () => { // Show validation errors when Save is clicked setShowValidationErrors(true); // Check if form is valid before saving if (!isFormValid) { return; } if (!event) { return; } const organizer = event.organizer; const targetCalendar = calList[calendarid]; if (!targetCalendar) { console.error("Target calendar not found"); return; } // Reset validation state when validation passes setShowValidationErrors(false); // Handle recurrence instances const [baseUID, recurrenceId] = event.uid.split("/"); // Check if this is a recurring event const isRecurringEvent = !!event.repetition?.freq; const getSeriesInstances = (): Record => { const instances: Record = {}; const seriesEvents = targetCalendar.events || {}; Object.keys(seriesEvents).forEach((eventId) => { const instance = seriesEvents[eventId]; if (instance && extractEventBaseUuid(eventId) === baseUID) { instances[eventId] = { ...instance }; } }); return instances; }; // When editing "all events" of a recurring series, use the master event we already fetched let masterEventData: CalendarEvent | null = null; if (isRecurringEvent && typeOfAction === "all") { // We already have the master event from state masterEventData = masterEvent; if (!masterEventData) { // This shouldn't happen, but handle it gracefully const formDataToSave = saveCurrentFormData(); const errorFormData = { ...formDataToSave, fromError: true, }; saveEventFormDataToTemp("update", errorFormData); showErrorNotification( "Failed to load master event data. Please try again." ); // Dispatch eventModalError to reopen modal window.dispatchEvent( new CustomEvent("eventModalError", { detail: { type: "update", eventId, calId, typeOfAction }, }) ); return; } } // Handle start and end dates based on all-day status let startDate: string; let endDate: string; // For single events or "solo" edits, use the edited dates from form if (allday) { // For all-day events, use date format (YYYY-MM-DD) // Extract date string directly to avoid timezone conversion issues const startDateOnly = (start || "").split("T")[0]; const endDateOnlyUI = (end || start || "").split("T")[0]; // API needs end date = UI end date + 1 day const endDateOnlyAPI = addDays(endDateOnlyUI, 1); // Parse date string and create Date at UTC midnight to avoid timezone offset issues const [startYear, startMonth, startDay] = startDateOnly .split("-") .map(Number); const [endYear, endMonth, endDay] = endDateOnlyAPI.split("-").map(Number); const startDateObj = new Date( Date.UTC(startYear, startMonth - 1, startDay, 0, 0, 0, 0) ); const endDateObj = new Date( Date.UTC(endYear, endMonth - 1, endDay, 0, 0, 0, 0) ); startDate = startDateObj.toISOString(); endDate = endDateObj.toISOString(); } else { // For timed events startDate = convertFormDateTimeToISO(start, timezone); // In normal mode, only override end date when the end date field is not shown and end date is same as start date const startDateOnly = (start || "").split("T")[0]; const endDateOnly = (end || "").split("T")[0]; if (!showMore && !hasEndDateChanged && startDateOnly === endDateOnly) { const endTimeOnly = end.includes("T") ? end.split("T")[1]?.slice(0, 5) || "00:00" : "00:00"; const endDateTime = `${startDateOnly}T${endTimeOnly}`; endDate = convertFormDateTimeToISO(endDateTime, timezone); } else { // Extended mode or end date explicitly shown in normal mode or end date differs from start date: use actual end datetime endDate = convertFormDateTimeToISO(end, timezone); } } const eventStartChanged = new Date(event.start).getTime() !== new Date(startDate).getTime(); const eventEndChanged = new Date(event?.end ?? "").getTime() !== new Date(endDate).getTime(); const timeChanged = eventStartChanged || eventEndChanged; const newEvent: CalendarEvent = { ...updateAttendeesAfterTimeChange(event, timeChanged, attendees), calId: newCalId || calId, title, URL: event.URL ?? `/calendars/${newCalId || calId}/${event.uid}.ics`, start: startDate, end: endDate, allday, uid: event.uid, description, location, repetition, class: eventClass, organizer: organizer, timezone, transp: busy, sequence: (event.sequence ?? 1) + 1, color: targetCalendar?.color, alarm: { trigger: alarm, action: "EMAIL" }, x_openpass_videoconference: meetingLink || undefined, }; // Special case: When converting recurring event to non-recurring if ( recurrenceId && typeOfAction === "all" && event.repetition?.freq && !repetition.freq ) { const baseUID = extractEventBaseUuid(event.uid); // Save current form data to temp storage before closing const formDataToSave = saveCurrentFormData(); saveEventFormDataToTemp("update", formDataToSave); // Close modal immediately for better UX closeModal(); const seriesInstancesSnapshot = getSeriesInstances(); let hasRemovedSeriesInstances = false; let createdSingleEventUid: string | null = null; const removeSeriesInstancesFromUI = () => { if (!seriesInstancesSnapshot) return; Object.keys(seriesInstancesSnapshot).forEach((eventId) => { dispatch(removeEvent({ calendarUid: calId, eventUid: eventId })); }); hasRemovedSeriesInstances = true; }; const restoreSeriesInstancesToUI = () => { if (!seriesInstancesSnapshot) return; Object.values(seriesInstancesSnapshot).forEach((instance) => { dispatch( updateEventLocal({ calId, event: instance, }) ); }); }; try { // STEP 1: Delete ALL instances of recurring event // Note: This system stores instances only, no master event file // Collect all instances that need to be deleted const instancesToDelete = Object.keys(targetCalendar.events) .filter((eventId) => extractEventBaseUuid(eventId) === baseUID) .map((eventId) => targetCalendar.events[eventId]); // Get unique URLs to avoid deleting same file multiple times const uniqueURLs = new Set(); const instancesByURL = new Map(); instancesToDelete.forEach((instance) => { if (!instancesByURL.has(instance.URL)) { instancesByURL.set(instance.URL, []); } instancesByURL.get(instance.URL)!.push(instance); uniqueURLs.add(instance.URL); }); // Delete each unique URL once const deletePromises = Array.from(uniqueURLs).map(async (url) => { try { await deleteEvent(url); } catch (deleteError) { // Check if error is an object with response or message properties const errorObj = deleteError as { response?: { status?: number }; message?: string; }; // Silently ignore 404 - file might already be deleted const is404 = errorObj.response?.status === 404 || errorObj.message?.includes("404") || errorObj.message?.includes("Not Found"); if (!is404) { console.error( `Failed to delete event file: ${errorObj.message || "Unknown error"}` ); } } }); await Promise.all(deletePromises); // Small delay to ensure backend processes deletions await new Promise((resolve) => setTimeout(resolve, 100)); // STEP 2: Create new non-recurring event const newEventUID = crypto.randomUUID(); const finalNewEvent = { ...newEvent, uid: newEventUID, URL: `/calendars/${newCalId || calId}/${newEventUID}.ics`, sequence: 1, // New event with new UID starts at sequence 1 recurrenceId: undefined, }; // STEP 3: Persist new event to server await putEvent(finalNewEvent, targetCalendar.owner?.emails?.[0]); // STEP 4: Update Redux store - Add new event first to prevent empty grid dispatch(updateEventLocal({ calId, event: finalNewEvent })); createdSingleEventUid = finalNewEvent.uid; // Clear cache to ensure navigation to other weeks works dispatch(clearFetchCache(calId)); // STEP 5: Remove old recurring instances only after the rest succeeds removeSeriesInstancesFromUI(); // Clear temp data on successful save clearEventFormTempData("update"); // Reset all state to default values only on successful save resetAllStateToDefault(); initializedKeyRef.current = null; } catch (err) { // Check if error is an object with a message property const errorObj = err as { message?: string }; // API failed - restore form data and mark as error const errorFormData = { ...formDataToSave, fromError: true, }; saveEventFormDataToTemp("update", errorFormData); // Show error notification showErrorNotification( errorObj.message || "Failed to convert recurring event. Please try again." ); if (createdSingleEventUid) { dispatch( removeEvent({ calendarUid: calId, eventUid: createdSingleEventUid }) ); } if (hasRemovedSeriesInstances) { restoreSeriesInstancesToUI(); } // Try to reopen modal by dispatching custom event window.dispatchEvent( new CustomEvent("eventModalError", { detail: { type: "update", eventId, calId, typeOfAction }, }) ); } return; } // Save current form data to temp storage before closing const formDataToSave = saveCurrentFormData(); saveEventFormDataToTemp("update", formDataToSave); // Close popup immediately for better UX (for non-special cases) closeModal(); // Execute API calls in background based on typeOfAction try { if (recurrenceId) { if (typeOfAction === "solo") { // Update single instance with optimistic update + rollback dispatch( updateEventLocal({ calId, event: { ...newEvent, recurrenceId }, }) ); const result = await dispatch( updateEventInstanceAsync({ cal: targetCalendar, event: { ...newEvent, recurrenceId }, }) ); // Handle result of updateEventInstanceAsync await assertThunkSuccess(result); // Clear temp data on successful save clearEventFormTempData("update"); } else if (typeOfAction === "all") { // Update all instances - check if repetition rules changed const baseUID = extractEventBaseUuid(event.uid); const changes = detectRecurringEventChanges( event, { repetition, timezone, allday, start, end }, masterEventData, resolveTimezone ); const repetitionRulesChanged = changes.repetitionRulesChanged; if (repetitionRulesChanged) { // Date/time or repetition rules changed - remove all overrides const seriesInstancesSnapshot = getSeriesInstances(); const removeSeriesInstancesFromUI = () => { Object.keys(seriesInstancesSnapshot).forEach((eventId) => { dispatch( removeEvent({ calendarUid: calId, eventUid: eventId }) ); }); }; const restoreSeriesInstancesFromSnapshot = () => { Object.values(seriesInstancesSnapshot).forEach((instance) => { dispatch( updateEventLocal({ calId, event: instance, }) ); }); }; try { // STEP 1: Remove ALL old instances from UI (including solo overrides) removeSeriesInstancesFromUI(); // STEP 2: Update series on server with removeOverrides=true // IMPORTANT: Use base event UID (master), not instance UID with recurrence-id const masterEventForUpdate = { ...newEvent, uid: baseUID, // Use base UID for updating the master recurrenceId: undefined, // Don't send recurrence-id for master update }; const result = await dispatch( updateSeriesAsync({ cal: targetCalendar, event: masterEventForUpdate, removeOverrides: true, }) ); // Handle result of updateSeriesAsync await assertThunkSuccess(result); // Clear cache after successful update dispatch(clearFetchCache(calId)); // Clear temp data on successful save clearEventFormTempData("update"); } catch (seriesError) { // Restore instances on error restoreSeriesInstancesFromSnapshot(); throw seriesError; } } else { // Only properties changed - use optimistic update and keep overrides // Store old instances for rollback const oldInstances = getSeriesInstances(); // Optimistic update: Apply new properties to all instances immediately Object.keys(oldInstances).forEach((eventId) => { const instance = oldInstances[eventId]; dispatch( updateEventLocal({ calId, event: { ...instance, title: newEvent.title, description: newEvent.description, location: newEvent.location, class: newEvent.class, transp: newEvent.transp, attendee: newEvent.attendee, alarm: newEvent.alarm, x_openpass_videoconference: newEvent.x_openpass_videoconference, }, }) ); }); // Update server in background with removeOverrides=false // IMPORTANT: Use base event UID (master), not instance UID with recurrence-id const masterEventForUpdate = { ...newEvent, uid: baseUID, // Use base UID for updating the master recurrenceId: undefined, // Don't send recurrence-id for master update }; const result = await dispatch( updateSeriesAsync({ cal: targetCalendar, event: masterEventForUpdate, removeOverrides: false, }) ); // Handle result of updateSeriesAsync await assertThunkSuccess(result); // Clear cache to ensure navigation shows updated data dispatch(clearFetchCache(calId)); // Clear temp data on successful save clearEventFormTempData("update"); } } } else { // Non-recurring event (or converting to recurring) // Special case: Converting no-repeat to repeat if (!event.repetition?.freq && repetition?.freq) { const oldEventUID = event.uid; // API call: putEventAsync will create recurring event and fetch all instances const result = await dispatch( putEventAsync({ cal: targetCalendar, newEvent }) ); // Handle result of putEventAsync - check if rejected first await assertThunkSuccess(result); // Remove old single event AFTER new recurring instances are added to store // This prevents empty grid during the transition dispatch(removeEvent({ calendarUid: calId, eventUid: oldEventUID })); // Clear cache to ensure navigation to other weeks works dispatch(clearFetchCache(calId)); // Clear temp data on successful save clearEventFormTempData("update"); } else if (newCalId === calId) { // Normal non-recurring event update (same calendar) const result = await dispatch( putEventAsync({ cal: targetCalendar, newEvent }) ); await assertThunkSuccess(result); // 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 (move to a different calendar) if (newCalId !== calId) { await moveEventBetweenCalendars({ dispatch, calList, newEvent, oldCalId: calId, newCalId, }); // Clear temp data on successful move clearEventFormTempData("update"); } // Reset all state to default values only on successful save (after all branches) clearEventFormTempData("update"); resetAllStateToDefault(); initializedKeyRef.current = null; } catch (error) { // Check if error is an object with a message property const errorObj = error as { message?: string }; // Handle errors for all branches // Rollback optimistic updates if any // API failed - restore form data and mark as error const errorFormData = { ...formDataToSave, fromError: true, }; saveEventFormDataToTemp("update", errorFormData); // Show error notification showErrorNotification( errorObj.message || "Failed to update event. Please try again." ); // Try to reopen modal window.dispatchEvent( new CustomEvent("eventModalError", { detail: { type: "update", eventId, calId, typeOfAction }, }) ); } }; const dialogActions = ( {!showMore && ( )} ); if (!event) return null; return ( setShowMore(!showMore)} actions={dialogActions} > { const selectedCalendar = calList[newCalendarId]; if (selectedCalendar) { setNewCalId(selectedCalendar.id); } }} onValidationChange={setIsFormValid} showValidationErrors={showValidationErrors} onHasEndDateChangedChange={setHasEndDateChanged} /> ); } export default EventUpdateModal;