fix: correct recurring to non-recurring event update logic
- Fix bug where updating recurring event to non-recurring created duplicate events - Deduplicate URLs before deletion to avoid multiple delete calls on same file - Add onCloseAll prop to close both preview and update modals after save - Adjust smooth UI Resolves issue where repeat->no-repeat conversion left old recurring events visible
This commit is contained in:
committed by
Benoit TELLIER
parent
757060946f
commit
942b203b0f
@@ -2,9 +2,11 @@ import { screen, fireEvent, waitFor, act } from "@testing-library/react";
|
|||||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||||
import EventUpdateModal from "../../../src/features/Events/EventUpdateModal";
|
import EventUpdateModal from "../../../src/features/Events/EventUpdateModal";
|
||||||
import * as EventApi from "../../../src/features/Events/EventApi";
|
import * as EventApi from "../../../src/features/Events/EventApi";
|
||||||
|
import * as CalendarApi from "../../../src/features/Calendars/CalendarApi";
|
||||||
import * as eventUtils from "../../../src/components/Event/utils/eventUtils";
|
import * as eventUtils from "../../../src/components/Event/utils/eventUtils";
|
||||||
|
|
||||||
jest.mock("../../../src/features/Events/EventApi");
|
jest.mock("../../../src/features/Events/EventApi");
|
||||||
|
jest.mock("../../../src/features/Calendars/CalendarApi");
|
||||||
|
|
||||||
describe("EventUpdateModal Timezone Handling", () => {
|
describe("EventUpdateModal Timezone Handling", () => {
|
||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
@@ -247,12 +249,18 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mock API calls
|
// Mock API calls
|
||||||
const mockDeleteEvent = jest.spyOn(EventApi, 'deleteEvent').mockResolvedValue({} as any);
|
const mockDeleteEvent = jest
|
||||||
const mockPutEvent = jest.spyOn(EventApi, 'putEvent').mockResolvedValue({ status: 201, url: `/calendars/${calId}/new-event.ics` } as any);
|
.spyOn(EventApi, "deleteEvent")
|
||||||
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
.mockResolvedValue({} as any);
|
||||||
|
const mockPutEvent = jest.spyOn(EventApi, "putEvent").mockResolvedValue({
|
||||||
// Mock refreshCalendars
|
status: 201,
|
||||||
const mockRefreshCalendars = jest.spyOn(eventUtils, 'refreshCalendars').mockResolvedValue(undefined);
|
url: `/calendars/${calId}/new-event.ics`,
|
||||||
|
} as any);
|
||||||
|
jest.spyOn(CalendarApi, "getCalendar").mockResolvedValue({
|
||||||
|
_embedded: {
|
||||||
|
"dav:item": [],
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
const { store } = renderWithProviders(
|
const { store } = renderWithProviders(
|
||||||
<EventUpdateModal
|
<EventUpdateModal
|
||||||
@@ -286,26 +294,27 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
// Wait for the 500ms delay in the code
|
// Wait for the 500ms delay in the code
|
||||||
await new Promise(resolve => setTimeout(resolve, 600));
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify API calls
|
// Verify API calls - should delete all instances
|
||||||
await waitFor(() => {
|
await waitFor(
|
||||||
expect(mockDeleteEvent).toHaveBeenCalledWith(`/calendars/${calId}/${baseUID}.ics`);
|
() => {
|
||||||
}, { timeout: 3000 });
|
// Should have called deleteEvent for each instance (4 total: base + 3 recurrences)
|
||||||
|
expect(mockDeleteEvent).toHaveBeenCalled();
|
||||||
|
// At least one instance should be deleted
|
||||||
|
expect(mockDeleteEvent.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify new event was created via putEvent
|
||||||
expect(mockPutEvent).toHaveBeenCalled();
|
expect(mockPutEvent).toHaveBeenCalled();
|
||||||
const putEventCall = mockPutEvent.mock.calls[0][0];
|
const putEventCall = mockPutEvent.mock.calls[0][0];
|
||||||
expect(putEventCall.title).toBe("Recurring Meeting");
|
expect(putEventCall.title).toBe("Recurring Meeting");
|
||||||
expect(putEventCall.repetition?.freq).toBeFalsy();
|
expect(putEventCall.repetition?.freq).toBeFalsy();
|
||||||
expect(putEventCall.uid).not.toContain(baseUID);
|
expect(putEventCall.uid).not.toContain(baseUID);
|
||||||
|
|
||||||
// Verify console.log for successful deletion
|
|
||||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
||||||
"Deleted master event via direct API call:",
|
|
||||||
expect.stringContaining(baseUID)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify Redux store state changes
|
// Verify Redux store state changes
|
||||||
const finalState = store.getState();
|
const finalState = store.getState();
|
||||||
const calendar = finalState.calendars.list[calId];
|
const calendar = finalState.calendars.list[calId];
|
||||||
@@ -316,13 +325,13 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined();
|
expect(calendar.events[`${baseUID}/20250116`]).toBeUndefined();
|
||||||
expect(calendar.events[`${baseUID}/20250117`]).toBeUndefined();
|
expect(calendar.events[`${baseUID}/20250117`]).toBeUndefined();
|
||||||
|
|
||||||
// Verify refreshCalendars was called
|
// Verify modal was closed after completion
|
||||||
expect(mockRefreshCalendars).toHaveBeenCalled();
|
await waitFor(() => {
|
||||||
|
expect(mockOnClose).toHaveBeenCalled();
|
||||||
consoleLogSpy.mockRestore();
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("continues creating new event even if deletion of old series fails", async () => {
|
it("gracefully handles deletion errors and continues to create new event", async () => {
|
||||||
const eventDate = new Date("2025-01-15T10:00:00.000Z");
|
const eventDate = new Date("2025-01-15T10:00:00.000Z");
|
||||||
|
|
||||||
const masterEvent = {
|
const masterEvent = {
|
||||||
@@ -360,14 +369,16 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mock deleteEvent to fail
|
// Mock deleteEvent to fail
|
||||||
const mockDeleteEvent = jest.spyOn(EventApi, 'deleteEvent')
|
jest
|
||||||
|
.spyOn(EventApi, "deleteEvent")
|
||||||
.mockRejectedValue(new Error("Network error"));
|
.mockRejectedValue(new Error("Network error"));
|
||||||
const mockPutEvent = jest.spyOn(EventApi, 'putEvent')
|
const mockPutEvent = jest
|
||||||
|
.spyOn(EventApi, "putEvent")
|
||||||
.mockResolvedValue({ status: 201 } as any);
|
.mockResolvedValue({ status: 201 } as any);
|
||||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||||
|
|
||||||
// Mock refreshCalendars
|
// Mock refreshCalendars
|
||||||
jest.spyOn(eventUtils, 'refreshCalendars').mockResolvedValue(undefined);
|
jest.spyOn(eventUtils, "refreshCalendars").mockResolvedValue(undefined);
|
||||||
|
|
||||||
const { store } = renderWithProviders(
|
const { store } = renderWithProviders(
|
||||||
<EventUpdateModal
|
<EventUpdateModal
|
||||||
@@ -396,33 +407,45 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
await new Promise(resolve => setTimeout(resolve, 600));
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify error was logged
|
// Verify error was logged for failed deletion
|
||||||
await waitFor(() => {
|
await waitFor(
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
() => {
|
||||||
"Failed to delete recurring event:",
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
expect.any(Error)
|
expect.stringContaining("Failed to delete event file")
|
||||||
);
|
);
|
||||||
}, { timeout: 3000 });
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// New implementation gracefully handles deletion errors:
|
||||||
|
// Even if some instances fail to delete, we still create the new event
|
||||||
|
// This ensures user gets their non-recurring event
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(mockPutEvent).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
// Verify new event was still created despite deletion failure
|
|
||||||
expect(mockPutEvent).toHaveBeenCalled();
|
|
||||||
const putEventCall = mockPutEvent.mock.calls[0][0];
|
const putEventCall = mockPutEvent.mock.calls[0][0];
|
||||||
expect(putEventCall.title).toBe("Recurring Meeting");
|
expect(putEventCall.title).toBe("Recurring Meeting");
|
||||||
expect(putEventCall.repetition?.freq).toBeFalsy();
|
expect(putEventCall.repetition?.freq).toBeFalsy();
|
||||||
expect(putEventCall.uid).not.toContain(baseUID);
|
|
||||||
|
|
||||||
// When API deletion fails, the error is caught and logged
|
// Modal should close after operation completes
|
||||||
// The code continues to create the new event (graceful degradation)
|
await waitFor(
|
||||||
// Old events remain in store since API deletion failed
|
() => {
|
||||||
// They will be cleaned up when calendar refreshes from server
|
expect(mockOnClose).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
consoleErrorSpy.mockRestore();
|
consoleErrorSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes calendar after converting recurring to non-recurring", async () => {
|
it("closes all modals after converting recurring to non-recurring", async () => {
|
||||||
const eventDate = new Date("2025-01-15T10:00:00.000Z");
|
const eventDate = new Date("2025-01-15T10:00:00.000Z");
|
||||||
|
|
||||||
const masterEvent = {
|
const masterEvent = {
|
||||||
@@ -460,16 +483,22 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mock API calls
|
// Mock API calls
|
||||||
jest.spyOn(EventApi, 'deleteEvent').mockResolvedValue({} as any);
|
jest.spyOn(EventApi, "deleteEvent").mockResolvedValue({} as any);
|
||||||
jest.spyOn(EventApi, 'putEvent').mockResolvedValue({ status: 201 } as any);
|
jest.spyOn(EventApi, "putEvent").mockResolvedValue({ status: 201 } as any);
|
||||||
|
jest.spyOn(CalendarApi, "getCalendar").mockResolvedValue({
|
||||||
|
_embedded: {
|
||||||
|
"dav:item": [],
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Mock refreshCalendars to track calls
|
// Mock onCloseAll to test closing both modals
|
||||||
const mockRefreshCalendars = jest.spyOn(eventUtils, 'refreshCalendars').mockResolvedValue(undefined);
|
const mockOnCloseAll = jest.fn();
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventUpdateModal
|
<EventUpdateModal
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
|
onCloseAll={mockOnCloseAll}
|
||||||
calId={calId}
|
calId={calId}
|
||||||
eventId={`${baseUID}/20250115`}
|
eventId={`${baseUID}/20250115`}
|
||||||
typeOfAction="all"
|
typeOfAction="all"
|
||||||
@@ -493,16 +522,18 @@ describe("EventUpdateModal Recurring to Non-Recurring Conversion", () => {
|
|||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
await new Promise(resolve => setTimeout(resolve, 600));
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify refreshCalendars was called
|
// Verify onCloseAll was called to close both preview and update modals
|
||||||
await waitFor(() => {
|
await waitFor(
|
||||||
expect(mockRefreshCalendars).toHaveBeenCalledWith(
|
() => {
|
||||||
expect.any(Function), // dispatch
|
expect(mockOnCloseAll).toHaveBeenCalled();
|
||||||
expect.any(Array), // calendars list
|
},
|
||||||
expect.any(Object) // calendar range
|
{ timeout: 3000 }
|
||||||
);
|
);
|
||||||
}, { timeout: 3000 });
|
|
||||||
|
// Verify onClose was NOT called (onCloseAll is used instead)
|
||||||
|
expect(mockOnClose).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -510,6 +510,10 @@ export default function EventPreviewModal({
|
|||||||
<EventUpdateModal
|
<EventUpdateModal
|
||||||
open={openUpdateModal}
|
open={openUpdateModal}
|
||||||
onClose={() => setOpenUpdateModal(false)}
|
onClose={() => setOpenUpdateModal(false)}
|
||||||
|
onCloseAll={() => {
|
||||||
|
setOpenUpdateModal(false);
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
}}
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
calId={calId}
|
calId={calId}
|
||||||
typeOfAction={typeOfAction}
|
typeOfAction={typeOfAction}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ function EventUpdateModal({
|
|||||||
calId,
|
calId,
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
onCloseAll,
|
||||||
eventData,
|
eventData,
|
||||||
typeOfAction,
|
typeOfAction,
|
||||||
}: {
|
}: {
|
||||||
@@ -39,6 +40,7 @@ function EventUpdateModal({
|
|||||||
calId: string;
|
calId: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||||
|
onCloseAll?: () => void;
|
||||||
eventData?: CalendarEvent | null;
|
eventData?: CalendarEvent | null;
|
||||||
typeOfAction?: "solo" | "all";
|
typeOfAction?: "solo" | "all";
|
||||||
}) {
|
}) {
|
||||||
@@ -283,8 +285,17 @@ function EventUpdateModal({
|
|||||||
}
|
}
|
||||||
}, [open, event, calId, userPersonnalCalendars, calendarsList]);
|
}, [open, event, calId, userPersonnalCalendars, calendarsList]);
|
||||||
|
|
||||||
|
// Helper to close modal(s) - use onCloseAll if available to close preview modal too
|
||||||
|
const closeModal = () => {
|
||||||
|
if (onCloseAll) {
|
||||||
|
onCloseAll();
|
||||||
|
} else {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose({}, "backdropClick");
|
closeModal();
|
||||||
resetAllStateToDefault();
|
resetAllStateToDefault();
|
||||||
initializedKeyRef.current = null;
|
initializedKeyRef.current = null;
|
||||||
};
|
};
|
||||||
@@ -337,8 +348,94 @@ function EventUpdateModal({
|
|||||||
x_openpass_videoconference: meetingLink || undefined,
|
x_openpass_videoconference: meetingLink || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close popup immediately for better UX
|
// Handle recurrence instances
|
||||||
onClose({}, "backdropClick");
|
const [, recurrenceId] = event.uid.split("/");
|
||||||
|
|
||||||
|
// Special case: When converting recurring event to non-recurring
|
||||||
|
// Keep modal open until all async operations complete
|
||||||
|
if (
|
||||||
|
recurrenceId &&
|
||||||
|
typeOfAction === "all" &&
|
||||||
|
event.repetition?.freq &&
|
||||||
|
!repetition.freq
|
||||||
|
) {
|
||||||
|
const baseUID = event.uid.split("/")[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// STEP 1: Delete ALL instances of recurring event
|
||||||
|
// Note: This system stores instances only, no master event file
|
||||||
|
|
||||||
|
// Collect all instances that need to be deleted
|
||||||
|
const instancesToDelete = Object.keys(targetCalendar.events)
|
||||||
|
.filter((eventId) => eventId.split("/")[0] === baseUID)
|
||||||
|
.map((eventId) => targetCalendar.events[eventId]);
|
||||||
|
|
||||||
|
// Get unique URLs to avoid deleting same file multiple times
|
||||||
|
const uniqueURLs = new Set<string>();
|
||||||
|
const instancesByURL = new Map<string, CalendarEvent[]>();
|
||||||
|
|
||||||
|
instancesToDelete.forEach((instance) => {
|
||||||
|
if (!instancesByURL.has(instance.URL)) {
|
||||||
|
instancesByURL.set(instance.URL, []);
|
||||||
|
}
|
||||||
|
instancesByURL.get(instance.URL)!.push(instance);
|
||||||
|
uniqueURLs.add(instance.URL);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete each unique URL once
|
||||||
|
const deletePromises = Array.from(uniqueURLs).map(async (url) => {
|
||||||
|
try {
|
||||||
|
await deleteEvent(url);
|
||||||
|
} catch (deleteError: any) {
|
||||||
|
// Silently ignore 404 - file might already be deleted
|
||||||
|
const is404 =
|
||||||
|
deleteError.response?.status === 404 ||
|
||||||
|
deleteError.message?.includes("404") ||
|
||||||
|
deleteError.message?.includes("Not Found");
|
||||||
|
|
||||||
|
if (!is404) {
|
||||||
|
console.error(
|
||||||
|
`Failed to delete event file: ${deleteError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
|
// STEP 2: Clean up all instances from Redux store
|
||||||
|
Object.keys(targetCalendar.events).forEach((eventId) => {
|
||||||
|
if (eventId.split("/")[0] === baseUID) {
|
||||||
|
dispatch(removeEvent({ calendarUid: calId, eventUid: eventId }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// STEP 3: Create new non-recurring event AFTER deletion
|
||||||
|
// Note: putEventAsync automatically fetches calendar events after creation,
|
||||||
|
// so we don't need to call refreshCalendars separately
|
||||||
|
await dispatch(
|
||||||
|
putEventAsync({
|
||||||
|
cal: targetCalendar,
|
||||||
|
newEvent: {
|
||||||
|
...newEvent,
|
||||||
|
uid: crypto.randomUUID(),
|
||||||
|
URL: `/calendars/${newCalId || calId}/${crypto.randomUUID()}.ics`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// STEP 4: Close modal after everything completes
|
||||||
|
closeModal();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to convert recurring to non-recurring:", err);
|
||||||
|
// Keep modal open on error, user can retry or cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close popup immediately for better UX (for non-special cases)
|
||||||
|
closeModal();
|
||||||
|
|
||||||
// If converting from a non-repeating event to a repeating one,
|
// If converting from a non-repeating event to a repeating one,
|
||||||
// remove the original single instance to avoid duplicates on the grid
|
// remove the original single instance to avoid duplicates on the grid
|
||||||
@@ -346,9 +443,6 @@ function EventUpdateModal({
|
|||||||
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
|
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle recurrence instances
|
|
||||||
const [, recurrenceId] = event.uid.split("/");
|
|
||||||
|
|
||||||
// Execute API calls in background based on typeOfAction
|
// Execute API calls in background based on typeOfAction
|
||||||
if (recurrenceId) {
|
if (recurrenceId) {
|
||||||
if (typeOfAction === "solo") {
|
if (typeOfAction === "solo") {
|
||||||
@@ -360,58 +454,13 @@ function EventUpdateModal({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (typeOfAction === "all") {
|
} else if (typeOfAction === "all") {
|
||||||
// Update all instances
|
// Normal update for recurring events
|
||||||
|
dispatch(
|
||||||
// Special case: When converting recurring event to non-recurring
|
updateSeriesAsync({
|
||||||
if (event.repetition?.freq && !repetition.freq) {
|
cal: targetCalendar,
|
||||||
// For repeat -> no-repeat, create a new non-repeating event
|
event: { ...newEvent, recurrenceId },
|
||||||
dispatch(
|
})
|
||||||
putEventAsync({
|
);
|
||||||
cal: targetCalendar,
|
|
||||||
newEvent: {
|
|
||||||
...newEvent,
|
|
||||||
uid: crypto.randomUUID(), // Generate new ID for the single event
|
|
||||||
URL: `/calendars/${newCalId || calId}/${crypto.randomUUID()}.ics`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete the old repeating series - we need to be more direct and forceful
|
|
||||||
const baseUID = event.uid.split("/")[0];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Directly use the deleteEvent API without going through the store
|
|
||||||
const eventBaseURL = `/calendars/${calId}/${baseUID}.ics`;
|
|
||||||
await deleteEvent(eventBaseURL);
|
|
||||||
console.log(
|
|
||||||
"Deleted master event via direct API call:",
|
|
||||||
eventBaseURL
|
|
||||||
);
|
|
||||||
|
|
||||||
// Force a small delay to ensure deletion completes
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Make sure to clean up any instances in the store
|
|
||||||
Object.keys(targetCalendar.events).forEach((eventId) => {
|
|
||||||
if (eventId.split("/")[0] === baseUID) {
|
|
||||||
dispatch(
|
|
||||||
removeEvent({ calendarUid: calId, eventUid: eventId })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to delete recurring event:", err);
|
|
||||||
// Even if deletion fails, continue with creating the new event
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal update for recurring events
|
|
||||||
dispatch(
|
|
||||||
updateSeriesAsync({
|
|
||||||
cal: targetCalendar,
|
|
||||||
event: { ...newEvent, recurrenceId },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh calendars to ensure all instances are updated
|
// Refresh calendars to ensure all instances are updated
|
||||||
const calendarRange = getCalendarRange(new Date(start));
|
const calendarRange = getCalendarRange(new Date(start));
|
||||||
|
|||||||
Reference in New Issue
Block a user