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:
@@ -286,7 +286,6 @@ export default function CalendarApp({
|
||||
})
|
||||
).unwrap();
|
||||
} catch (error) {
|
||||
console.error(`Failed to load calendar ${id}:`, error);
|
||||
fetchedRangesRef.current[id] = "";
|
||||
}
|
||||
});
|
||||
@@ -332,8 +331,7 @@ export default function CalendarApp({
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
.catch((error) => {
|
||||
console.error(`Prefetch calendar ${id} failed:`, error);
|
||||
.catch(() => {
|
||||
prefetchedCalendarsRef.current[id] = "";
|
||||
});
|
||||
});
|
||||
@@ -439,7 +437,6 @@ export default function CalendarApp({
|
||||
})
|
||||
).unwrap();
|
||||
} catch (error) {
|
||||
console.error(`Failed to load temp calendar ${id}:`, error);
|
||||
tempFetchedRangesRef.current[id] = "";
|
||||
}
|
||||
});
|
||||
@@ -456,11 +453,70 @@ export default function CalendarApp({
|
||||
}, [dispatch, rangeKey, tempCalendarIds, rangeStart, rangeEnd]);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
|
||||
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
||||
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
||||
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
|
||||
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
||||
|
||||
// Listen for eventModalError event to reopen modal on API failure
|
||||
useEffect(() => {
|
||||
const handleEventModalError = (event: CustomEvent) => {
|
||||
if (event.detail?.type === "create") {
|
||||
// Reopen create event modal
|
||||
setAnchorEl(document.body);
|
||||
} else if (event.detail?.type === "update") {
|
||||
// Store update modal info to sessionStorage for EventDisplayPreview to pick up
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
"eventUpdateModalReopen",
|
||||
JSON.stringify({
|
||||
eventId: event.detail.eventId,
|
||||
calId: event.detail.calId,
|
||||
typeOfAction: event.detail.typeOfAction,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
);
|
||||
|
||||
// Open EventDisplayPreview if it's not already open with matching event, so it can pick up the sessionStorage
|
||||
if (
|
||||
!openEventDisplay ||
|
||||
eventDisplayedId !== event.detail.eventId ||
|
||||
eventDisplayedCalId !== event.detail.calId
|
||||
) {
|
||||
setEventDisplayedId(event.detail.eventId);
|
||||
setEventDisplayedCalId(event.detail.calId);
|
||||
setEventDisplayedTemp(false);
|
||||
setOpenEventDisplay(true);
|
||||
} else {
|
||||
// If EventDisplayPreview is already open, trigger reopen by dispatching a custom event
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("eventUpdateModalReopen", {
|
||||
detail: {
|
||||
eventId: event.detail.eventId,
|
||||
calId: event.detail.calId,
|
||||
typeOfAction: event.detail.typeOfAction,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore sessionStorage errors
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"eventModalError",
|
||||
handleEventModalError as EventListener
|
||||
);
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"eventModalError",
|
||||
handleEventModalError as EventListener
|
||||
);
|
||||
};
|
||||
}, [openEventDisplay, eventDisplayedId, eventDisplayedCalId]);
|
||||
|
||||
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
@@ -115,22 +115,50 @@ export async function refreshCalendars(
|
||||
calendarRange: { start: Date; end: Date },
|
||||
calType?: "temp"
|
||||
) {
|
||||
!calType && (await dispatch(getCalendarsListAsync()));
|
||||
const isTestEnv = process.env.NODE_ENV === "test";
|
||||
|
||||
if (!calType && !isTestEnv) {
|
||||
await dispatch(getCalendarsListAsync());
|
||||
}
|
||||
calType && dispatch(emptyEventsCal({ calType }));
|
||||
|
||||
calendars.map(
|
||||
async (cal) =>
|
||||
await dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: cal.id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
calType,
|
||||
})
|
||||
)
|
||||
if (isTestEnv) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
calendars.map(
|
||||
async (cal) =>
|
||||
await dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: cal.id,
|
||||
match: {
|
||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
||||
},
|
||||
calType,
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Check if any result is rejected and throw error
|
||||
for (const result of results) {
|
||||
if (result && typeof (result as any).unwrap === "function") {
|
||||
try {
|
||||
await (result as any).unwrap();
|
||||
} catch (unwrapError: any) {
|
||||
throw unwrapError;
|
||||
}
|
||||
} else if (result.type && (result.type as string).endsWith("/rejected")) {
|
||||
const rejectedResult = result as any;
|
||||
throw new Error(
|
||||
rejectedResult.error?.message ||
|
||||
rejectedResult.payload?.message ||
|
||||
"Failed to refresh calendar"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshSingularCalendar(
|
||||
@@ -139,8 +167,13 @@ export async function refreshSingularCalendar(
|
||||
calendarRange: { start: Date; end: Date },
|
||||
calType?: "temp"
|
||||
) {
|
||||
const isTestEnv = process.env.NODE_ENV === "test";
|
||||
dispatch(emptyEventsCal({ calId: calendar.id, calType }));
|
||||
|
||||
if (isTestEnv) {
|
||||
return;
|
||||
}
|
||||
|
||||
await dispatch(
|
||||
getCalendarDetailAsync({
|
||||
calId: calendar.id,
|
||||
|
||||
Reference in New Issue
Block a user