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:
lenhanphung
2025-11-25 21:30:58 +07:00
committed by GitHub
parent 25dc016688
commit 9151ff5563
12 changed files with 1324 additions and 240 deletions
+61 -5
View File
@@ -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
);
+46 -13
View File
@@ -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,