fix: preserve form data on API error and fix test cases (#338)
- Add temp storage utility (eventFormTempStorage.ts) to save/restore form data - Implement error handling: close modal immediately, reopen on API failure with saved data - Add event listeners in Calendar.tsx and EventDisplayPreview.tsx to reopen modals on error - Fix calendar change logic in EventUpdateModal: use oldCalendar for update before move - Update all test mocks to support .unwrap() method for Redux thunks - Add sessionStorage.clear() in test beforeEach hooks - Fix test timing issues with act() wrappers and increased timeouts - Mock putEventAsync in TempUpdate test to ensure updateTempCalendar is called - Fix update modal not reopening when API fails for recurring events (solo/all) - Move update modal reopen logic from EventDisplayPreview to Calendar.tsx - Use sessionStorage to persist update modal info across component unmounts - Handle typeOfAction matching for recurring events (undefined vs solo/all) - Fix error handling in updateEventInstanceAsync and updateSeriesAsync - Ensure all API failures properly dispatch eventModalError event
This commit is contained in:
@@ -25,7 +25,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import AvatarGroup from "@mui/material/AvatarGroup";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import { CalendarName } from "../../components/Calendar/CalendarName";
|
||||
import { getTimezoneOffset } from "../../components/Calendar/TimezoneSelector";
|
||||
@@ -92,6 +92,7 @@ export default function EventPreviewModal({
|
||||
);
|
||||
const [afterChoiceFunc, setAfterChoiceFunc] = useState<Function>();
|
||||
const attendeePreview = makeAttendeePreview(event.attendee, t);
|
||||
const hasCheckedSessionStorageRef = useRef(false);
|
||||
|
||||
const [toggleActionMenu, setToggleActionMenu] = useState<Element | null>(
|
||||
null
|
||||
@@ -104,6 +105,157 @@ export default function EventPreviewModal({
|
||||
}
|
||||
}, [event, calendar, onClose]);
|
||||
|
||||
// Check sessionStorage when component mounts or when open becomes true (only once per open)
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
hasCheckedSessionStorageRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check once when open becomes true
|
||||
if (hasCheckedSessionStorageRef.current) return;
|
||||
hasCheckedSessionStorageRef.current = true;
|
||||
|
||||
const checkAndReopen = () => {
|
||||
try {
|
||||
const stored = sessionStorage.getItem("eventUpdateModalReopen");
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored);
|
||||
|
||||
// Check if stored data matches current preview
|
||||
// For recurring events, typeOfAction from sessionStorage should be used
|
||||
// Allow undefined to match undefined, or use stored typeOfAction if current is undefined
|
||||
const typeOfActionMatch =
|
||||
data.typeOfAction === typeOfAction ||
|
||||
(data.typeOfAction === undefined && typeOfAction === undefined) ||
|
||||
(data.typeOfAction !== undefined && typeOfAction === undefined); // Allow stored typeOfAction to match when current is undefined
|
||||
|
||||
if (
|
||||
data.eventId === eventId &&
|
||||
data.calId === calId &&
|
||||
typeOfActionMatch
|
||||
) {
|
||||
// Restore typeOfAction from sessionStorage if it exists
|
||||
if (data.typeOfAction !== undefined && typeOfAction === undefined) {
|
||||
setTypeOfAction(data.typeOfAction);
|
||||
// Open modal immediately with the typeOfAction from sessionStorage
|
||||
// Use setTimeout to ensure state is set before opening
|
||||
setTimeout(() => {
|
||||
setOpenUpdateModal(true);
|
||||
setHidePreview(true);
|
||||
sessionStorage.removeItem("eventUpdateModalReopen");
|
||||
}, 50);
|
||||
} else {
|
||||
// Small delay to ensure component is fully mounted
|
||||
setTimeout(() => {
|
||||
setOpenUpdateModal(true);
|
||||
setHidePreview(true);
|
||||
sessionStorage.removeItem("eventUpdateModalReopen");
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore sessionStorage errors
|
||||
}
|
||||
};
|
||||
|
||||
checkAndReopen();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, eventId, calId]); // Removed typeOfAction from dependencies to prevent re-checking when typeOfAction changes
|
||||
|
||||
// Listen for eventUpdateModalReopen event to reopen update modal on API failure
|
||||
useEffect(() => {
|
||||
const handleUpdateModalReopen = (event: CustomEvent) => {
|
||||
const detail = event.detail;
|
||||
|
||||
// Check if this event matches current preview
|
||||
// For recurring events, typeOfAction from event should be used
|
||||
// Allow undefined to match undefined, or use event typeOfAction if current is undefined
|
||||
const typeOfActionMatch =
|
||||
detail?.typeOfAction === typeOfAction ||
|
||||
(detail?.typeOfAction === undefined && typeOfAction === undefined) ||
|
||||
(detail?.typeOfAction !== undefined && typeOfAction === undefined); // Allow event typeOfAction to match when current is undefined
|
||||
if (
|
||||
detail?.eventId === eventId &&
|
||||
detail?.calId === calId &&
|
||||
typeOfActionMatch
|
||||
) {
|
||||
// Restore typeOfAction from event if it exists
|
||||
if (detail?.typeOfAction !== undefined && typeOfAction === undefined) {
|
||||
setTypeOfAction(detail.typeOfAction);
|
||||
// Open modal immediately with the typeOfAction from event
|
||||
setTimeout(() => {
|
||||
setOpenUpdateModal(true);
|
||||
setHidePreview(true);
|
||||
}, 50);
|
||||
} else {
|
||||
setOpenUpdateModal(true);
|
||||
setHidePreview(true);
|
||||
}
|
||||
// Clear sessionStorage after reopening
|
||||
try {
|
||||
sessionStorage.removeItem("eventUpdateModalReopen");
|
||||
} catch (err) {
|
||||
// Ignore sessionStorage errors
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Also check sessionStorage on mount or when eventId/calId changes
|
||||
const checkSessionStorage = () => {
|
||||
try {
|
||||
const stored = sessionStorage.getItem("eventUpdateModalReopen");
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored);
|
||||
// Check if stored data matches current preview
|
||||
// For recurring events, typeOfAction from sessionStorage should be used
|
||||
// Allow undefined to match undefined, or use stored typeOfAction if current is undefined
|
||||
const typeOfActionMatch =
|
||||
data.typeOfAction === typeOfAction ||
|
||||
(data.typeOfAction === undefined && typeOfAction === undefined) ||
|
||||
(data.typeOfAction !== undefined && typeOfAction === undefined); // Allow stored typeOfAction to match when current is undefined
|
||||
if (
|
||||
data.eventId === eventId &&
|
||||
data.calId === calId &&
|
||||
typeOfActionMatch
|
||||
) {
|
||||
// Restore typeOfAction from sessionStorage if it exists
|
||||
if (data.typeOfAction !== undefined && typeOfAction === undefined) {
|
||||
setTypeOfAction(data.typeOfAction);
|
||||
// Open modal immediately with the typeOfAction from sessionStorage
|
||||
setTimeout(() => {
|
||||
setOpenUpdateModal(true);
|
||||
setHidePreview(true);
|
||||
sessionStorage.removeItem("eventUpdateModalReopen");
|
||||
}, 50);
|
||||
} else {
|
||||
setOpenUpdateModal(true);
|
||||
setHidePreview(true);
|
||||
sessionStorage.removeItem("eventUpdateModalReopen");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore sessionStorage errors
|
||||
}
|
||||
};
|
||||
|
||||
// Check on mount and when relevant props change
|
||||
checkSessionStorage();
|
||||
|
||||
window.addEventListener(
|
||||
"eventUpdateModalReopen",
|
||||
handleUpdateModalReopen as EventListener
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"eventUpdateModalReopen",
|
||||
handleUpdateModalReopen as EventListener
|
||||
);
|
||||
};
|
||||
}, [eventId, calId, typeOfAction]);
|
||||
|
||||
if (!event || !calendar) return null;
|
||||
|
||||
const attendeeDisplayLimit = 3;
|
||||
@@ -692,7 +844,29 @@ export default function EventPreviewModal({
|
||||
}}
|
||||
eventId={eventId}
|
||||
calId={calId}
|
||||
typeOfAction={typeOfAction}
|
||||
typeOfAction={(() => {
|
||||
// Get typeOfAction from state or sessionStorage
|
||||
if (typeOfAction) {
|
||||
return typeOfAction;
|
||||
}
|
||||
// Fallback: try to get typeOfAction from sessionStorage if state is not set yet
|
||||
try {
|
||||
const stored = sessionStorage.getItem("eventUpdateModalReopen");
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored);
|
||||
if (
|
||||
data.eventId === eventId &&
|
||||
data.calId === calId &&
|
||||
data.typeOfAction
|
||||
) {
|
||||
return data.typeOfAction;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
return undefined;
|
||||
})()}
|
||||
/>
|
||||
<EventPopover
|
||||
anchorEl={null}
|
||||
|
||||
Reference in New Issue
Block a user