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
+172
View File
@@ -0,0 +1,172 @@
import { RepetitionObject } from "../features/Events/EventsTypes";
import { userAttendee } from "../features/User/userDataTypes";
export interface EventFormTempData {
// Form fields
title: string;
description: string;
location: string;
start: string;
end: string;
allday: boolean;
repetition: RepetitionObject;
attendees: userAttendee[];
alarm: string;
busy: string;
eventClass: string;
timezone: string;
calendarid: string;
hasVideoConference: boolean;
meetingLink: string | null;
// UI state
showMore?: boolean;
showDescription?: boolean;
showRepeat?: boolean;
hasEndDateChanged?: boolean;
// Context (for update modal only)
eventId?: string;
calId?: string;
typeOfAction?: "solo" | "all";
// Flag to indicate this is from an error
fromError?: boolean;
}
const STORAGE_KEY_PREFIX = "eventFormTempData_";
export function saveEventFormDataToTemp(
modalType: "create" | "update",
formData: EventFormTempData
): void {
try {
const key = `${STORAGE_KEY_PREFIX}${modalType}`;
sessionStorage.setItem(key, JSON.stringify(formData));
} catch (error) {
console.error("Failed to save form data to temp storage:", error);
}
}
export function restoreEventFormDataFromTemp(
modalType: "create" | "update"
): EventFormTempData | null {
try {
const key = `${STORAGE_KEY_PREFIX}${modalType}`;
const data = sessionStorage.getItem(key);
if (data) {
return JSON.parse(data) as EventFormTempData;
}
return null;
} catch (error) {
console.error("Failed to restore form data from temp storage:", error);
return null;
}
}
export function clearEventFormTempData(modalType: "create" | "update"): void {
try {
const key = `${STORAGE_KEY_PREFIX}${modalType}`;
sessionStorage.removeItem(key);
} catch (error) {
console.error("Failed to clear form data from temp storage:", error);
}
}
export function showErrorNotification(message: string): void {
console.error(`[ERROR] ${message}`);
}
export interface EventFormState {
title: string;
description: string;
location: string;
start: string;
end: string;
allday: boolean;
repetition: RepetitionObject;
attendees: userAttendee[];
alarm: string;
busy: string;
eventClass: string;
timezone: string;
calendarid: string;
hasVideoConference: boolean;
meetingLink: string | null;
showMore?: boolean;
showDescription?: boolean;
showRepeat?: boolean;
hasEndDateChanged?: boolean;
}
export interface EventFormContext {
eventId?: string;
calId?: string;
typeOfAction?: "solo" | "all";
}
export function buildEventFormTempData(
formState: EventFormState,
context?: EventFormContext
): EventFormTempData {
return {
...formState,
...context,
fromError: false,
};
}
export interface EventFormSetters {
setTitle: (value: string) => void;
setDescription: (value: string) => void;
setLocation: (value: string) => void;
setStart: (value: string) => void;
setEnd: (value: string) => void;
setAllDay: (value: boolean) => void;
setRepetition: (value: RepetitionObject) => void;
setAttendees: (value: userAttendee[]) => void;
setAlarm: (value: string) => void;
setBusy: (value: string) => void;
setEventClass: (value: string) => void;
setTimezone: (value: string) => void;
setCalendarid: (value: string) => void;
setHasVideoConference: (value: boolean) => void;
setMeetingLink: (value: string | null) => void;
setShowMore?: (value: boolean) => void;
setShowDescription?: (value: boolean) => void;
setShowRepeat?: (value: boolean) => void;
setHasEndDateChanged?: (value: boolean) => void;
}
export function restoreFormDataFromTemp(
tempData: EventFormTempData,
setters: EventFormSetters
): void {
setters.setTitle(tempData.title);
setters.setDescription(tempData.description);
setters.setLocation(tempData.location);
setters.setStart(tempData.start);
setters.setEnd(tempData.end);
setters.setAllDay(tempData.allday);
setters.setRepetition(tempData.repetition);
setters.setAttendees(tempData.attendees);
setters.setAlarm(tempData.alarm);
setters.setBusy(tempData.busy);
setters.setEventClass(tempData.eventClass);
setters.setTimezone(tempData.timezone);
setters.setCalendarid(tempData.calendarid);
setters.setHasVideoConference(tempData.hasVideoConference);
setters.setMeetingLink(tempData.meetingLink);
if (tempData.showMore !== undefined && setters.setShowMore) {
setters.setShowMore(tempData.showMore);
}
if (tempData.showDescription !== undefined && setters.setShowDescription) {
setters.setShowDescription(tempData.showDescription);
}
if (tempData.showRepeat !== undefined && setters.setShowRepeat) {
setters.setShowRepeat(tempData.showRepeat);
}
if (
tempData.hasEndDateChanged !== undefined &&
setters.setHasEndDateChanged
) {
setters.setHasEndDateChanged(tempData.hasEndDateChanged);
}
}