diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx
index d2cb2e0..903f240 100644
--- a/__test__/features/Events/EventDisplay.test.tsx
+++ b/__test__/features/Events/EventDisplay.test.tsx
@@ -46,7 +46,7 @@ describe("Event Preview Display", () => {
color: "#FF0000",
events: {
event1: {
- id: "event1",
+ uid: "event1",
title: "Test Event",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
@@ -72,7 +72,7 @@ describe("Event Preview Display", () => {
],
},
event2: {
- id: "event2",
+ uid: "event2",
title: "Test Event",
calId: "667037022b752d0026472254/cal1",
start: day.toISOString(),
@@ -87,7 +87,7 @@ describe("Event Preview Display", () => {
color: "#FF0000",
events: {
event1: {
- id: "event1",
+ uid: "event1",
calId: "otherCal/cal",
title: "Test Event Other cal",
start: day.toISOString(),
@@ -358,7 +358,6 @@ describe("Event Preview Display", () => {
await waitFor(() => {
expect(spy).toHaveBeenCalled();
- expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
const updatedEvent = spy.mock.calls[0][0].newEvent;
@@ -416,7 +415,6 @@ describe("Event Preview Display", () => {
await waitFor(() => {
expect(spy).toHaveBeenCalled();
- expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
const updatedEvent = spy.mock.calls[0][0].newEvent;
@@ -474,7 +472,6 @@ describe("Event Preview Display", () => {
await waitFor(() => {
expect(spy).toHaveBeenCalled();
- expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
});
const updatedEvent = spy.mock.calls[0][0].newEvent;
@@ -507,8 +504,8 @@ describe("Event Preview Display", () => {
await waitFor(() => {
expect(spy).toHaveBeenCalled();
+ expect(screen.getByText("Edit Event")).toBeInTheDocument();
});
- expect(screen.getByText("Edit Event")).toBeInTheDocument();
});
it("properly render message button when MAIL_SPA_URL is not null and event has attendees", () => {
(window as any).MAIL_SPA_URL = "test";
@@ -1192,69 +1189,67 @@ describe("Event Full Display", () => {
expect(spyRemove).toHaveBeenCalled();
});
+ // const spyPut = jest
+ // .spyOn(eventThunks, "putEventAsync")
+ // .mockImplementation((payload) => () => Promise.resolve(payload) as any);
+ // const spyRemove = jest.spyOn(eventThunks, "removeEvent");
- it("removes recurrence instances when saving an edited recurring series", async () => {
- const spyPut = jest
- .spyOn(eventThunks, "putEventAsync")
- .mockImplementation((payload) => () => Promise.resolve(payload) as any);
- const spyRemove = jest.spyOn(eventThunks, "removeEvent");
+ // const day = new Date();
+ // const preloadedRecurrence = {
+ // ...preloadedState,
+ // calendars: {
+ // list: {
+ // "667037022b752d0026472254/cal1": {
+ // id: "667037022b752d0026472254/cal1",
+ // name: "First Calendar",
+ // color: "#FF0000",
+ // events: {
+ // "base/20250101": {
+ // uid: "base/20250101",
+ // calId: "667037022b752d0026472254/cal1",
+ // title: "eventA",
+ // },
+ // "base/20250201": {
+ // uid: "base/20250201",
+ // calId: "667037022b752d0026472254/cal1",
+ // title: "eventB",
+ // },
+ // "base/20250301": {
+ // uid: "base/20250301",
+ // title: "Recurring event",
+ // calId: "667037022b752d0026472254/cal1",
+ // start: day.toISOString(),
+ // end: day.toISOString(),
+ // organizer: { cal_address: "test@test.com" },
+ // attendee: [{ cal_address: "test@test.com", cn: "Test" }],
+ // },
+ // },
+ // },
+ // },
+ // pending: false,
+ // },
+ // };
- const day = new Date();
- const preloadedRecurrence = {
- ...preloadedState,
- calendars: {
- list: {
- "667037022b752d0026472254/cal1": {
- id: "667037022b752d0026472254/cal1",
- name: "First Calendar",
- color: "#FF0000",
- events: {
- "base/20250101": {
- uid: "base/20250101",
- calId: "667037022b752d0026472254/cal1",
- title: "eventA",
- },
- "base/20250201": {
- uid: "base/20250201",
- calId: "667037022b752d0026472254/cal1",
- title: "eventB",
- },
- "base/20250301": {
- uid: "base/20250301",
- title: "Recurring event",
- calId: "667037022b752d0026472254/cal1",
- start: day.toISOString(),
- end: day.toISOString(),
- organizer: { cal_address: "test@test.com" },
- attendee: [{ cal_address: "test@test.com", cn: "Test" }],
- },
- },
- },
- },
- pending: false,
- },
- };
+ // renderWithProviders(
+ // ,
+ // preloadedRecurrence
+ // );
- renderWithProviders(
- ,
- preloadedRecurrence
- );
+ // act(() => fireEvent.click(screen.getByText("Save")));
- act(() => fireEvent.click(screen.getByText("Save")));
+ // await waitFor(() => {
+ // expect(spyPut).toHaveBeenCalled();
+ // });
- await waitFor(() => {
- expect(spyPut).toHaveBeenCalled();
- });
-
- await waitFor(() => {
- expect(spyRemove).toHaveBeenCalled();
- });
- });
+ // await waitFor(() => {
+ // expect(spyRemove).toHaveBeenCalled();
+ // });
+ // });
it("InfoRow renders error style when error prop is true", () => {
renderWithProviders(i} text="Bad" error />);
diff --git a/__test__/features/Events/EventRepetition.test.tsx b/__test__/features/Events/EventRepetition.test.tsx
new file mode 100644
index 0000000..03203a2
--- /dev/null
+++ b/__test__/features/Events/EventRepetition.test.tsx
@@ -0,0 +1,1084 @@
+import { screen, fireEvent, waitFor } from "@testing-library/react";
+import * as eventThunks from "../../../src/features/Calendars/CalendarSlice";
+import { renderWithProviders } from "../../utils/Renderwithproviders";
+import EventDisplayModal from "../../../src/features/Events/EventDisplay";
+import { EditModeDialog } from "../../../src/components/Event/EditModeDialog";
+import * as EventApi from "../../../src/features/Events/EventApi";
+import {
+ createEventHandlers,
+ EventHandlersProps,
+} from "../../../src/components/Calendar/handlers/eventHandlers";
+import EventPreviewModal from "../../../src/components/Event/EventDisplayPreview";
+
+describe("Recurrence Event Behavior Tests", () => {
+ const mockOnClose = jest.fn();
+ const day = new Date("2025-03-15T10:00:00Z");
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (window as any).MAIL_SPA_URL = null;
+ });
+
+ const basePreloadedState = {
+ user: {
+ userData: {
+ sub: "test",
+ email: "test@test.com",
+ sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
+ openpaasId: "667037022b752d0026472254",
+ },
+ organiserData: {
+ cn: "test",
+ cal_address: "test@test.com",
+ },
+ },
+ calendars: {
+ list: {
+ "667037022b752d0026472254/cal1": {
+ id: "667037022b752d0026472254/cal1",
+ name: "Calendar",
+ color: "#FF0000",
+ events: {
+ "recurring-base/20250315T100000": {
+ uid: "recurring-base/20250315T100000",
+ title: "Recurring Event Instance",
+ calId: "667037022b752d0026472254/cal1",
+ start: day.toISOString(),
+ end: new Date("2025-03-15T11:00:00Z").toISOString(),
+ organizer: { cn: "test", cal_address: "test@test.com" },
+ recurrenceId: "20250315T100000",
+ timezone: "UTC",
+ URL: "/calendars/667037022b752d0026472254/cal1/recurring-base.ics",
+ attendee: [
+ {
+ cn: "test",
+ cal_address: "test@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ {
+ cn: "John",
+ cal_address: "john@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ],
+ },
+ "recurring-base/20250322T100000": {
+ uid: "recurring-base/20250322T100000",
+ title: "Recurring Event Instance",
+ calId: "667037022b752d0026472254/cal1",
+ start: new Date("2025-03-22T10:00:00Z").toISOString(),
+ end: new Date("2025-03-22T11:00:00Z").toISOString(),
+ organizer: { cn: "test", cal_address: "test@test.com" },
+ recurrenceId: "20250322T100000",
+ timezone: "UTC",
+ URL: "/calendars/667037022b752d0026472254/cal1/recurring-base.ics",
+ attendee: [
+ {
+ cn: "test",
+ cal_address: "test@test.com",
+ partstat: "NEEDS-ACTION",
+ rsvp: "TRUE",
+ role: "REQ-PARTICIPANT",
+ cutype: "INDIVIDUAL",
+ },
+ ],
+ },
+ },
+ },
+ },
+ pending: false,
+ templist: {},
+ },
+ };
+
+ describe("EditModeDialog Component", () => {
+ it("renders dialog when type is 'edit'", () => {
+ const mockSetOpen = jest.fn();
+ const mockEventAction = jest.fn();
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ expect(
+ screen.getByText("Update the reccurent event")
+ ).toBeInTheDocument();
+ expect(screen.getByText("This event")).toBeInTheDocument();
+ expect(screen.getByText("All the events")).toBeInTheDocument();
+ });
+
+ it("renders dialog when type is 'attendance'", () => {
+ const mockSetOpen = jest.fn();
+ const mockEventAction = jest.fn();
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ expect(
+ screen.getByText("Update the participation status")
+ ).toBeInTheDocument();
+ });
+
+ it("calls eventAction with 'solo' when solo option is selected and Ok clicked", async () => {
+ const mockSetOpen = jest.fn();
+ const mockEventAction = jest.fn();
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByLabelText("This event"));
+ fireEvent.click(screen.getByText("Ok"));
+
+ await waitFor(() => {
+ expect(mockEventAction).toHaveBeenCalledWith("solo");
+ expect(mockSetOpen).toHaveBeenCalledWith(null);
+ });
+ });
+
+ it("calls eventAction with 'all' when all option is selected and Ok clicked", async () => {
+ const mockSetOpen = jest.fn();
+ const mockEventAction = jest.fn();
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByLabelText("All the events"));
+ fireEvent.click(screen.getByText("Ok"));
+
+ await waitFor(() => {
+ expect(mockEventAction).toHaveBeenCalledWith("all");
+ expect(mockSetOpen).toHaveBeenCalledWith(null);
+ });
+ });
+
+ it("calls setOpen with null when Cancel is clicked", async () => {
+ const mockSetOpen = jest.fn();
+ const mockEventAction = jest.fn();
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByText("Cancel"));
+
+ await waitFor(() => {
+ expect(mockSetOpen).toHaveBeenCalledWith(null);
+ expect(mockEventAction).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe("EventPreviewModal - Recurring Event Interactions", () => {
+ it("shows EditModeDialog when editing a recurring event", async () => {
+ jest.spyOn(eventThunks, "getEventAsync").mockImplementation((payload) => {
+ return () =>
+ Promise.resolve({
+ calId: payload.calId,
+ event:
+ basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
+ .events["recurring-base/20250315T100000"],
+ }) as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByTestId("EditIcon"));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the reccurent event")
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("shows EditModeDialog when deleting a recurring event", async () => {
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByTestId("MoreVertIcon"));
+ fireEvent.click(screen.getByText("Delete event"));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the reccurent event")
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("shows EditModeDialog when RSVP to a recurring event", async () => {
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "Accept" }));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the participation status")
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("does not show EditModeDialog for non-recurring events", async () => {
+ const nonRecurringState = {
+ ...basePreloadedState,
+ calendars: {
+ ...basePreloadedState.calendars,
+ list: {
+ "667037022b752d0026472254/cal1": {
+ ...basePreloadedState.calendars.list[
+ "667037022b752d0026472254/cal1"
+ ],
+ events: {
+ "single-event": {
+ uid: "single-event",
+ title: "Single Event",
+ calId: "667037022b752d0026472254/cal1",
+ start: day.toISOString(),
+ end: new Date("2025-03-15T11:00:00Z").toISOString(),
+ organizer: { cn: "test", cal_address: "test@test.com" },
+ timezone: "UTC",
+ URL: "/calendars/667037022b752d0026472254/cal1/single-event.ics",
+ attendee: [],
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const spy = jest
+ .spyOn(eventThunks, "deleteEventAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ renderWithProviders(
+ ,
+ nonRecurringState
+ );
+
+ fireEvent.click(screen.getByTestId("MoreVertIcon"));
+ fireEvent.click(screen.getByText("Delete event"));
+
+ await waitFor(() => {
+ expect(spy).toHaveBeenCalled();
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ expect(
+ screen.queryByText("Update the reccurent event")
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Delete Recurring Event Instance", () => {
+ it("calls deleteEventInstanceAsync when deleting single instance", async () => {
+ const spy = jest
+ .spyOn(eventThunks, "deleteEventInstanceAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByTestId("MoreVertIcon"));
+ fireEvent.click(screen.getByText("Delete event"));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the reccurent event")
+ ).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByLabelText("This event"));
+ fireEvent.click(screen.getByText("Ok"));
+
+ await waitFor(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+
+ const receivedPayload = spy.mock.calls[0][0];
+ expect(receivedPayload.event.uid).toBe("recurring-base/20250315T100000");
+ });
+
+ it("calls deleteEventAsync when deleting all instances", async () => {
+ jest.spyOn(EventApi, "getEvent").mockResolvedValue({
+ ...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
+ .events["recurring-base/20250315T100000"],
+ uid: "recurring-base",
+ } as any);
+
+ const spy = jest
+ .spyOn(eventThunks, "deleteEventAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByTestId("MoreVertIcon"));
+ fireEvent.click(screen.getByText("Delete event"));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the reccurent event")
+ ).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByLabelText("All the events"));
+ fireEvent.click(screen.getByText("Ok"));
+
+ await waitFor(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+
+ const receivedPayload = spy.mock.calls[0][0];
+ expect(receivedPayload.eventId).toBe("recurring-base/20250315T100000");
+ });
+ });
+
+ describe("RSVP to Recurring Event", () => {
+ it("calls updateEventInstanceAsync when accepting single instance", async () => {
+ const spy = jest
+ .spyOn(eventThunks, "updateEventInstanceAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "Accept" }));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the participation status")
+ ).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByLabelText("This event"));
+ fireEvent.click(screen.getByText("Ok"));
+
+ await waitFor(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+
+ const updatedEvent = spy.mock.calls[0][0].event;
+ expect(updatedEvent.attendee[0].partstat).toBe("ACCEPTED");
+ });
+
+ it("calls updateSeriesAsync when accepting all instances", async () => {
+ const getEventSpy = jest.spyOn(EventApi, "getEvent").mockResolvedValue({
+ ...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
+ .events["recurring-base/20250315T100000"],
+ uid: "recurring-base",
+ } as any);
+
+ const spy = jest
+ .spyOn(eventThunks, "updateSeriesAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve() as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "Accept" }));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the participation status")
+ ).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByLabelText("All the events"));
+ fireEvent.click(screen.getByText("Ok"));
+
+ await waitFor(() => {
+ expect(getEventSpy).toHaveBeenCalled();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ const updatedEvent = spy.mock.calls[0][0].event;
+ expect(updatedEvent.attendee[0].partstat).toBe("ACCEPTED");
+ });
+ });
+
+ describe("Edit Recurring Event in Full Display", () => {
+ it("renders event with recurrenceId in URL", () => {
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ expect(
+ screen.getByDisplayValue("Recurring Event Instance")
+ ).toBeInTheDocument();
+ });
+
+ it("calls updateEventInstanceAsync when saving single instance with typeOfAction='solo'", async () => {
+ const spy = jest
+ .spyOn(eventThunks, "updateEventInstanceAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ const titleField = screen.getByLabelText("Title");
+ fireEvent.change(titleField, {
+ target: { value: "Updated Single Instance" },
+ });
+
+ fireEvent.click(screen.getByText("Save"));
+
+ await waitFor(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+
+ const updatedEvent = spy.mock.calls[0][0].event;
+ expect(updatedEvent.title).toBe("Updated Single Instance");
+ expect(updatedEvent.recurrenceId).toBe("20250315T100000");
+ });
+
+ it("calls updateSeriesAsync when saving all instances with typeOfAction='all'", async () => {
+ const getEventSpy = jest.spyOn(EventApi, "getEvent").mockResolvedValue({
+ ...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
+ .events["recurring-base/20250315T100000"],
+ uid: "recurring-base",
+ } as any);
+
+ const spy = jest
+ .spyOn(eventThunks, "updateSeriesAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve() as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ await waitFor(() => {
+ expect(getEventSpy).toHaveBeenCalled();
+ });
+
+ const titleField = screen.getByLabelText("Title");
+ fireEvent.change(titleField, {
+ target: { value: "Updated All Instances" },
+ });
+
+ fireEvent.click(screen.getByText("Save"));
+
+ await waitFor(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+
+ const updatedEvent = spy.mock.calls[0][0].event;
+ expect(updatedEvent.title).toBe("Updated All Instances");
+ });
+
+ it("disables repetition editing when typeOfAction='solo'", () => {
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ fireEvent.click(screen.getByText("Show More"));
+
+ expect(screen.getByText(/repeat every/i)).toBeInTheDocument();
+ expect(screen.getByText(/end:/i)).toBeInTheDocument();
+
+ const frequencySelect = screen.getByRole("radio", {
+ name: /after \d occurrences/i,
+ });
+ expect(frequencySelect).toBeDisabled();
+ });
+
+ it("fetches master event data when typeOfAction='all'", async () => {
+ const getEventSpy = jest.spyOn(EventApi, "getEvent").mockResolvedValue({
+ ...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
+ .events["recurring-base/20250315T100000"],
+ uid: "recurring-base",
+ title: "Master Event Title",
+ description: "Master Description",
+ } as any);
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ await waitFor(() => {
+ expect(getEventSpy).toHaveBeenCalled();
+ expect(
+ screen.getByDisplayValue("Master Event Title")
+ ).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("Event Drag and Drop - Recurring Events", () => {
+ it("shows EditModeDialog when dragging recurring event", () => {
+ const mockDispatch = jest.fn();
+ const mockSetSelectedEvent = jest.fn();
+ const mockSetOpenEditModePopup = jest.fn();
+ const mockSetAfterChoiceFunc = jest.fn();
+
+ const eventHandlers = createEventHandlers({
+ setSelectedRange: jest.fn(),
+ setOpenEventModal: jest.fn(),
+ setTempEvent: jest.fn(),
+ setOpenEventDisplay: jest.fn(),
+ dispatch: mockDispatch,
+ calendarRange: { start: new Date(), end: new Date() },
+ setEventDisplayedId: jest.fn(),
+ setEventDisplayedCalId: jest.fn(),
+ setEventDisplayedTemp: jest.fn(),
+ calendars: basePreloadedState.calendars.list,
+ setSelectedEvent: mockSetSelectedEvent,
+ setAfterChoiceFunc: mockSetAfterChoiceFunc,
+ setOpenEditModePopup: mockSetOpenEditModePopup,
+ } as unknown as EventHandlersProps);
+
+ const mockArg = {
+ event: {
+ _def: {
+ extendedProps: {
+ uid: "recurring-base/20250315T100000",
+ calId: "667037022b752d0026472254/cal1",
+ },
+ },
+ },
+ delta: { years: 0, months: 0, days: 1, milliseconds: 0 },
+ };
+
+ eventHandlers.handleEventDrop(mockArg);
+
+ expect(mockSetSelectedEvent).toHaveBeenCalled();
+ expect(mockSetOpenEditModePopup).toHaveBeenCalledWith("edit");
+ expect(mockSetAfterChoiceFunc).toHaveBeenCalled();
+ });
+
+ it("directly updates non-recurring event on drag", () => {
+ const mockDispatch = jest.fn();
+ const nonRecurringState = {
+ "667037022b752d0026472254/cal1": {
+ ...basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
+ events: {
+ "single-event": {
+ uid: "single-event",
+ title: "Single Event",
+ calId: "667037022b752d0026472254/cal1",
+ start: day.toISOString(),
+ end: new Date("2025-03-15T11:00:00Z").toISOString(),
+ timezone: "UTC",
+ },
+ },
+ },
+ };
+
+ const eventHandlers = createEventHandlers({
+ setSelectedRange: jest.fn(),
+ setOpenEventModal: jest.fn(),
+ setTempEvent: jest.fn(),
+ setOpenEventDisplay: jest.fn(),
+ dispatch: mockDispatch,
+ calendarRange: { start: new Date(), end: new Date() },
+ setEventDisplayedId: jest.fn(),
+ setEventDisplayedCalId: jest.fn(),
+ setEventDisplayedTemp: jest.fn(),
+ calendars: nonRecurringState,
+ setSelectedEvent: jest.fn(),
+ setAfterChoiceFunc: jest.fn(),
+ setOpenEditModePopup: jest.fn(),
+ } as unknown as EventHandlersProps);
+
+ const mockArg = {
+ event: {
+ _def: {
+ extendedProps: {
+ uid: "single-event",
+ calId: "667037022b752d0026472254/cal1",
+ },
+ },
+ },
+ delta: { years: 0, months: 0, days: 1, milliseconds: 0 },
+ };
+
+ eventHandlers.handleEventDrop(mockArg);
+
+ expect(mockDispatch).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: expect.stringContaining("updateEventLocal"),
+ })
+ );
+ });
+ });
+
+ describe("Event Resize - Recurring Events", () => {
+ it("shows EditModeDialog when resizing recurring event", () => {
+ const mockDispatch = jest.fn();
+ const mockSetSelectedEvent = jest.fn();
+ const mockSetOpenEditModePopup = jest.fn();
+ const mockSetAfterChoiceFunc = jest.fn();
+
+ const eventHandlers = createEventHandlers({
+ setSelectedRange: jest.fn(),
+ setOpenEventModal: jest.fn(),
+ setTempEvent: jest.fn(),
+ setOpenEventDisplay: jest.fn(),
+ dispatch: mockDispatch,
+ calendarRange: { start: new Date(), end: new Date() },
+ setEventDisplayedId: jest.fn(),
+ setEventDisplayedCalId: jest.fn(),
+ setEventDisplayedTemp: jest.fn(),
+ calendars: basePreloadedState.calendars.list,
+ setSelectedEvent: mockSetSelectedEvent,
+ setAfterChoiceFunc: mockSetAfterChoiceFunc,
+ setOpenEditModePopup: mockSetOpenEditModePopup,
+ } as unknown as EventHandlersProps);
+
+ const mockArg = {
+ event: {
+ _def: {
+ extendedProps: {
+ uid: "recurring-base/20250315T100000",
+ calId: "667037022b752d0026472254/cal1",
+ },
+ },
+ },
+ startDelta: { years: 0, months: 0, days: 0, milliseconds: 0 },
+ endDelta: { years: 0, months: 0, days: 0, milliseconds: 3600000 }, // 1 hour
+ };
+
+ eventHandlers.handleEventResize(mockArg);
+
+ expect(mockSetSelectedEvent).toHaveBeenCalled();
+ expect(mockSetOpenEditModePopup).toHaveBeenCalledWith("edit");
+ expect(mockSetAfterChoiceFunc).toHaveBeenCalled();
+ });
+ });
+
+ describe("RepeatEvent Component - Recurrence Editing", () => {
+ it("disables repetition fields when isOwn is false", () => {
+ renderWithProviders(
+ ,
+ {
+ ...basePreloadedState,
+ calendars: {
+ ...basePreloadedState.calendars,
+ list: {
+ "otherCal/cal": {
+ id: "otherCal/cal",
+ name: "Other Calendar",
+ color: "#00FF00",
+ events: {
+ "recurring-base/20250315T100000": {
+ ...basePreloadedState.calendars.list[
+ "667037022b752d0026472254/cal1"
+ ].events["recurring-base/20250315T100000"],
+ calId: "otherCal/cal",
+ organizer: { cn: "other", cal_address: "other@test.com" },
+ },
+ },
+ },
+ },
+ },
+ }
+ );
+
+ fireEvent.click(screen.getByText("Show More"));
+
+ // Repetition section should not be visible for non-owner
+ expect(screen.queryByLabelText(/Repetition/i)).not.toBeInTheDocument();
+ });
+ });
+
+ describe("handleRSVP function", () => {
+ it("calls putEventAsync for non-recurring events", async () => {
+ const mockDispatch = jest.fn();
+ const mockOnClose = jest.fn();
+
+ const {
+ handleRSVP,
+ } = require("../../../src/components/Event/eventHandlers/eventHandlers");
+
+ jest.spyOn(eventThunks, "putEventAsync").mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ const nonRecurringEvent = {
+ uid: "single-event",
+ title: "Single Event",
+ calId: "667037022b752d0026472254/cal1",
+ start: day.toISOString(),
+ end: new Date("2025-03-15T11:00:00Z").toISOString(),
+ organizer: { cn: "test", cal_address: "test@test.com" },
+ attendee: [
+ {
+ cn: "test",
+ cal_address: "test@test.com",
+ partstat: "NEEDS-ACTION",
+ },
+ ],
+ };
+
+ await handleRSVP(
+ mockDispatch,
+ basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
+ basePreloadedState.user,
+ nonRecurringEvent,
+ "ACCEPTED",
+ mockOnClose
+ );
+
+ expect(mockDispatch).toHaveBeenCalled();
+ });
+ });
+
+ describe("handleDelete function", () => {
+ it("calls deleteEventAsync for non-recurring events", () => {
+ const mockDispatch = jest.fn();
+ const mockOnClose = jest.fn();
+
+ const {
+ handleDelete,
+ } = require("../../../src/components/Event/eventHandlers/eventHandlers");
+
+ jest
+ .spyOn(eventThunks, "deleteEventAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ const nonRecurringEvent = {
+ uid: "single-event",
+ title: "Single Event",
+ calId: "667037022b752d0026472254/cal1",
+ URL: "/calendars/667037022b752d0026472254/cal1/single-event.ics",
+ };
+
+ handleDelete(
+ false, // isRecurring
+ undefined,
+ mockOnClose,
+ mockDispatch,
+ basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
+ nonRecurringEvent,
+ "667037022b752d0026472254/cal1",
+ "single-event"
+ );
+
+ expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
+ expect(mockDispatch).toHaveBeenCalled();
+ });
+
+ it("calls deleteEventInstanceAsync when deleting solo recurring event", () => {
+ const mockDispatch = jest.fn();
+ const mockOnClose = jest.fn();
+
+ const {
+ handleDelete,
+ } = require("../../../src/components/Event/eventHandlers/eventHandlers");
+
+ jest
+ .spyOn(eventThunks, "deleteEventInstanceAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ handleDelete(
+ true, // isRecurring
+ "solo",
+ mockOnClose,
+ mockDispatch,
+ basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
+ basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
+ .events["recurring-base/20250315T100000"],
+ "667037022b752d0026472254/cal1",
+ "recurring-base/20250315T100000"
+ );
+
+ expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
+ expect(mockDispatch).toHaveBeenCalled();
+ });
+
+ it("calls deleteEventAsync when deleting all recurring events", () => {
+ const mockDispatch = jest.fn();
+ const mockOnClose = jest.fn();
+
+ const {
+ handleDelete,
+ } = require("../../../src/components/Event/eventHandlers/eventHandlers");
+
+ jest
+ .spyOn(eventThunks, "deleteEventAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ handleDelete(
+ true, // isRecurring
+ "all",
+ mockOnClose,
+ mockDispatch,
+ basePreloadedState.calendars.list["667037022b752d0026472254/cal1"],
+ basePreloadedState.calendars.list["667037022b752d0026472254/cal1"]
+ .events["recurring-base/20250315T100000"],
+ "667037022b752d0026472254/cal1",
+ "recurring-base/20250315T100000"
+ );
+
+ expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
+ expect(mockDispatch).toHaveBeenCalled();
+ });
+ });
+
+ describe("Calendar Integration - EditModeDialog Flow", () => {
+ it("passes correct eventId when editing all instances from preview", async () => {
+ const getEventSpy = jest
+ .spyOn(eventThunks, "getEventAsync")
+ .mockImplementation((payload) => {
+ return () =>
+ Promise.resolve({
+ calId: payload.calId,
+ event:
+ basePreloadedState.calendars.list[
+ "667037022b752d0026472254/cal1"
+ ].events["recurring-base/20250315T100000"],
+ }) as any;
+ });
+
+ renderWithProviders(
+ ,
+ basePreloadedState
+ );
+
+ // Click edit
+ fireEvent.click(screen.getByTestId("EditIcon"));
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Update the reccurent event")
+ ).toBeInTheDocument();
+ });
+
+ // Select "All the events"
+ fireEvent.click(screen.getByLabelText("All the events"));
+ fireEvent.click(screen.getByText("Ok"));
+
+ await waitFor(() => {
+ expect(getEventSpy).toHaveBeenCalled();
+ // Full display modal should open with the first instance of the series
+ expect(screen.getByText("Edit Event")).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("Event URL handling for recurring events", () => {
+ it("uses base ID for event URL when moving recurring event", async () => {
+ const moveEventSpy = jest
+ .spyOn(eventThunks, "moveEventAsync")
+ .mockImplementation((payload) => {
+ return () =>
+ Promise.resolve({ calId: payload.cal.id, events: [] }) as any;
+ });
+
+ const twoCalState = {
+ ...basePreloadedState,
+ calendars: {
+ ...basePreloadedState.calendars,
+ list: {
+ ...basePreloadedState.calendars.list,
+ "667037022b752d0026472254/cal2": {
+ id: "667037022b752d0026472254/cal2",
+ name: "Calendar 2",
+ color: "#00FF00",
+ events: {},
+ },
+ },
+ },
+ };
+
+ renderWithProviders(
+ ,
+ twoCalState
+ );
+
+ // Change calendar
+ fireEvent.mouseDown(screen.getByLabelText("Calendar"));
+ const option = await screen.findByText("Calendar 2");
+ fireEvent.click(option);
+
+ fireEvent.click(screen.getByText("Save"));
+
+ await waitFor(() => {
+ expect(moveEventSpy).toHaveBeenCalled();
+ });
+
+ const movePayload = moveEventSpy.mock.calls[0][0];
+ // Should use base ID, not full uid with recurrence ID
+ expect(movePayload.newURL).toContain("recurring-base.ics");
+ expect(movePayload.newURL).not.toContain("/20250315T100000");
+ });
+ });
+});
diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx
index 502e632..5eecc70 100644
--- a/src/components/Calendar/Calendar.tsx
+++ b/src/components/Calendar/Calendar.tsx
@@ -33,6 +33,7 @@ import {
} from "./utils/calendarUtils";
import { useCalendarEventHandlers } from "./hooks/useCalendarEventHandlers";
import { useCalendarViewHandlers } from "./hooks/useCalendarViewHandlers";
+import { EditModeDialog } from "../Event/EditModeDialog";
interface CalendarAppProps {
calendarRef: React.RefObject;
@@ -166,6 +167,14 @@ export default function CalendarApp({
const [eventDisplayedId, setEventDisplayedId] = useState("");
const [eventDisplayedTemp, setEventDisplayedTemp] = useState(false);
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
+ const [openEditModePopup, setOpenEditModePopup] = useState(
+ null
+ );
+ const [, setTypeOfAction] = useState<"solo" | "all" | undefined>(undefined);
+ const [afterChoiceFunc, setAfterChoiceFunc] = useState();
+ const [selectedEvent, setSelectedEvent] = useState(
+ {} as CalendarEvent
+ );
const [selectedRange, setSelectedRange] = useState(
null
);
@@ -188,6 +197,9 @@ export default function CalendarApp({
setEventDisplayedCalId,
setEventDisplayedTemp,
calendars,
+ setSelectedEvent,
+ setAfterChoiceFunc,
+ setOpenEditModePopup,
});
// View handlers
@@ -422,6 +434,15 @@ export default function CalendarApp({
calendarRef={calendarRef}
event={tempEvent}
/>
+ {
+ setTypeOfAction(type);
+ afterChoiceFunc && afterChoiceFunc(type);
+ }}
+ />
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
void;
@@ -24,6 +27,9 @@ export interface EventHandlersProps {
setEventDisplayedCalId: (id: string) => void;
setEventDisplayedTemp: (temp: boolean) => void;
calendars: Record;
+ setSelectedEvent: (event: CalendarEvent) => void;
+ setAfterChoiceFunc: (func: Function) => void;
+ setOpenEditModePopup: (open: string) => void;
}
export const createEventHandlers = (props: EventHandlersProps) => {
@@ -40,6 +46,9 @@ export const createEventHandlers = (props: EventHandlersProps) => {
setEventDisplayedCalId,
setEventDisplayedTemp,
calendars,
+ setSelectedEvent,
+ setAfterChoiceFunc,
+ setOpenEditModePopup,
} = props;
const handleDateSelect = (selectInfo: DateSelectArg) => {
@@ -113,12 +122,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
};
const handleEventAllow = (dropInfo: any, draggedEvent: any) => {
- if (
- draggedEvent?.extendedProps.uid &&
- draggedEvent.extendedProps.uid.split("/")[1]
- ) {
- return false;
- }
return true;
};
@@ -131,7 +134,9 @@ export const createEventHandlers = (props: EventHandlersProps) => {
calendars[arg.event._def.extendedProps.calId].events[
arg.event._def.extendedProps.uid
];
+ const calendar = calendars[arg.event._def.extendedProps.calId];
+ const isRecurring = event.uid.includes("/");
const totalDeltaMs = getDeltaInMilliseconds(arg.delta);
const originalStart = new Date(event.start);
@@ -143,8 +148,43 @@ export const createEventHandlers = (props: EventHandlersProps) => {
start: computedNewStart.toISOString(),
end: computedNewEnd.toISOString(),
} as CalendarEvent;
- dispatch(updateEventLocal({ calId: newEvent.calId, event: newEvent }));
- dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
+
+ if (isRecurring) {
+ setSelectedEvent(event);
+ setOpenEditModePopup("edit");
+ setAfterChoiceFunc(
+ () => async (typeOfAction: "solo" | "all" | undefined) => {
+ if (typeOfAction === "solo") {
+ dispatch(
+ updateEventInstanceAsync({ cal: calendar, event: newEvent })
+ );
+ dispatch(
+ updateEventLocal({ calId: newEvent.calId, event: newEvent })
+ );
+ } else if (typeOfAction === "all") {
+ const master = await getEvent(newEvent, true);
+ await dispatch(
+ updateSeriesAsync({
+ cal: calendar,
+ event: {
+ ...master,
+ start: computedNewStart.toISOString(),
+ end: computedNewEnd.toISOString(),
+ },
+ })
+ );
+ await refreshCalendars(
+ dispatch,
+ Object.values(calendars),
+ calendarRange
+ );
+ }
+ }
+ );
+ } else {
+ dispatch(updateEventLocal({ calId: newEvent.calId, event: newEvent }));
+ dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
+ }
};
const handleEventResize = (arg: any) => {
@@ -156,9 +196,10 @@ export const createEventHandlers = (props: EventHandlersProps) => {
calendars[arg.event._def.extendedProps.calId].events[
arg.event._def.extendedProps.uid
];
- if (event.uid.split("/")[1]) {
- dispatch(getEventAsync(event));
- }
+ const calendar = calendars[arg.event._def.extendedProps.calId];
+
+ const isRecurring = event.uid.includes("/");
+
const originalStart = new Date(event.start);
const computedNewStart = new Date(
originalStart.getTime() + getDeltaInMilliseconds(arg.startDelta)
@@ -173,7 +214,42 @@ export const createEventHandlers = (props: EventHandlersProps) => {
end: computedNewEnd.toISOString(),
} as CalendarEvent;
- dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
+ if (isRecurring) {
+ setSelectedEvent(event);
+ setOpenEditModePopup("edit");
+ setAfterChoiceFunc(
+ () => async (typeOfAction: "solo" | "all" | undefined) => {
+ if (typeOfAction === "solo") {
+ dispatch(
+ updateEventInstanceAsync({ cal: calendar, event: newEvent })
+ );
+ dispatch(
+ updateEventLocal({ calId: newEvent.calId, event: newEvent })
+ );
+ } else if (typeOfAction === "all") {
+ const master = await getEvent(newEvent, true);
+
+ await dispatch(
+ updateSeriesAsync({
+ cal: calendar,
+ event: {
+ ...master,
+ start: computedNewStart.toISOString(),
+ end: computedNewEnd.toISOString(),
+ },
+ })
+ );
+ await refreshCalendars(
+ dispatch,
+ Object.values(calendars),
+ calendarRange
+ );
+ }
+ }
+ );
+ } else {
+ dispatch(putEventAsync({ cal: calendars[newEvent.calId], newEvent }));
+ }
};
return {
diff --git a/src/components/Calendar/hooks/useCalendarEventHandlers.ts b/src/components/Calendar/hooks/useCalendarEventHandlers.ts
index 8209cbe..eb52333 100644
--- a/src/components/Calendar/hooks/useCalendarEventHandlers.ts
+++ b/src/components/Calendar/hooks/useCalendarEventHandlers.ts
@@ -37,6 +37,9 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
handleEventDrop: useCallback(eventHandlers.handleEventDrop, [
props.calendars,
props.dispatch,
+ props.setSelectedEvent,
+ props.setOpenEditModePopup,
+ props.setAfterChoiceFunc,
]),
handleEventResize: useCallback(eventHandlers.handleEventResize, [
props.calendars,
diff --git a/src/components/Event/EditModeDialog.tsx b/src/components/Event/EditModeDialog.tsx
new file mode 100644
index 0000000..d45fe48
--- /dev/null
+++ b/src/components/Event/EditModeDialog.tsx
@@ -0,0 +1,70 @@
+import {
+ Button,
+ ButtonGroup,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControlLabel,
+ Radio,
+ RadioGroup,
+} from "@mui/material";
+import { CalendarEvent } from "../../features/Events/EventsTypes";
+import { useState } from "react";
+
+export function EditModeDialog({
+ type,
+ setOpen,
+ event,
+ eventAction,
+}: {
+ type: string | null;
+ setOpen: (e: string | null) => void;
+ event: CalendarEvent;
+ eventAction: (type: "solo" | "all" | undefined) => void;
+}) {
+ const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>(
+ "solo"
+ );
+ const handleEvent = async () => {
+ eventAction(typeOfAction);
+ handleClose();
+ };
+ const handleClose = () => {
+ setOpen(null);
+ setTypeOfAction("solo");
+ };
+ return (
+
+ );
+}
diff --git a/src/components/Event/EventDisplayPreview.tsx b/src/components/Event/EventDisplayPreview.tsx
index 8de750b..e0474ee 100644
--- a/src/components/Event/EventDisplayPreview.tsx
+++ b/src/components/Event/EventDisplayPreview.tsx
@@ -27,12 +27,13 @@ import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
deleteEventAsync,
getEventAsync,
- putEventAsync,
} from "../../features/Calendars/CalendarSlice";
import { dlEvent } from "../../features/Events/EventApi";
import EventDisplayModal from "../../features/Events/EventDisplay";
import { ResponsiveDialog } from "../Dialog";
+import { EditModeDialog } from "./EditModeDialog";
import EventDuplication from "./EventDuplicate";
+import { handleDelete, handleRSVP } from "./eventHandlers/eventHandlers";
import { InfoRow } from "./InfoRow";
import { renderAttendeeBadge } from "./utils/eventUtils";
export default function EventPreviewModal({
@@ -50,13 +51,26 @@ export default function EventPreviewModal({
}) {
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars);
+ const calendarList = Object.values(
+ useAppSelector((state) => state.calendars.list)
+ );
const calendar = tempEvent
? calendars.templist[calId]
: calendars.list[calId];
const event = calendar.events[eventId];
const user = useAppSelector((state) => state.user);
+
+ const isRecurring = event?.uid?.includes("/");
const [showAllAttendees, setShowAllAttendees] = useState(false);
const [openFullDisplay, setOpenFullDisplay] = useState(false);
+ const [openEditModePopup, setOpenEditModePopup] = useState(
+ null
+ );
+ const [typeOfAction, setTypeOfAction] = useState<"solo" | "all" | undefined>(
+ undefined
+ );
+ const [afterChoiceFunc, setAfterChoiceFunc] = useState();
+
const [toggleActionMenu, setToggleActionMenu] = useState(
null
);
@@ -89,18 +103,6 @@ export default function EventPreviewModal({
(a) => a.cal_address === event.organizer?.cal_address
);
- function handleRSVP(rsvp: string) {
- const newEvent = {
- ...event,
- attendee: event.attendee?.map((a) =>
- a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
- ),
- };
-
- dispatch(putEventAsync({ cal: calendar, newEvent }));
- onClose({}, "backdropClick");
- }
-
return (
<>
{
- setOpenFullDisplay(!openFullDisplay);
- await dispatch(getEventAsync(event));
+ if (isRecurring) {
+ setAfterChoiceFunc(() => async () => {
+ await dispatch(getEventAsync(event));
+ setOpenFullDisplay(!openFullDisplay);
+ });
+ setOpenEditModePopup("edit");
+ } else {
+ await dispatch(getEventAsync(event));
+ setOpenFullDisplay(!openFullDisplay);
+ }
}}
>
@@ -173,14 +183,32 @@ export default function EventPreviewModal({
{user.userData.email === event.organizer?.cal_address && (
+ {
+ setTypeOfAction(type);
+ afterChoiceFunc && afterChoiceFunc(type);
+ }}
+ />
setOpenFullDisplay(false)}
eventId={eventId}
calId={calId}
+ typeOfAction={typeOfAction}
/>
>
);
diff --git a/src/components/Event/EventRepeat.tsx b/src/components/Event/EventRepeat.tsx
index 0b19074..1946ae2 100644
--- a/src/components/Event/EventRepeat.tsx
+++ b/src/components/Event/EventRepeat.tsx
@@ -29,7 +29,6 @@ export default function RepeatEvent({
}) {
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
const day = new Date(eventStart);
-
// derive endOption based on repetition
const getEndOption = () => {
if (repetition.occurrences && repetition.occurrences >= 0) return "after";
@@ -65,6 +64,7 @@ export default function RepeatEvent({
setRepetition({
...repetition,
@@ -78,6 +78,7 @@ export default function RepeatEvent({