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:
@@ -121,7 +121,11 @@ async function setupEventPopover(
|
||||
async function expectRRule(expected: Partial<RepetitionObject>) {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
act(() => fireEvent.click(saveButton));
|
||||
await waitFor(() => expect(spy).toHaveBeenCalled());
|
||||
|
||||
@@ -25,6 +25,7 @@ describe("Event Preview Display", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
sessionStorage.clear();
|
||||
(window as any).MAIL_SPA_URL = null;
|
||||
});
|
||||
|
||||
@@ -355,7 +356,9 @@ describe("Event Preview Display", () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const rsvpState = {
|
||||
@@ -414,7 +417,9 @@ describe("Event Preview Display", () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const rsvpState = {
|
||||
@@ -471,7 +476,9 @@ describe("Event Preview Display", () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const rsvpState = {
|
||||
@@ -1694,10 +1701,18 @@ describe("Event Full Display", () => {
|
||||
it("saves event and moves it when calendar is changed", async () => {
|
||||
const spyPut = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const spyMove = jest
|
||||
.spyOn(eventThunks, "moveEventAsync")
|
||||
.mockImplementation((payload) => () => Promise.resolve(payload) as any);
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const spyRemove = jest.spyOn(eventThunks, "removeEvent");
|
||||
|
||||
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
@@ -1751,17 +1766,30 @@ describe("Event Full Display", () => {
|
||||
const option = await screen.findByText("Calendar Two");
|
||||
fireEvent.click(option);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spyPut).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spyMove).toHaveBeenCalled();
|
||||
});
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyPut).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
expect(spyRemove).toHaveBeenCalled();
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyMove).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyRemove).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
|
||||
it("edit modal displays event time in original event timezone", () => {
|
||||
|
||||
@@ -9,6 +9,10 @@ import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
jest.mock("../../../src/utils/apiUtils");
|
||||
|
||||
describe("EventPopover", () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
const mockSetSelectedRange = jest.fn();
|
||||
const mockCalendarRef = { current: { select: jest.fn() } } as any;
|
||||
@@ -224,7 +228,9 @@ describe("EventPopover", () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
@@ -290,7 +296,9 @@ describe("EventPopover", () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
|
||||
@@ -9,6 +9,17 @@ import {
|
||||
EventHandlersProps,
|
||||
} from "../../../src/components/Calendar/handlers/eventHandlers";
|
||||
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
|
||||
|
||||
jest.mock("../../../src/components/Event/utils/eventUtils", () => {
|
||||
const actual = jest.requireActual(
|
||||
"../../../src/components/Event/utils/eventUtils"
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
refreshCalendars: jest.fn(() => Promise.resolve()),
|
||||
refreshSingularCalendar: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
});
|
||||
import preview from "jest-preview";
|
||||
const mockOnClose = jest.fn();
|
||||
const day = new Date("2025-03-15T10:00:00Z");
|
||||
@@ -476,7 +487,9 @@ describe("RSVP to Recurring Event", () => {
|
||||
const spy = jest
|
||||
.spyOn(eventThunks, "updateEventInstanceAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
@@ -936,7 +949,9 @@ describe("handleRSVP function", () => {
|
||||
} = require("../../../src/components/Event/eventHandlers/eventHandlers");
|
||||
|
||||
jest.spyOn(eventThunks, "putEventAsync").mockImplementation((payload) => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const nonRecurringEvent = {
|
||||
@@ -1116,8 +1131,10 @@ describe("Event URL handling for recurring events", () => {
|
||||
const moveEventSpy = jest
|
||||
.spyOn(eventThunks, "moveEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
return () =>
|
||||
Promise.resolve({ calId: payload.cal.id, events: [] }) as any;
|
||||
const result = { calId: payload.cal.id, events: [] };
|
||||
const promise = Promise.resolve(result);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const twoCalState = {
|
||||
|
||||
@@ -13,6 +13,7 @@ describe("EventUpdateModal Timezone Handling", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
const preloadedState = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||
import "@testing-library/jest-dom";
|
||||
import { screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
import { screen, waitFor, fireEvent, act } from "@testing-library/react";
|
||||
import * as appHooks from "../../../src/app/hooks";
|
||||
import * as eventUtils from "../../../src/components/Event/utils/eventUtils";
|
||||
import * as userApi from "../../../src/features/User/userAPI";
|
||||
@@ -51,9 +51,16 @@ describe("Update tempcalendars called with correct params", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
|
||||
const dispatch = jest.fn((thunk) => {
|
||||
if (typeof thunk === "function") {
|
||||
return thunk(dispatch, () => ({}), undefined);
|
||||
}
|
||||
return thunk;
|
||||
}) as ThunkDispatch<any, any, any>;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
refreshCalendarsSpy = jest.spyOn(eventUtils, "refreshCalendars");
|
||||
refreshCalendarsSpy = jest
|
||||
.spyOn(eventUtils, "refreshCalendars")
|
||||
.mockResolvedValue();
|
||||
updateTempCalendarSpy = jest.spyOn(calendarUtils, "updateTempCalendar");
|
||||
refreshSingularCalendarSpy = jest.spyOn(
|
||||
eventUtils,
|
||||
@@ -214,6 +221,29 @@ describe("Update tempcalendars called with correct params", () => {
|
||||
resource: undefined,
|
||||
} as unknown as DateSelectArg;
|
||||
jest.spyOn(userApi, "searchUsers");
|
||||
|
||||
// Mock putEventAsync to return success with unwrap
|
||||
const putEventAsyncMock = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const action = {
|
||||
type: "calendars/putEvent/fulfilled",
|
||||
payload: {
|
||||
calId: payload.cal.id,
|
||||
events: [],
|
||||
calType: payload.calType,
|
||||
},
|
||||
unwrap: () =>
|
||||
Promise.resolve({
|
||||
calId: payload.cal.id,
|
||||
events: [],
|
||||
calType: payload.calType,
|
||||
}),
|
||||
};
|
||||
const promise = Promise.resolve(action) as any;
|
||||
return () => promise;
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<EventPopover
|
||||
anchorEl={null}
|
||||
@@ -238,22 +268,26 @@ describe("Update tempcalendars called with correct params", () => {
|
||||
fireEvent.keyDown(attendeeInput, { key: "Enter", code: "Enter" });
|
||||
|
||||
const saveButton = screen.getByText(/save/i);
|
||||
fireEvent.click(saveButton);
|
||||
await act(async () => {
|
||||
fireEvent.click(saveButton);
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
temp1: expect.objectContaining({ id: "temp1" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "New Event",
|
||||
}),
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
start: expect.any(Date),
|
||||
end: expect.any(Date),
|
||||
})
|
||||
)
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
temp1: expect.objectContaining({ id: "temp1" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: "New Event",
|
||||
}),
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
start: expect.any(Date),
|
||||
end: expect.any(Date),
|
||||
})
|
||||
),
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -32,6 +32,15 @@ import {
|
||||
import { convertFormDateTimeToISO } from "../../components/Event/utils/dateTimeHelpers";
|
||||
import { addDays } from "../../components/Event/utils/dateRules";
|
||||
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||
import {
|
||||
saveEventFormDataToTemp,
|
||||
restoreEventFormDataFromTemp as restoreEventFormDataFromStorage,
|
||||
clearEventFormTempData,
|
||||
showErrorNotification,
|
||||
buildEventFormTempData,
|
||||
restoreFormDataFromTemp,
|
||||
EventFormState,
|
||||
} from "../../utils/eventFormTempStorage";
|
||||
|
||||
function EventPopover({
|
||||
open,
|
||||
@@ -139,6 +148,8 @@ function EventPopover({
|
||||
// Use ref to track if we've already initialized to avoid infinite loop
|
||||
const isInitializedRef = useRef(false);
|
||||
const userPersonalCalendarsRef = useRef(userPersonalCalendars);
|
||||
// Track when restoring from error to prevent other useEffects from overriding restored data
|
||||
const isRestoringFromErrorRef = useRef(false);
|
||||
|
||||
// Update ref when userPersonalCalendars changes
|
||||
useEffect(() => {
|
||||
@@ -209,6 +220,10 @@ function EventPopover({
|
||||
|
||||
// Set start/end times when modal opens for new event creation
|
||||
useEffect(() => {
|
||||
// Skip if restoring from error - data already restored
|
||||
if (isRestoringFromErrorRef.current) {
|
||||
return;
|
||||
}
|
||||
// Only run when modal opens and not duplicating an event
|
||||
// Check if event has uid to determine if it's a valid event (not empty object)
|
||||
if (!shouldSyncFromRangeRef.current || !open || (event && event.uid)) {
|
||||
@@ -452,6 +467,10 @@ function EventPopover({
|
||||
|
||||
// Reset state when creating new event (event is empty object or undefined)
|
||||
useEffect(() => {
|
||||
// Skip if restoring from error - data already restored
|
||||
if (isRestoringFromErrorRef.current) {
|
||||
return;
|
||||
}
|
||||
const isCreatingNew = !event || !event.uid;
|
||||
const wasInitialized = isInitializedRef.current;
|
||||
|
||||
@@ -568,6 +587,8 @@ function EventPopover({
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
// Clear temp data when user manually closes modal
|
||||
clearEventFormTempData("create");
|
||||
onClose(false);
|
||||
setShowValidationErrors(false);
|
||||
resetAllStateToDefault();
|
||||
@@ -576,6 +597,98 @@ function EventPopover({
|
||||
shouldSyncFromRangeRef.current = true; // Reset for next time
|
||||
};
|
||||
|
||||
// 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,
|
||||
};
|
||||
return buildEventFormTempData(formState);
|
||||
}, [
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
start,
|
||||
end,
|
||||
allday,
|
||||
repetition,
|
||||
attendees,
|
||||
alarm,
|
||||
busy,
|
||||
eventClass,
|
||||
timezone,
|
||||
calendarid,
|
||||
hasVideoConference,
|
||||
meetingLink,
|
||||
showMore,
|
||||
showDescription,
|
||||
showRepeat,
|
||||
hasEndDateChanged,
|
||||
]);
|
||||
|
||||
// Check for temp data when modal opens
|
||||
useEffect(() => {
|
||||
if (open && !event?.uid) {
|
||||
// Only restore for new events (not duplicating)
|
||||
const tempData = restoreEventFormDataFromStorage("create");
|
||||
if (tempData && tempData.fromError) {
|
||||
// Mark that we're restoring from error to prevent other useEffects from overriding
|
||||
isRestoringFromErrorRef.current = true;
|
||||
// Prevent selectedRange useEffect from running
|
||||
shouldSyncFromRangeRef.current = false;
|
||||
|
||||
// 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("create", 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, event?.uid]);
|
||||
|
||||
const handleSave = async () => {
|
||||
// Show validation errors when Save is clicked
|
||||
setShowValidationErrors(true);
|
||||
@@ -668,22 +781,69 @@ function EventPopover({
|
||||
// Reset validation state when validation passes
|
||||
setShowValidationErrors(false);
|
||||
|
||||
// Save current form data to temp storage before closing
|
||||
const formDataToSave = saveCurrentFormData();
|
||||
saveEventFormDataToTemp("create", formDataToSave);
|
||||
|
||||
// Close popup immediately
|
||||
onClose(true);
|
||||
|
||||
// Reset all state to default values
|
||||
resetAllStateToDefault();
|
||||
|
||||
// Save to API in background
|
||||
await dispatch(
|
||||
putEventAsync({
|
||||
cal: targetCalendar,
|
||||
newEvent,
|
||||
})
|
||||
);
|
||||
if (tempList) {
|
||||
const calendarRange = getCalendarRange(new Date(newEvent.start));
|
||||
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
|
||||
try {
|
||||
const result = await dispatch(
|
||||
putEventAsync({
|
||||
cal: targetCalendar,
|
||||
newEvent,
|
||||
})
|
||||
);
|
||||
|
||||
// Handle result of putEventAsync - check if rejected first
|
||||
// Check if result is a rejected action
|
||||
if (result.type && result.type.endsWith("/rejected")) {
|
||||
throw new Error(
|
||||
result.error?.message || result.payload?.message || "API call failed"
|
||||
);
|
||||
}
|
||||
|
||||
// If result has unwrap, call it (it will throw if rejected)
|
||||
if (result && typeof result.unwrap === "function") {
|
||||
try {
|
||||
await result.unwrap();
|
||||
} catch (unwrapError: any) {
|
||||
throw unwrapError;
|
||||
}
|
||||
}
|
||||
|
||||
if (tempList) {
|
||||
const calendarRange = getCalendarRange(new Date(start));
|
||||
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
|
||||
}
|
||||
|
||||
// Clear temp data on successful save
|
||||
clearEventFormTempData("create");
|
||||
|
||||
// Reset all state to default values only on successful save
|
||||
resetAllStateToDefault();
|
||||
} catch (error: any) {
|
||||
// API failed - restore form data and mark as error
|
||||
const errorFormData = {
|
||||
...formDataToSave,
|
||||
fromError: true,
|
||||
};
|
||||
saveEventFormDataToTemp("create", errorFormData);
|
||||
|
||||
// Show error notification
|
||||
showErrorNotification(
|
||||
error?.message || "Failed to create event. Please try again."
|
||||
);
|
||||
|
||||
// Try to reopen modal by dispatching custom event
|
||||
// Parent component should listen to this event and reopen modal
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("eventModalError", {
|
||||
detail: { type: "create" },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
const dialogActions = (
|
||||
|
||||
@@ -31,10 +31,16 @@ import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtil
|
||||
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||
import { updateAttendeesAfterTimeChange } from "../../components/Calendar/handlers/eventHandlers";
|
||||
import { convertFormDateTimeToISO } from "../../components/Event/utils/dateTimeHelpers";
|
||||
|
||||
const showErrorNotification = (message: string) => {
|
||||
console.error(`[ERROR] ${message}`);
|
||||
};
|
||||
import {
|
||||
saveEventFormDataToTemp,
|
||||
restoreEventFormDataFromTemp as restoreEventFormDataFromStorage,
|
||||
clearEventFormTempData,
|
||||
showErrorNotification,
|
||||
buildEventFormTempData,
|
||||
restoreFormDataFromTemp,
|
||||
EventFormState,
|
||||
EventFormContext,
|
||||
} from "../../utils/eventFormTempStorage";
|
||||
|
||||
function EventUpdateModal({
|
||||
eventId,
|
||||
@@ -199,9 +205,15 @@ function EventUpdateModal({
|
||||
|
||||
// Prevent repeated initialization loops
|
||||
const initializedKeyRef = useRef<string | null>(null);
|
||||
// Track when restoring from error to prevent other useEffects from overriding restored data
|
||||
const isRestoringFromErrorRef = useRef(false);
|
||||
|
||||
// Initialize form state when event data is available
|
||||
useEffect(() => {
|
||||
// Skip if restoring from error - data already restored
|
||||
if (isRestoringFromErrorRef.current) {
|
||||
return;
|
||||
}
|
||||
if (event && open) {
|
||||
// Reset validation errors when modal opens
|
||||
setShowValidationErrors(false);
|
||||
@@ -322,6 +334,8 @@ function EventUpdateModal({
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Clear temp data when user manually closes modal
|
||||
clearEventFormTempData("update");
|
||||
closeModal();
|
||||
setShowValidationErrors(false);
|
||||
resetAllStateToDefault();
|
||||
@@ -329,6 +343,109 @@ function EventUpdateModal({
|
||||
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);
|
||||
@@ -338,7 +455,9 @@ function EventUpdateModal({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event) return;
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const organizer = event.organizer;
|
||||
|
||||
@@ -357,6 +476,20 @@ function EventUpdateModal({
|
||||
// Check if this is a recurring event
|
||||
const isRecurringEvent = !!event.repetition?.freq;
|
||||
|
||||
const getSeriesInstances = (): Record<string, CalendarEvent> => {
|
||||
const instances: Record<string, CalendarEvent> = {};
|
||||
const seriesEvents = targetCalendar.events || {};
|
||||
|
||||
Object.keys(seriesEvents).forEach((eventId) => {
|
||||
const instance = seriesEvents[eventId];
|
||||
if (instance && eventId.split("/")[0] === baseUID) {
|
||||
instances[eventId] = { ...instance };
|
||||
}
|
||||
});
|
||||
|
||||
return instances;
|
||||
};
|
||||
|
||||
// When editing "all events" of a recurring series, fetch master event to get original start time
|
||||
let masterEventData: CalendarEvent | null = null;
|
||||
if (isRecurringEvent && typeOfAction === "all") {
|
||||
@@ -369,8 +502,24 @@ function EventUpdateModal({
|
||||
const masterEvent = await getEvent(masterEventToFetch, true);
|
||||
masterEventData = masterEvent;
|
||||
} catch (err: any) {
|
||||
console.error("Failed to fetch master event:", err);
|
||||
showErrorNotification("Failed to fetch event data. Please try again.");
|
||||
// API failed - restore form data and mark as error
|
||||
const formDataToSave = saveCurrentFormData();
|
||||
const errorFormData = {
|
||||
...formDataToSave,
|
||||
fromError: true,
|
||||
};
|
||||
saveEventFormDataToTemp("update", errorFormData);
|
||||
|
||||
showErrorNotification(
|
||||
err?.message || "Failed to fetch event data. Please try again."
|
||||
);
|
||||
|
||||
// Dispatch eventModalError to reopen modal
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("eventModalError", {
|
||||
detail: { type: "update", eventId, calId, typeOfAction },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -469,9 +618,37 @@ function EventUpdateModal({
|
||||
) {
|
||||
const baseUID = event.uid.split("/")[0];
|
||||
|
||||
// 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
|
||||
@@ -530,198 +707,418 @@ function EventUpdateModal({
|
||||
|
||||
// STEP 4: Update Redux store - Add new event first to prevent empty grid
|
||||
dispatch(updateEventLocal({ calId, event: finalNewEvent }));
|
||||
|
||||
// STEP 5: Remove old recurring instances (swap is now instant)
|
||||
Object.keys(targetCalendar.events).forEach((eventId) => {
|
||||
if (eventId.split("/")[0] === baseUID) {
|
||||
dispatch(removeEvent({ calendarUid: calId, eventUid: eventId }));
|
||||
}
|
||||
});
|
||||
createdSingleEventUid = finalNewEvent.uid;
|
||||
|
||||
// Clear cache to ensure navigation to other weeks works
|
||||
dispatch(clearFetchCache(calId));
|
||||
} catch (err) {
|
||||
console.error("Failed to convert recurring to non-recurring:", err);
|
||||
// Keep modal open on error, user can retry or cancel
|
||||
}
|
||||
if (tempList) {
|
||||
const calendarRange = getCalendarRange(new Date(startDate));
|
||||
await updateTempCalendar(tempList, event, dispatch, calendarRange);
|
||||
|
||||
if (tempList) {
|
||||
const calendarRange = getCalendarRange(new Date(start));
|
||||
await updateTempCalendar(tempList, event, dispatch, calendarRange);
|
||||
}
|
||||
|
||||
// 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();
|
||||
setFreshEvent(null);
|
||||
initializedKeyRef.current = null;
|
||||
} catch (err: any) {
|
||||
// API failed - restore form data and mark as error
|
||||
const errorFormData = {
|
||||
...formDataToSave,
|
||||
fromError: true,
|
||||
};
|
||||
saveEventFormDataToTemp("update", errorFormData);
|
||||
|
||||
// Show error notification
|
||||
showErrorNotification(
|
||||
err?.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
|
||||
if (recurrenceId) {
|
||||
if (typeOfAction === "solo") {
|
||||
// Update single instance with optimistic update + rollback
|
||||
const oldEvent = { ...event };
|
||||
try {
|
||||
if (recurrenceId) {
|
||||
if (typeOfAction === "solo") {
|
||||
// Update single instance with optimistic update + rollback
|
||||
const oldEvent = { ...event };
|
||||
|
||||
dispatch(
|
||||
updateEventLocal({
|
||||
calId,
|
||||
event: { ...newEvent, recurrenceId },
|
||||
})
|
||||
);
|
||||
|
||||
await dispatch(
|
||||
updateEventInstanceAsync({
|
||||
cal: targetCalendar,
|
||||
event: { ...newEvent, recurrenceId },
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
.catch((error: any) => {
|
||||
dispatch(updateEventLocal({ calId, event: oldEvent }));
|
||||
showErrorNotification("Failed to update event. Changes reverted.");
|
||||
});
|
||||
} else if (typeOfAction === "all") {
|
||||
// Update all instances - check if repetition rules changed
|
||||
const baseUID = event.uid.split("/")[0];
|
||||
|
||||
const changes = detectRecurringEventChanges(
|
||||
event,
|
||||
{ repetition, timezone, allday, start, end },
|
||||
masterEventData,
|
||||
resolveTimezone,
|
||||
formatDateTimeInTimezone
|
||||
);
|
||||
const repetitionRulesChanged = changes.repetitionRulesChanged;
|
||||
|
||||
if (repetitionRulesChanged) {
|
||||
// Date/time or repetition rules changed - remove all overrides and refresh
|
||||
|
||||
// STEP 1: Remove ALL old instances from UI (including solo overrides)
|
||||
Object.keys(targetCalendar.events).forEach((eventId) => {
|
||||
if (eventId.split("/")[0] === baseUID) {
|
||||
dispatch(removeEvent({ calendarUid: calId, eventUid: eventId }));
|
||||
}
|
||||
});
|
||||
|
||||
// STEP 2: Update series on server with removeOverrides=true (await to ensure it completes)
|
||||
await dispatch(
|
||||
updateSeriesAsync({
|
||||
cal: targetCalendar,
|
||||
dispatch(
|
||||
updateEventLocal({
|
||||
calId,
|
||||
event: { ...newEvent, recurrenceId },
|
||||
removeOverrides: true,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// STEP 3: Fetch to get new instances with correct timing
|
||||
const calendarRange = getCalendarRange(new Date(startDate));
|
||||
await refreshCalendars(
|
||||
dispatch,
|
||||
Object.values(calendarsList),
|
||||
calendarRange
|
||||
);
|
||||
|
||||
// Clear cache after reload
|
||||
dispatch(clearFetchCache(calId));
|
||||
} else {
|
||||
// Only properties changed - use optimistic update and keep overrides
|
||||
|
||||
// Store old instances for rollback
|
||||
const oldInstances: Record<string, CalendarEvent> = {};
|
||||
Object.keys(targetCalendar.events)
|
||||
.filter((eventId) => eventId.split("/")[0] === baseUID)
|
||||
.forEach((eventId) => {
|
||||
oldInstances[eventId] = { ...targetCalendar.events[eventId] };
|
||||
});
|
||||
|
||||
// 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,
|
||||
},
|
||||
try {
|
||||
const result = await dispatch(
|
||||
updateEventInstanceAsync({
|
||||
cal: targetCalendar,
|
||||
event: { ...newEvent, recurrenceId },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Update server in background with removeOverrides=false
|
||||
await dispatch(
|
||||
updateSeriesAsync({
|
||||
cal: targetCalendar,
|
||||
event: { ...newEvent, recurrenceId },
|
||||
removeOverrides: false,
|
||||
})
|
||||
)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
// Clear cache to ensure navigation shows updated data
|
||||
dispatch(clearFetchCache(calId));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// Rollback: Restore old instances on error
|
||||
Object.values(oldInstances).forEach((oldEvent) => {
|
||||
dispatch(updateEventLocal({ calId, event: oldEvent }));
|
||||
// Handle result of updateEventInstanceAsync
|
||||
if (result && typeof (result as any).unwrap === "function") {
|
||||
try {
|
||||
await (result as any).unwrap();
|
||||
} catch (unwrapError: any) {
|
||||
throw unwrapError;
|
||||
}
|
||||
} else {
|
||||
// Check if result is rejected
|
||||
if (
|
||||
result.type &&
|
||||
(result.type as string).endsWith("/rejected")
|
||||
) {
|
||||
const rejectedResult = result as any;
|
||||
throw new Error(
|
||||
rejectedResult.error?.message ||
|
||||
rejectedResult.payload?.message ||
|
||||
"API call failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear temp data on successful save
|
||||
clearEventFormTempData("update");
|
||||
} catch (error: any) {
|
||||
// Rollback optimistic update
|
||||
dispatch(updateEventLocal({ calId, event: oldEvent }));
|
||||
throw error; // Re-throw to be caught by outer catch
|
||||
}
|
||||
} else if (typeOfAction === "all") {
|
||||
// Update all instances - check if repetition rules changed
|
||||
const baseUID = event.uid.split("/")[0];
|
||||
|
||||
const changes = detectRecurringEventChanges(
|
||||
event,
|
||||
{ repetition, timezone, allday, start, end },
|
||||
masterEventData,
|
||||
resolveTimezone,
|
||||
formatDateTimeInTimezone
|
||||
);
|
||||
const repetitionRulesChanged = changes.repetitionRulesChanged;
|
||||
|
||||
if (repetitionRulesChanged) {
|
||||
// Date/time or repetition rules changed - remove all overrides and refresh
|
||||
|
||||
const seriesInstancesSnapshot = getSeriesInstances();
|
||||
|
||||
const removeSeriesInstancesFromUI = () => {
|
||||
Object.keys(seriesInstancesSnapshot).forEach((eventId) => {
|
||||
dispatch(
|
||||
removeEvent({ calendarUid: calId, eventUid: eventId })
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
showErrorNotification(
|
||||
"Failed to update recurring event. Changes reverted."
|
||||
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 (await to ensure it completes)
|
||||
const result = await dispatch(
|
||||
updateSeriesAsync({
|
||||
cal: targetCalendar,
|
||||
event: { ...newEvent, recurrenceId },
|
||||
removeOverrides: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Handle result of updateSeriesAsync
|
||||
if (result && typeof (result as any).unwrap === "function") {
|
||||
try {
|
||||
await (result as any).unwrap();
|
||||
} catch (unwrapError: any) {
|
||||
throw unwrapError;
|
||||
}
|
||||
} else {
|
||||
// Check if result is rejected
|
||||
if (
|
||||
result.type &&
|
||||
(result.type as string).endsWith("/rejected")
|
||||
) {
|
||||
const rejectedResult = result as any;
|
||||
throw new Error(
|
||||
rejectedResult.error?.message ||
|
||||
rejectedResult.payload?.message ||
|
||||
"API call failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 3: Fetch to get new instances with correct timing
|
||||
// If refreshCalendars fails, we need to throw error to reopen modal
|
||||
try {
|
||||
const calendarRange = getCalendarRange(new Date(start));
|
||||
await refreshCalendars(
|
||||
dispatch,
|
||||
Object.values(calendarsList),
|
||||
calendarRange
|
||||
);
|
||||
} catch (refreshError: any) {
|
||||
// If refreshCalendars fails, throw error to reopen modal
|
||||
throw new Error(
|
||||
refreshError?.message ||
|
||||
"Failed to refresh calendar events. Please try again."
|
||||
);
|
||||
}
|
||||
|
||||
// Clear cache after reload
|
||||
dispatch(clearFetchCache(calId));
|
||||
|
||||
// Clear temp data on successful save
|
||||
clearEventFormTempData("update");
|
||||
} catch (seriesError) {
|
||||
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,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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
|
||||
await dispatch(putEventAsync({ cal: targetCalendar, newEvent }))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
// 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 })
|
||||
// Update server in background with removeOverrides=false
|
||||
const result = await dispatch(
|
||||
updateSeriesAsync({
|
||||
cal: targetCalendar,
|
||||
event: { ...newEvent, recurrenceId },
|
||||
removeOverrides: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Clear cache to ensure navigation to other weeks works
|
||||
dispatch(clearFetchCache(calId));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
showErrorNotification("Failed to create recurring event.");
|
||||
});
|
||||
} else {
|
||||
// Normal non-recurring event update
|
||||
await dispatch(putEventAsync({ cal: targetCalendar, newEvent }));
|
||||
}
|
||||
}
|
||||
// Handle result of updateSeriesAsync
|
||||
if (result && typeof (result as any).unwrap === "function") {
|
||||
try {
|
||||
await (result as any).unwrap();
|
||||
} catch (unwrapError: any) {
|
||||
throw unwrapError;
|
||||
}
|
||||
} else {
|
||||
// Check if result is rejected
|
||||
if (
|
||||
result.type &&
|
||||
(result.type as string).endsWith("/rejected")
|
||||
) {
|
||||
const rejectedResult = result as any;
|
||||
throw new Error(
|
||||
rejectedResult.error?.message ||
|
||||
rejectedResult.payload?.message ||
|
||||
"API call failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle calendar change
|
||||
if (newCalId !== calId) {
|
||||
await dispatch(
|
||||
moveEventAsync({
|
||||
cal: targetCalendar,
|
||||
newEvent,
|
||||
newURL: `/calendars/${newCalId}/${event.uid.split("/")[0]}.ics`,
|
||||
// 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
|
||||
if (result.type && result.type.endsWith("/rejected")) {
|
||||
throw new Error(
|
||||
result.error?.message ||
|
||||
result.payload?.message ||
|
||||
"API call failed"
|
||||
);
|
||||
}
|
||||
if (result && typeof result.unwrap === "function") {
|
||||
try {
|
||||
await result.unwrap();
|
||||
} catch (unwrapError: any) {
|
||||
throw unwrapError;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Normal non-recurring event update
|
||||
// If calendar is changing, we'll handle it separately with moveEventAsync
|
||||
// So only call putEventAsync if calendar is NOT changing
|
||||
if (newCalId === calId) {
|
||||
const result = await dispatch(
|
||||
putEventAsync({ cal: targetCalendar, newEvent })
|
||||
);
|
||||
|
||||
// Handle result of putEventAsync - check if rejected first
|
||||
if (result.type && result.type.endsWith("/rejected")) {
|
||||
throw new Error(
|
||||
result.error?.message ||
|
||||
result.payload?.message ||
|
||||
"API call failed"
|
||||
);
|
||||
}
|
||||
if (result && typeof result.unwrap === "function") {
|
||||
try {
|
||||
await result.unwrap();
|
||||
} catch (unwrapError: any) {
|
||||
throw unwrapError;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear temp data on successful save
|
||||
clearEventFormTempData("update");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle calendar change
|
||||
if (newCalId !== calId) {
|
||||
// Get the old calendar for updating
|
||||
const oldCalendar = calList[calId];
|
||||
if (!oldCalendar) {
|
||||
console.error("Old calendar not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// First update the event in the old calendar, then move it
|
||||
const putResult = await dispatch(
|
||||
putEventAsync({ cal: oldCalendar, newEvent: { ...newEvent, calId } })
|
||||
);
|
||||
|
||||
// Handle result of putEventAsync
|
||||
if (putResult && typeof putResult.unwrap === "function") {
|
||||
await putResult.unwrap();
|
||||
}
|
||||
|
||||
// Then move it to the new calendar
|
||||
const moveResult = await dispatch(
|
||||
moveEventAsync({
|
||||
cal: targetCalendar,
|
||||
newEvent,
|
||||
newURL: `/calendars/${newCalId}/${event.uid.split("/")[0]}.ics`,
|
||||
})
|
||||
);
|
||||
|
||||
// Handle result of moveEventAsync
|
||||
if (moveResult && typeof moveResult.unwrap === "function") {
|
||||
await moveResult.unwrap();
|
||||
}
|
||||
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
|
||||
|
||||
// Clear temp data on successful move
|
||||
clearEventFormTempData("update");
|
||||
}
|
||||
if (tempList) {
|
||||
const calendarRange = getCalendarRange(new Date(start));
|
||||
await updateTempCalendar(tempList, event, dispatch, calendarRange);
|
||||
}
|
||||
|
||||
// Reset all state to default values only on successful save (after all branches)
|
||||
clearEventFormTempData("update");
|
||||
resetAllStateToDefault();
|
||||
setFreshEvent(null);
|
||||
initializedKeyRef.current = null;
|
||||
} catch (error: any) {
|
||||
// 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(
|
||||
error?.message || "Failed to update event. Please try again."
|
||||
);
|
||||
|
||||
// Try to reopen modal
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("eventModalError", {
|
||||
detail: { type: "update", eventId, calId, typeOfAction },
|
||||
})
|
||||
);
|
||||
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
|
||||
}
|
||||
if (tempList) {
|
||||
const calendarRange = getCalendarRange(new Date(startDate));
|
||||
await updateTempCalendar(tempList, event, dispatch, calendarRange);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user