[#446] removed direct state update (#468)

This commit is contained in:
Camille Moussu
2026-01-21 15:17:49 +01:00
committed by GitHub
parent 23d50f250d
commit 00742119be
38 changed files with 119 additions and 1366 deletions
@@ -396,6 +396,22 @@ describe("CalendarSlice", () => {
it("createCalendarAsync.fulfilled adds a new calendar", () => { it("createCalendarAsync.fulfilled adds a new calendar", () => {
const payload = { const payload = {
userData: {
openpaasId: "u1",
family_name: "Owner",
email: "o@example.com",
sid: "",
sub: "",
given_name: "Test",
name: "Test Owner",
},
calId: "cal1",
color: { "apple:color": "#f00" },
name: "Test",
desc: "Desc",
};
const payloadResponse = {
userId: "u1", userId: "u1",
calId: "cal1", calId: "cal1",
color: { "apple:color": "#f00" }, color: { "apple:color": "#f00" },
@@ -406,7 +422,7 @@ describe("CalendarSlice", () => {
}; };
const state = reducer( const state = reducer(
initialState, initialState,
createCalendarAsync.fulfilled(payload, "req4", payload) createCalendarAsync.fulfilled(payloadResponse, "req4", payload)
); );
expect(state.list["u1/cal1"].name).toBe("Test"); expect(state.list["u1/cal1"].name).toBe("Test");
expect(state.list["u1/cal1"].color?.["apple:color"]).toBe("#f00"); expect(state.list["u1/cal1"].color?.["apple:color"]).toBe("#f00");
@@ -441,28 +457,6 @@ describe("CalendarSlice", () => {
expect(state.list["c1"].name).toBe("Shared"); expect(state.list["c1"].name).toBe("Shared");
}); });
it("deleteEventAsync.fulfilled removes event", () => {
const calId = "c1";
const prev = {
...initialState,
list: {
[calId]: {
id: calId,
events: { e1: { uid: "e1" } },
} as unknown as Calendar,
},
};
const state = reducer(
prev,
deleteEventAsync.fulfilled({ calId, eventId: "e1" }, "req6", {
calId,
eventId: "e1",
eventURL: "e1.ics",
})
);
expect(state.list[calId].events).toEqual({});
});
it("getTempCalendarsListAsync.fulfilled updates templist", () => { it("getTempCalendarsListAsync.fulfilled updates templist", () => {
const payload = { const payload = {
t1: { t1: {
@@ -490,19 +484,6 @@ describe("CalendarSlice", () => {
expect(state.templist.t1.name).toBe("Temp"); expect(state.templist.t1.name).toBe("Temp");
}); });
it("putEventAsync.fulfilled updates calendar events", () => {
const cal = { id: "c1", events: {} } as any;
const payload = { calId: "c1", events: [{ uid: "e1" }] as any[] };
const state = reducer(
initialState,
putEventAsync.fulfilled(payload, "req8", {
cal,
newEvent: { uid: "e1" } as CalendarEvent,
})
);
expect(state.list.c1.events.e1.uid).toBe("e1");
});
it("getEventAsync.fulfilled adds single event", () => { it("getEventAsync.fulfilled adds single event", () => {
const payload = { calId: "c1", event: { uid: "e1" } as any }; const payload = { calId: "c1", event: { uid: "e1" } as any };
const state = reducer( const state = reducer(
@@ -512,20 +493,6 @@ describe("CalendarSlice", () => {
expect(state.list.c1.events.e1.uid).toBe("e1"); expect(state.list.c1.events.e1.uid).toBe("e1");
}); });
it("moveEventAsync.fulfilled updates events after move", () => {
const cal = { id: "c1", events: {} } as any;
const payload = { calId: "c1", events: [{ uid: "e1" }] as any[] };
const state = reducer(
initialState,
moveEventAsync.fulfilled(payload, "req10", {
cal,
newEvent: { uid: "e1" } as CalendarEvent,
newURL: "e1.ics",
})
);
expect(state.list.c1.events.e1.uid).toBe("e1");
});
it("getCalendarDetailAsync.fulfilled adds calendar events", () => { it("getCalendarDetailAsync.fulfilled adds calendar events", () => {
const payload = { calId: "c1", events: [{ uid: "e1" }] as any[] }; const payload = { calId: "c1", events: [{ uid: "e1" }] as any[] };
const state = reducer( const state = reducer(
@@ -1832,7 +1832,6 @@ describe("Event Full Display", () => {
(promise as any).unwrap = () => promise; (promise as any).unwrap = () => promise;
return () => promise as any; return () => promise as any;
}); });
const spyRemove = jest.spyOn(calendarSlice, "removeEvent");
const testDate = new Date("2025-01-15T10:00:00.000Z"); const testDate = new Date("2025-01-15T10:00:00.000Z");
const testEndDate = new Date("2025-01-15T11:00:00.000Z"); const testEndDate = new Date("2025-01-15T11:00:00.000Z");
@@ -1902,13 +1901,6 @@ describe("Event Full Display", () => {
}, },
{ timeout: 3000 } { timeout: 3000 }
); );
await waitFor(
() => {
expect(spyRemove).toHaveBeenCalled();
},
{ timeout: 3000 }
);
}); });
it("edit modal displays event time in original event timezone", () => { it("edit modal displays event time in original event timezone", () => {
@@ -784,61 +784,6 @@ describe("Event Drag and Drop - Recurring Events", () => {
expect(mockSetOpenEditModePopup).toHaveBeenCalledWith("edit"); expect(mockSetOpenEditModePopup).toHaveBeenCalledWith("edit");
expect(mockSetAfterChoiceFunc).toHaveBeenCalled(); 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", () => { describe("Event Resize - Recurring Events", () => {
@@ -1,859 +0,0 @@
import * as appHooks from "@/app/hooks";
import CalendarLayout from "@/components/Calendar/CalendarLayout";
import * as calendarUtils from "@/components/Calendar/utils/calendarUtils";
import * as eventUtils from "@/components/Event/utils/eventUtils";
import * as CalendarSlice from "@/features/Calendars/CalendarSlice";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import * as eventThunks from "@/features/Calendars/services";
import EventPreviewModal from "@/features/Events/EventDisplayPreview";
import EventPopover from "@/features/Events/EventModal";
import { CalendarEvent } from "@/features/Events/EventsTypes";
import EventUpdateModal from "@/features/Events/EventUpdateModal";
import * as userApi from "@/features/User/userAPI";
import { DateSelectArg } from "@fullcalendar/core";
import { jest } from "@jest/globals";
import { ThunkDispatch } from "@reduxjs/toolkit";
import "@testing-library/jest-dom";
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
import { SpiedFunction } from "jest-mock";
import { renderWithProviders } from "../../utils/Renderwithproviders";
describe("Update tempcalendars called with correct params", () => {
const today = new Date();
const start = new Date(today);
start.setHours(10, 0, 0, 0);
const end = new Date(today);
end.setHours(11, 0, 0, 0);
let refreshCalendarsSpy: SpiedFunction<
(
dispatch: ThunkDispatch<any, any, any>,
calendars: Calendar[],
calendarRange: { start: Date; end: Date },
calType?: "temp"
) => Promise<void>
>;
let refreshSingularCalendarSpy: SpiedFunction<
(
dispatch: ThunkDispatch<any, any, any>,
calendar: Calendar,
calendarRange: { start: Date; end: Date },
calType?: "temp"
) => Promise<void>
>;
let updateTempCalendarSpy: SpiedFunction<
(
tempcalendars: Record<string, Calendar>,
event: CalendarEvent,
dispatch: ThunkDispatch<any, any, any>,
calendarRange: { start: Date; end: Date }
) => Promise<void>
>;
beforeEach(() => {
jest.clearAllMocks();
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")
.mockResolvedValue();
updateTempCalendarSpy = jest.spyOn(calendarUtils, "updateTempCalendar");
refreshSingularCalendarSpy = jest.spyOn(
eventUtils,
"refreshSingularCalendar"
);
});
afterEach(() => {
refreshCalendarsSpy.mockRestore();
updateTempCalendarSpy.mockRestore();
refreshSingularCalendarSpy.mockRestore();
});
const createPreloadedState = (withTempList = true) => ({
user: {
userData: {
sub: "test",
email: "owner@test.com",
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
},
organiserData: {
cn: "Owner",
cal_address: "owner@test.com",
},
tokens: {
accessToken: "token",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
ownerEmails: ["owner@test.com"],
events: {
event1: {
id: "event1",
calId: "667037022b752d0026472254/cal1",
uid: "event1",
title: "Test Event",
start: start.toISOString(),
end: end.toISOString(),
attendee: [{ cal_address: "attendee@test.com" }],
organizer: {
cn: "Owner",
cal_address: "owner@test.com",
},
},
},
},
},
templist: withTempList
? {
temp1: {
id: "temp1",
name: "Temp Calendar",
color: "#00FF00",
events: {},
ownerEmails: ["attendee@test.com"],
},
}
: undefined,
pending: false,
},
});
it("should call updateTempCalendar with correct params after event deletion", async () => {
const preloadedState = createPreloadedState(true);
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedState
);
await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem);
await waitFor(() =>
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
expect.objectContaining({
temp1: expect.objectContaining({ id: "temp1" }),
}),
expect.objectContaining({
attendee: [
expect.objectContaining({ cal_address: "attendee@test.com" }),
],
}),
expect.any(Function),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
})
)
);
await waitFor(() =>
expect(refreshSingularCalendarSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({ id: "temp1" }),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
}),
"temp"
)
);
});
it("should NOT call updateTempCalendar when templist is undefined", async () => {
const preloadedState = createPreloadedState(false);
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedState
);
await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem);
await waitFor(() => {
expect(updateTempCalendarSpy).not.toHaveBeenCalled();
expect(refreshSingularCalendarSpy).not.toHaveBeenCalled();
});
});
it("should call updateTempCalendar with correct params after event creation", async () => {
const preloadedState = createPreloadedState(true);
const defaultSelectedRange = {
startStr: "2025-07-18T09:00Z",
endStr: "2025-07-18T10:00Z",
start: new Date("2025-07-18T09:00Z"),
end: new Date("2025-07-18T10:00Z"),
allDay: false,
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}
open={true}
onClose={jest.fn()}
selectedRange={defaultSelectedRange}
setSelectedRange={jest.fn()}
calendarRef={{ current: null }}
/>,
preloadedState
);
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
const titleInput = screen.getByLabelText(/title/i);
fireEvent.change(titleInput, { target: { value: "New Event" } });
const attendeeInput = screen.getByLabelText(/peopleSearch.label/i);
fireEvent.change(attendeeInput, { target: { value: "attendee@test.com" } });
fireEvent.keyDown(attendeeInput, { key: "Enter", code: "Enter" });
const saveButton = screen.getByText(/save/i);
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),
})
),
{ timeout: 3000 }
);
await waitFor(() =>
expect(refreshSingularCalendarSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({ id: "temp1" }),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
}),
"temp"
)
);
});
it("should call updateTempCalendar with correct params after event update", async () => {
const preloadedState = createPreloadedState(true);
const mockOnClose = jest.fn();
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
eventId="event1"
calId="667037022b752d0026472254/cal1"
/>,
preloadedState
);
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
const saveButton = screen.getByText(/save/i);
fireEvent.click(saveButton);
await waitFor(() =>
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
expect.objectContaining({
temp1: expect.objectContaining({ id: "temp1" }),
}),
expect.objectContaining({
attendee: [
expect.objectContaining({ cal_address: "attendee@test.com" }),
],
}),
expect.any(Function),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
})
)
);
await waitFor(() =>
expect(refreshSingularCalendarSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({ id: "temp1" }),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
}),
"temp"
)
);
});
it("should call updateTempCalendar with correct params after recurring event instance update", async () => {
const preloadedStateWithRecurring = {
...createPreloadedState(true),
calendars: {
...createPreloadedState(true).calendars,
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
ownerEmails: ["owner@test.com"],
events: {
event1: {
id: "event1",
calId: "667037022b752d0026472254/cal1",
uid: "event1",
title: "Recurring Event",
start: start.toISOString(),
end: end.toISOString(),
attendee: [{ cal_address: "attendee@test.com" }],
rrule: {
freq: "WEEKLY",
},
organizer: {
cn: "Owner",
cal_address: "owner@test.com",
},
},
},
},
},
},
};
const mockOnClose = jest.fn();
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
eventId="event1"
calId="667037022b752d0026472254/cal1"
typeOfAction="solo"
/>,
preloadedStateWithRecurring
);
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
const saveButton = screen.getByText(/save/i);
fireEvent.click(saveButton);
await waitFor(() =>
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
expect.objectContaining({
temp1: expect.objectContaining({ id: "temp1" }),
}),
expect.objectContaining({
attendee: [
expect.objectContaining({ cal_address: "attendee@test.com" }),
],
}),
expect.any(Function),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
})
)
);
await waitFor(() =>
expect(refreshSingularCalendarSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({ id: "temp1" }),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
}),
"temp"
)
);
});
it("should call updateTempCalendar with correct params after recurring series update", async () => {
const preloadedStateWithRecurring = {
...createPreloadedState(true),
calendars: {
...createPreloadedState(true).calendars,
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
ownerEmails: ["owner@test.com"],
events: {
event1: {
id: "event1",
calId: "667037022b752d0026472254/cal1",
uid: "event1",
title: "Recurring Event",
start: start.toISOString(),
end: end.toISOString(),
attendee: [{ cal_address: "attendee@test.com" }],
rrule: {
freq: "DAILY",
},
organizer: {
cn: "Owner",
cal_address: "owner@test.com",
},
},
},
},
},
},
};
const mockOnClose = jest.fn();
renderWithProviders(
<EventUpdateModal
open={true}
onClose={mockOnClose}
eventId="event1"
calId="667037022b752d0026472254/cal1"
typeOfAction="all"
/>,
preloadedStateWithRecurring
);
await waitFor(() => {
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
const saveButton = screen.getByText(/save/i);
fireEvent.click(saveButton);
await waitFor(() =>
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
expect.objectContaining({
temp1: expect.objectContaining({ id: "temp1" }),
}),
expect.objectContaining({
attendee: [
expect.objectContaining({ cal_address: "attendee@test.com" }),
],
}),
expect.any(Function),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
})
)
);
await waitFor(() =>
expect(refreshSingularCalendarSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({ id: "temp1" }),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
}),
"temp"
)
);
});
it("should call refreshCalendars with 'temp' param in CalendarLayout handleRefresh", async () => {
const preloadedState = createPreloadedState(true);
renderWithProviders(<CalendarLayout />, preloadedState);
fireEvent.click(screen.getByTestId("RefreshIcon"));
await waitFor(() => {
// Should call refreshCalendars for regular calendars
expect(refreshCalendarsSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Array),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
})
);
// Should also call refreshCalendars with 'temp' for templist
expect(refreshCalendarsSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.arrayContaining([expect.objectContaining({ id: "temp1" })]),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
}),
"temp"
);
});
});
it("should only refresh temp calendars where attendees match ownerEmails", async () => {
const preloadedStateMultipleTempCals = {
...createPreloadedState(true),
calendars: {
...createPreloadedState(true).calendars,
templist: {
temp1: {
id: "temp1",
name: "Temp Calendar 1",
color: "#00FF00",
events: {},
ownerEmails: ["attendee@test.com"], // matches event attendee
},
temp2: {
id: "temp2",
name: "Temp Calendar 2",
color: "#0000FF",
events: {},
ownerEmails: ["other@test.com"], // does NOT match event attendee
},
},
},
};
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedStateMultipleTempCals
);
await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem);
await waitFor(() => {
// Should only refresh temp1, not temp2
const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter(
(call) => call[1]?.id === "temp1"
);
const temp2Calls = refreshSingularCalendarSpy.mock.calls.filter(
(call) => call[1]?.id === "temp2"
);
expect(temp1Calls.length).toBeGreaterThan(0);
expect(temp2Calls.length).toBe(0);
});
});
it("should call emptyEventsCal with correct params before refreshing", async () => {
const emptyEventsCalSpy = jest.spyOn(CalendarSlice, "emptyEventsCal");
const preloadedState = createPreloadedState(true);
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedState
);
await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem);
await waitFor(() => {
// emptyEventsCal should be called with calType: "temp" for singular calendar refresh
expect(emptyEventsCalSpy).toHaveBeenCalledWith(
expect.objectContaining({
calId: "temp1",
calType: "temp",
})
);
});
emptyEventsCalSpy.mockRestore();
});
it("should handle temp calendar with multiple ownerEmails where one matches", async () => {
const preloadedStateMultipleOwners = {
...createPreloadedState(true),
calendars: {
...createPreloadedState(true).calendars,
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
ownerEmails: ["owner@test.com"],
events: {
event1: {
id: "event1",
calId: "667037022b752d0026472254/cal1",
uid: "event1",
title: "Test Event",
start: start.toISOString(),
end: end.toISOString(),
attendee: [{ cal_address: "attendee@test.com" }],
organizer: {
cn: "Owner",
cal_address: "owner@test.com",
},
},
},
},
},
templist: {
temp1: {
id: "temp1",
name: "Shared Calendar",
color: "#00FF00",
events: {},
ownerEmails: [
"someoneelse@test.com",
"attendee@test.com", // matches
"anotherperson@test.com",
],
},
},
},
};
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedStateMultipleOwners
);
await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem);
await waitFor(() => {
// Should still refresh temp1 because one ownerEmail matches
const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter(
(call) => call[1]?.id === "temp1"
);
expect(temp1Calls.length).toBeGreaterThan(0);
expect(temp1Calls[0][3]).toBe("temp");
});
});
it("should update multiple temp calendars when event has multiple attendees matching different ownerEmails", async () => {
const preloadedStateMultipleAttendees = {
...createPreloadedState(true),
calendars: {
...createPreloadedState(true).calendars,
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
ownerEmails: ["owner@test.com"],
events: {
event1: {
id: "event1",
calId: "667037022b752d0026472254/cal1",
uid: "event1",
title: "Test Event",
start: start.toISOString(),
end: end.toISOString(),
attendee: [
{ cal_address: "attendee1@test.com" },
{ cal_address: "attendee2@test.com" },
{ cal_address: "attendee3@test.com" },
],
organizer: {
cn: "Owner",
cal_address: "owner@test.com",
},
},
},
},
},
templist: {
temp1: {
id: "temp1",
name: "Temp Calendar 1",
color: "#00FF00",
events: {},
ownerEmails: ["attendee1@test.com"],
},
temp2: {
id: "temp2",
name: "Temp Calendar 2",
color: "#0000FF",
events: {},
ownerEmails: ["attendee2@test.com"],
},
temp3: {
id: "temp3",
name: "Temp Calendar 3",
color: "#FF00FF",
events: {},
ownerEmails: ["attendee3@test.com"],
},
temp4: {
id: "temp4",
name: "Temp Calendar 4",
color: "#FFFF00",
events: {},
ownerEmails: ["nonmatching@test.com"], // should NOT be refreshed
},
},
},
};
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedStateMultipleAttendees
);
await waitFor(() => {
fireEvent.click(screen.getByTestId("MoreVertIcon"));
expect(screen.getByText("eventPreview.deleteEvent")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("eventPreview.deleteEvent");
fireEvent.click(deleteMenuItem);
await waitFor(() => {
// Should be called once with all templist
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
expect.objectContaining({
temp1: expect.objectContaining({ id: "temp1" }),
temp2: expect.objectContaining({ id: "temp2" }),
temp3: expect.objectContaining({ id: "temp3" }),
temp4: expect.objectContaining({ id: "temp4" }),
}),
expect.objectContaining({
attendee: expect.arrayContaining([
expect.objectContaining({ cal_address: "attendee1@test.com" }),
expect.objectContaining({ cal_address: "attendee2@test.com" }),
expect.objectContaining({ cal_address: "attendee3@test.com" }),
]),
}),
expect.any(Function),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
})
);
});
await waitFor(() => {
// Should refresh temp1, temp2, and temp3 (matching attendees)
const temp1Calls = refreshSingularCalendarSpy.mock.calls.filter(
(call) => call[1]?.id === "temp1"
);
const temp2Calls = refreshSingularCalendarSpy.mock.calls.filter(
(call) => call[1]?.id === "temp2"
);
const temp3Calls = refreshSingularCalendarSpy.mock.calls.filter(
(call) => call[1]?.id === "temp3"
);
const temp4Calls = refreshSingularCalendarSpy.mock.calls.filter(
(call) => call[1]?.id === "temp4"
);
expect(temp1Calls.length).toBeGreaterThan(0);
expect(temp2Calls.length).toBeGreaterThan(0);
expect(temp3Calls.length).toBeGreaterThan(0);
expect(temp4Calls.length).toBe(0); // Should NOT be refreshed
// Verify all matching calendars were refreshed with 'temp' param
expect(temp1Calls[0][3]).toBe("temp");
expect(temp2Calls[0][3]).toBe("temp");
expect(temp3Calls[0][3]).toBe("temp");
});
});
});
+3 -3
View File
@@ -59,7 +59,7 @@ describe("findCalendarById", () => {
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
it("should prioritize main list over temp list", () => { it("should prioritize temp list over main list", () => {
const stateWithDuplicate = { const stateWithDuplicate = {
calendars: { calendars: {
list: { list: {
@@ -73,8 +73,8 @@ describe("findCalendarById", () => {
const result = findCalendarById(stateWithDuplicate, "dup1"); const result = findCalendarById(stateWithDuplicate, "dup1");
expect(result?.calendar).toEqual(mockCalendar1); expect(result?.calendar).toEqual(mockTempCalendar);
expect(result?.type).toBeUndefined(); expect(result?.type).toBe("temp");
}); });
it("should handle undefined list or templist", () => { it("should handle undefined list or templist", () => {
-7
View File
@@ -406,10 +406,6 @@ export default function CalendarApp({
}); });
}, [calendarsWithClearedCache, dispatch, rangeKey, rangeStart, rangeEnd]); }, [calendarsWithClearedCache, dispatch, rangeKey, rangeStart, rangeEnd]);
const tempCalendarControllersRef = useRef<Map<string, AbortController>>(
new Map()
);
useEffect(() => { useEffect(() => {
const currentIds = new Set(tempCalendarIds); const currentIds = new Set(tempCalendarIds);
Object.keys(tempFetchedRangesRef.current).forEach((id) => { Object.keys(tempFetchedRangesRef.current).forEach((id) => {
@@ -558,9 +554,6 @@ export default function CalendarApp({
setSelectedRange, setSelectedRange,
setAnchorEl, setAnchorEl,
calendarRef, calendarRef,
selectedCalendars,
tempcalendars,
calendarRange,
dispatch, dispatch,
setOpenEventDisplay, setOpenEventDisplay,
setEventDisplayedId, setEventDisplayedId,
+7 -8
View File
@@ -30,11 +30,10 @@ function CalendarPopover({
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const userId = const userData = useAppSelector((state) => state.user.userData) ?? {};
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const calendars = useAppSelector((state) => state.calendars.list); const calendars = useAppSelector((state) => state.calendars.list);
const isOwn = calendar?.id const isOwn = calendar?.id
? extractEventBaseUuid(calendar.id) === userId ? extractEventBaseUuid(calendar.id) === userData.openpaasId
: true; : true;
// existing calendar params // existing calendar params
@@ -104,14 +103,14 @@ function CalendarPopover({
name: name.trim(), name: name.trim(),
desc: desc.trim(), desc: desc.trim(),
color: color, color: color,
userId, userData,
calId, calId,
}) })
); );
dispatch( dispatch(
patchACLCalendarAsync({ patchACLCalendarAsync({
calId: `${userId}/${calId}`, calId: `${userData.openpaasId}/${calId}`,
calLink: `/calendars/${userId}/${calId}.json`, calLink: `/calendars/${userData.openpaasId}/${calId}.json`,
request: visibility === "public" ? "{DAV:}read" : "", request: visibility === "public" ? "{DAV:}read" : "",
}) })
); );
@@ -142,7 +141,7 @@ function CalendarPopover({
importedContent && importedContent &&
dispatch( dispatch(
importEventFromFileAsync({ importEventFromFileAsync({
calLink: `/calendar/${userId}/${calId}.json`, calLink: `/calendar/${userData.openpaasId}/${calId}.json`,
file: importedContent, file: importedContent,
}) })
); );
@@ -227,7 +226,7 @@ function CalendarPopover({
importTarget={importTarget} importTarget={importTarget}
setImportTarget={setImportTarget} setImportTarget={setImportTarget}
setImportedContent={setImportedContent} setImportedContent={setImportedContent}
userId={userId} userId={userData.openpaasId ?? ""}
newCalParams={{ newCalParams={{
name: newCalName, name: newCalName,
setName: setNewCalName, setName: setNewCalName,
@@ -1,10 +1,8 @@
import { AppDispatch } from "@/app/store";
import { User } from "@/components/Attendees/PeopleSearch"; import { User } from "@/components/Attendees/PeopleSearch";
import { formatLocalDateTime } from "@/components/Event/utils/dateTimeFormatters"; import { formatLocalDateTime } from "@/components/Event/utils/dateTimeFormatters";
import { refreshCalendars } from "@/components/Event/utils/eventUtils";
import { updateEventLocal } from "@/features/Calendars/CalendarSlice";
import { Calendar } from "@/features/Calendars/CalendarTypes"; import { Calendar } from "@/features/Calendars/CalendarTypes";
import { import {
getCalendarDetailAsync,
getEventAsync, getEventAsync,
putEventAsync, putEventAsync,
updateEventInstanceAsync, updateEventInstanceAsync,
@@ -14,21 +12,15 @@ import { getEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes"; import { CalendarEvent } from "@/features/Events/EventsTypes";
import { userAttendee } from "@/features/User/models/attendee"; import { userAttendee } from "@/features/User/models/attendee";
import { createAttendee } from "@/features/User/models/attendee.mapper"; import { createAttendee } from "@/features/User/models/attendee.mapper";
import { import { getDeltaInMilliseconds } from "@/utils/dateUtils";
formatDateToYYYYMMDDTHHMMSS, import { CalendarApi, DateSelectArg, EventDropArg } from "@fullcalendar/core";
getDeltaInMilliseconds, import { EventResizeDoneArg } from "@fullcalendar/interaction";
} from "@/utils/dateUtils";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import { updateTempCalendar } from "../utils/calendarUtils";
export interface EventHandlersProps { export interface EventHandlersProps {
setSelectedRange: (range: DateSelectArg | null) => void; setSelectedRange: (range: DateSelectArg | null) => void;
setAnchorEl: (el: HTMLElement | null) => void; setAnchorEl: (el: HTMLElement | null) => void;
calendarRef: React.RefObject<CalendarApi | null>; calendarRef: React.RefObject<CalendarApi | null>;
selectedCalendars: string[]; dispatch: AppDispatch;
tempcalendars: Record<string, Calendar>;
calendarRange: { start: Date; end: Date };
dispatch: any;
setOpenEventDisplay: (open: boolean) => void; setOpenEventDisplay: (open: boolean) => void;
setEventDisplayedId: (id: string) => void; setEventDisplayedId: (id: string) => void;
setEventDisplayedCalId: (id: string) => void; setEventDisplayedCalId: (id: string) => void;
@@ -47,9 +39,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
setSelectedRange, setSelectedRange,
setAnchorEl, setAnchorEl,
calendarRef, calendarRef,
selectedCalendars,
tempcalendars,
calendarRange,
dispatch, dispatch,
setOpenEventDisplay, setOpenEventDisplay,
setEventDisplayedId, setEventDisplayedId,
@@ -88,35 +77,10 @@ export const createEventHandlers = (props: EventHandlersProps) => {
setAnchorEl(document.body); setAnchorEl(document.body);
}; };
const handleClosePopover = (refresh?: boolean) => { const handleClosePopover = () => {
calendarRef.current?.unselect(); calendarRef.current?.unselect();
setAnchorEl(null); setAnchorEl(null);
setSelectedRange(null); setSelectedRange(null);
if (refresh) {
selectedCalendars.forEach((calId) =>
dispatch(
getCalendarDetailAsync({
calId,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
})
)
);
Object.keys(tempcalendars).forEach((calId) =>
dispatch(
getCalendarDetailAsync({
calId,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
calType: "temp",
})
)
);
}
}; };
const handleCloseEventDisplay = () => { const handleCloseEventDisplay = () => {
@@ -151,11 +115,11 @@ export const createEventHandlers = (props: EventHandlersProps) => {
} }
}; };
const handleEventAllow = (dropInfo: any, draggedEvent: any) => { const handleEventAllow = () => {
return true; return true;
}; };
const handleEventDrop = async (arg: any) => { const handleEventDrop = async (arg: EventDropArg) => {
if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) { if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) {
return; return;
} }
@@ -182,7 +146,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
} as CalendarEvent, } as CalendarEvent,
true true
); );
if (isRecurring) { if (isRecurring) {
setSelectedEvent(event); setSelectedEvent(event);
setOpenEditModePopup("edit"); setOpenEditModePopup("edit");
@@ -192,9 +155,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
await dispatch( await dispatch(
updateEventInstanceAsync({ cal: calendar, event: newEvent }) updateEventInstanceAsync({ cal: calendar, event: newEvent })
); );
dispatch(
updateEventLocal({ calId: newEvent.calId, event: newEvent })
);
} else if (typeOfAction === "all") { } else if (typeOfAction === "all") {
const master = await getEvent(newEvent, true); const master = await getEvent(newEvent, true);
await dispatch( await dispatch(
@@ -208,30 +168,17 @@ export const createEventHandlers = (props: EventHandlersProps) => {
}, },
}) })
); );
await refreshCalendars(
dispatch,
Object.values(calendars),
calendarRange
);
await updateTempCalendar(
tempcalendars,
event,
dispatch,
calendarRange
);
} }
} }
); );
} else { } else {
dispatch(updateEventLocal({ calId: newEvent.calId, event: newEvent }));
await dispatch( await dispatch(
putEventAsync({ cal: calendars[newEvent.calId], newEvent }) putEventAsync({ cal: calendars[newEvent.calId], newEvent })
); );
} }
await updateTempCalendar(tempcalendars, event, dispatch, calendarRange);
}; };
const handleEventResize = async (arg: any) => { const handleEventResize = async (arg: EventResizeDoneArg) => {
if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) { if (!arg.event || !arg.event._def || !arg.event._def.extendedProps) {
return; return;
} }
@@ -261,7 +208,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
} as CalendarEvent, } as CalendarEvent,
true true
); );
if (isRecurring) { if (isRecurring) {
setSelectedEvent(event); setSelectedEvent(event);
setOpenEditModePopup("edit"); setOpenEditModePopup("edit");
@@ -271,9 +217,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
await dispatch( await dispatch(
updateEventInstanceAsync({ cal: calendar, event: newEvent }) updateEventInstanceAsync({ cal: calendar, event: newEvent })
); );
dispatch(
updateEventLocal({ calId: newEvent.calId, event: newEvent })
);
} else if (typeOfAction === "all") { } else if (typeOfAction === "all") {
const master = await getEvent(newEvent, true); const master = await getEvent(newEvent, true);
@@ -288,17 +231,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
}, },
}) })
); );
await refreshCalendars(
dispatch,
Object.values(calendars),
calendarRange
);
await updateTempCalendar(
tempcalendars,
event,
dispatch,
calendarRange
);
} }
} }
); );
@@ -307,7 +239,6 @@ export const createEventHandlers = (props: EventHandlersProps) => {
putEventAsync({ cal: calendars[newEvent.calId], newEvent }) putEventAsync({ cal: calendars[newEvent.calId], newEvent })
); );
} }
await updateTempCalendar(tempcalendars, event, dispatch, calendarRange);
}; };
return { return {
@@ -19,9 +19,6 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
props.calendarRef, props.calendarRef,
props.setAnchorEl, props.setAnchorEl,
props.setSelectedRange, props.setSelectedRange,
props.selectedCalendars,
props.tempcalendars,
props.calendarRange,
props.dispatch, props.dispatch,
]), ]),
handleCloseEventDisplay: useCallback( handleCloseEventDisplay: useCallback(
@@ -43,14 +40,10 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
props.setSelectedEvent, props.setSelectedEvent,
props.setOpenEditModePopup, props.setOpenEditModePopup,
props.setAfterChoiceFunc, props.setAfterChoiceFunc,
props.tempcalendars,
props.calendarRange,
]), ]),
handleEventResize: useCallback(eventHandlers.handleEventResize, [ handleEventResize: useCallback(eventHandlers.handleEventResize, [
props.calendars, props.calendars,
props.dispatch, props.dispatch,
props.tempcalendars,
props.calendarRange,
]), ]),
}; };
}; };
@@ -271,35 +271,3 @@ export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" {
if (hasRead) return "public"; if (hasRead) return "public";
return "private"; return "private";
} }
export async function updateTempCalendar(
tempcalendars: Record<string, Calendar>,
event: CalendarEvent,
dispatch: ThunkDispatch<any, any, any>,
calendarRange: { start: Date; end: Date }
) {
if (tempcalendars && event?.attendee) {
const attendeesEmails = event.attendee
.map((a) => a.cal_address)
.filter(Boolean);
const tempCalendarValues = Object.values(tempcalendars);
for (const tempCalendar of tempCalendarValues) {
const ownerEmails = tempCalendar.ownerEmails || [];
// Check if any of the attendees are owners of this temp calendar
const isOwnerAttendee = ownerEmails.some((ownerEmail) =>
attendeesEmails.includes(ownerEmail)
);
if (isOwnerAttendee) {
await refreshSingularCalendar(
dispatch,
tempCalendar,
calendarRange,
"temp"
);
}
}
}
}
@@ -12,8 +12,6 @@ import { PartStat } from "@/features/User/models/attendee";
import { createAttendee } from "@/features/User/models/attendee.mapper"; import { createAttendee } from "@/features/User/models/attendee.mapper";
import { userData } from "@/features/User/userDataTypes"; import { userData } from "@/features/User/userDataTypes";
import { buildFamilyName } from "@/utils/buildFamilyName"; import { buildFamilyName } from "@/utils/buildFamilyName";
import { getCalendarRange } from "@/utils/dateUtils";
import { refreshCalendars } from "../utils/eventUtils";
function updateEventAttendees( function updateEventAttendees(
event: CalendarEvent, event: CalendarEvent,
@@ -78,15 +76,11 @@ async function handleSoloRSVP(
} }
async function handleAllRSVP( async function handleAllRSVP(
dispatch: AppDispatch,
event: CalendarEvent, event: CalendarEvent,
userEmail: string, userEmail: string,
rsvp: PartStat, rsvp: PartStat
calendars: Calendar[]
) { ) {
const calendarRange = getCalendarRange(new Date(event.start));
await updateSeriesPartstat(event, userEmail, rsvp); await updateSeriesPartstat(event, userEmail, rsvp);
await refreshCalendars(dispatch, calendars, calendarRange);
} }
async function handleDefaultRSVP( async function handleDefaultRSVP(
@@ -103,8 +97,7 @@ export async function handleRSVP(
user: userData | undefined, user: userData | undefined,
event: CalendarEvent, event: CalendarEvent,
rsvp: PartStat, rsvp: PartStat,
typeOfAction?: string, typeOfAction?: string
calendars?: Calendar[]
) { ) {
const newEvent = { const newEvent = {
...event, ...event,
@@ -114,13 +107,10 @@ export async function handleRSVP(
if (typeOfAction === "solo") { if (typeOfAction === "solo") {
await handleSoloRSVP(dispatch, calendar, newEvent); await handleSoloRSVP(dispatch, calendar, newEvent);
} else if (typeOfAction === "all") { } else if (typeOfAction === "all") {
if (!calendars || calendars.length === 0) {
throw new Error("Cannot update all occurrences without calendar list");
}
if (!user?.email) { if (!user?.email) {
throw new Error("Cannot update all occurrences without user email"); throw new Error("Cannot update all occurrences without user email");
} }
await handleAllRSVP(dispatch, event, user.email, rsvp, calendars); await handleAllRSVP(event, user.email, rsvp);
} else { } else {
await handleDefaultRSVP(dispatch, calendar, newEvent); await handleDefaultRSVP(dispatch, calendar, newEvent);
} }
+9 -85
View File
@@ -23,12 +23,6 @@ import {
updateSeriesAsync, updateSeriesAsync,
} from "./services"; } from "./services";
// Define error type for rejected actions
export interface RejectedError {
message: string;
status?: number;
}
const CalendarSlice = createSlice({ const CalendarSlice = createSlice({
name: "calendars", name: "calendars",
initialState: { initialState: {
@@ -182,42 +176,10 @@ const CalendarSlice = createSlice({
); );
} }
) )
.addCase( .addCase(putEventAsync.fulfilled, (state) => {
putEventAsync.fulfilled, state.pending = false;
( state.error = null;
state, })
action: PayloadAction<{
calId: string;
events: CalendarEvent[];
calType?: "temp";
}>
) => {
state.pending = false;
const type = action.payload.calType === "temp" ? "templist" : "list";
if (!state[type][action.payload.calId]) {
state[type][action.payload.calId] = {
id: action.payload.calId,
events: {},
} as Calendar;
}
action.payload.events.forEach((event) => {
state[type][action.payload.calId].events[event.uid] = event;
});
Object.keys(state[type][action.payload.calId].events).forEach(
(id) => {
state[type][action.payload.calId].events[id].color =
state[type][action.payload.calId].color;
state[type][action.payload.calId].events[id].calId =
action.payload.calId;
if (!state[type][action.payload.calId].events[id].timezone) {
state[type][action.payload.calId].events[id].timezone =
browserDefaultTimeZone;
}
}
);
}
)
.addCase( .addCase(
getEventAsync.fulfilled, getEventAsync.fulfilled,
( (
@@ -236,50 +198,12 @@ const CalendarSlice = createSlice({
action.payload.event; action.payload.event;
} }
) )
.addCase( .addCase(moveEventAsync.fulfilled, (state) => {
moveEventAsync.fulfilled, state.pending = false;
( state.error = null;
state, })
action: PayloadAction<{ calId: string; events: CalendarEvent[] }> .addCase(deleteEventAsync.fulfilled, (state) => {
) => {
state.pending = false;
if (!state.list[action.payload.calId]) {
state.list[action.payload.calId] = {
id: action.payload.calId,
events: {},
} as Calendar;
}
action.payload.events.forEach((event) => {
state.list[action.payload.calId].events[event.uid] = event;
});
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
state.list[action.payload.calId].events[id].color =
state.list[action.payload.calId].color;
state.list[action.payload.calId].events[id].calId =
action.payload.calId;
if (!state.list[action.payload.calId].events[id].timezone) {
state.list[action.payload.calId].events[id].timezone =
browserDefaultTimeZone;
}
});
}
)
.addCase(deleteEventAsync.fulfilled, (state, action) => {
state.pending = false; state.pending = false;
const [baseId, recurrenceId] = action.payload.eventId.split("/");
if (recurrenceId) {
Object.keys(state.list[action.payload.calId].events).forEach(
(element) => {
if (extractEventBaseUuid(element) === baseId) {
delete state.list[action.payload.calId].events[element];
}
}
);
} else {
delete state.list[action.payload.calId].events[
action.payload.eventId
];
}
state.error = null; state.error = null;
}) })
.addCase(deleteEventInstanceAsync.fulfilled, (state, action) => { .addCase(deleteEventInstanceAsync.fulfilled, (state, action) => {
@@ -2,7 +2,7 @@ import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { getUserDetails } from "@/features/User/userAPI"; import { getUserDetails } from "@/features/User/userAPI";
import { addSharedCalendar } from "../CalendarApi"; import { addSharedCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const addSharedCalendarAsync = createAsyncThunk< export const addSharedCalendarAsync = createAsyncThunk<
{ {
@@ -1,8 +1,9 @@
import { getUserDetails } from "@/features/User/userAPI"; import { getUserDetails } from "@/features/User/userAPI";
import { userData } from "@/features/User/userDataTypes";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { postCalendar } from "../CalendarApi"; import { postCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const createCalendarAsync = createAsyncThunk< export const createCalendarAsync = createAsyncThunk<
{ {
@@ -15,7 +16,7 @@ export const createCalendarAsync = createAsyncThunk<
ownerEmails: string[]; ownerEmails: string[];
}, },
{ {
userId: string; userData: userData;
calId: string; calId: string;
color: Record<string, string>; color: Record<string, string>;
name: string; name: string;
@@ -24,21 +25,25 @@ export const createCalendarAsync = createAsyncThunk<
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>( >(
"calendars/createCalendar", "calendars/createCalendar",
async ({ userId, calId, color, name, desc }, { rejectWithValue }) => { async ({ userData, calId, color, name, desc }, { rejectWithValue }) => {
try { try {
await postCalendar(userId, calId, color, name, desc); if (!userData.openpaasId) {
const ownerData: any = await getUserDetails(userId.split("/")[0]); throw new Error("No openpaasId");
}
await postCalendar(userData.openpaasId, calId, color, name, desc);
const owner = [userData.given_name, userData.family_name]
.filter(Boolean)
.join(" ");
return { return {
userId, userId: userData.openpaasId,
calId, calId,
color, color,
name, name,
desc, desc,
owner: [ownerData.firstname, ownerData.lastname] owner,
.filter(Boolean) ownerEmails: userData.email ? [userData.email] : [],
.join(" "),
ownerEmails: ownerData.emails ?? [],
}; };
} catch (err: any) { } catch (err: any) {
return rejectWithValue({ return rejectWithValue({
@@ -1,7 +1,7 @@
import { deleteEvent } from "@/features/Events/EventApi"; import { deleteEvent } from "@/features/Events/EventApi";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const deleteEventAsync = createAsyncThunk< export const deleteEventAsync = createAsyncThunk<
{ calId: string; eventId: string }, { calId: string; eventId: string },
@@ -2,7 +2,7 @@ import { deleteEventInstance } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes"; import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
export const deleteEventInstanceAsync = createAsyncThunk< export const deleteEventInstanceAsync = createAsyncThunk<
@@ -2,7 +2,7 @@ import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi"; import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { extractCalendarEvents } from "../utils/extractCalendarEvents"; import { extractCalendarEvents } from "../utils/extractCalendarEvents";
export const getCalendarDetailAsync = createAsyncThunk< export const getCalendarDetailAsync = createAsyncThunk<
@@ -2,7 +2,7 @@ import { getOpenPaasUser, getUserDetails } from "@/features/User/userAPI";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendars } from "../CalendarApi"; import { getCalendars } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
import { normalizeCalendar } from "../utils/normalizeCalendar"; import { normalizeCalendar } from "../utils/normalizeCalendar";
@@ -2,7 +2,7 @@ import { getEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes"; import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const getEventAsync = createAsyncThunk< export const getEventAsync = createAsyncThunk<
{ calId: string; event: CalendarEvent }, { calId: string; event: CalendarEvent },
@@ -4,7 +4,7 @@ import { getUserDetails } from "@/features/User/userAPI";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendars } from "../CalendarApi"; import { getCalendars } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
export const getTempCalendarsListAsync = createAsyncThunk< export const getTempCalendarsListAsync = createAsyncThunk<
@@ -14,9 +14,14 @@ export const getTempCalendarsListAsync = createAsyncThunk<
>("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => { >("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => {
try { try {
const importedCalendars: Record<string, Calendar> = {}; const importedCalendars: Record<string, Calendar> = {};
if (!tempUser.openpaasId) {
const username = tempUser.displayName || tempUser.email || "User";
throw new Error(
`TRANSLATION:calendar.userDoesNotHaveValidId|name=${encodeURIComponent(username)}`
);
}
const calendars = (await getCalendars( const calendars = (await getCalendars(
tempUser.openpaasId ?? "", tempUser.openpaasId,
"sharedPublic=true&" "sharedPublic=true&"
)) as Record<string, any>; )) as Record<string, any>;
@@ -2,7 +2,7 @@ import { importEventFromFile } from "@/features/Events/EventApi";
import { importFile } from "@/utils/apiUtils"; import { importFile } from "@/utils/apiUtils";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const importEventFromFileAsync = createAsyncThunk< export const importEventFromFileAsync = createAsyncThunk<
void, void,
@@ -8,11 +8,11 @@ import {
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi"; import { getCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
export const moveEventAsync = createAsyncThunk< export const moveEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, { calId: string },
{ cal: Calendar; newEvent: CalendarEvent; newURL: string }, { cal: Calendar; newEvent: CalendarEvent; newURL: string },
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>( >(
@@ -21,31 +21,8 @@ export const moveEventAsync = createAsyncThunk<
try { try {
await moveEvent(newEvent, newURL); await moveEvent(newEvent, newURL);
const eventDate = new Date(newEvent.start);
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
})) as Record<string, any>;
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
(eventdata: any) => {
const vevents = eventdata.data[2] as any[][];
const eventURL = eventdata._links.self.href;
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(
vevent[1],
cal.color ?? {},
cal.id,
eventURL
);
});
}
);
return { return {
calId: cal.id, calId: cal.id,
events,
}; };
} catch (err: any) { } catch (err: any) {
return rejectWithValue({ return rejectWithValue({
@@ -1,7 +1,7 @@
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { updateAclCalendar } from "../CalendarApi"; import { updateAclCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const patchACLCalendarAsync = createAsyncThunk< export const patchACLCalendarAsync = createAsyncThunk<
{ {
@@ -1,7 +1,7 @@
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { proppatchCalendar } from "../CalendarApi"; import { proppatchCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const patchCalendarAsync = createAsyncThunk< export const patchCalendarAsync = createAsyncThunk<
{ {
@@ -1,18 +1,12 @@
import { putEvent } from "@/features/Events/EventApi"; import { putEvent } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes"; import { CalendarEvent } from "@/features/Events/EventsTypes";
import { parseCalendarEvent } from "@/features/Events/eventUtils";
import {
computeWeekRange,
formatDateToYYYYMMDDTHHMMSS,
} from "@/utils/dateUtils";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { getCalendar } from "../CalendarApi"; import { RejectedError } from "../types/RejectedError";
import { RejectedError } from "../CalendarSlice";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
export const putEventAsync = createAsyncThunk< export const putEventAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[]; calType?: "temp" }, { calId: string; calType?: "temp" },
{ cal: Calendar; newEvent: CalendarEvent; calType?: "temp" }, { cal: Calendar; newEvent: CalendarEvent; calType?: "temp" },
{ rejectValue: RejectedError } { rejectValue: RejectedError }
>( >(
@@ -23,34 +17,9 @@ export const putEventAsync = createAsyncThunk<
newEvent, newEvent,
cal.ownerEmails ? cal.ownerEmails[0] : undefined cal.ownerEmails ? cal.ownerEmails[0] : undefined
); );
const eventDate = new Date(newEvent.start);
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
const calEvents = (await getCalendar(cal.id, {
start: formatDateToYYYYMMDDTHHMMSS(weekStart),
end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
})) as Record<string, any>;
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
(eventdata: any) => {
const vevents = eventdata.data[2] as any[][];
const eventURL = eventdata._links.self.href;
const valarm = eventdata.data[2][0][2][0];
return vevents.map((vevent: any[]) => {
return parseCalendarEvent(
vevent[1],
cal.color ?? {},
cal.id,
eventURL,
valarm
);
});
}
);
return { return {
calId: cal.id, calId: cal.id,
events,
calType, calType,
}; };
} catch (err: any) { } catch (err: any) {
@@ -3,7 +3,7 @@ import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import pMap from "p-map"; import pMap from "p-map";
import { fetchSyncTokenChanges } from "../api/fetchSyncTokenChanges"; import { fetchSyncTokenChanges } from "../api/fetchSyncTokenChanges";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
import { expandEventFunction } from "../utils/expandEventFunction"; import { expandEventFunction } from "../utils/expandEventFunction";
import { processSyncUpdates } from "../utils/processSyncTokenUpdates"; import { processSyncUpdates } from "../utils/processSyncTokenUpdates";
@@ -1,7 +1,7 @@
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { removeCalendar } from "../CalendarApi"; import { removeCalendar } from "../CalendarApi";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
export const removeCalendarAsync = createAsyncThunk< export const removeCalendarAsync = createAsyncThunk<
{ {
@@ -2,7 +2,7 @@ import { putEventWithOverrides } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes"; import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
export const updateEventInstanceAsync = createAsyncThunk< export const updateEventInstanceAsync = createAsyncThunk<
@@ -2,7 +2,7 @@ import { updateSeries } from "@/features/Events/EventApi";
import { CalendarEvent } from "@/features/Events/EventsTypes"; import { CalendarEvent } from "@/features/Events/EventsTypes";
import { formatReduxError } from "@/utils/errorUtils"; import { formatReduxError } from "@/utils/errorUtils";
import { createAsyncThunk } from "@reduxjs/toolkit"; import { createAsyncThunk } from "@reduxjs/toolkit";
import { RejectedError } from "../CalendarSlice"; import { RejectedError } from "../types/RejectedError";
import { Calendar } from "../CalendarTypes"; import { Calendar } from "../CalendarTypes";
export const updateSeriesAsync = createAsyncThunk< export const updateSeriesAsync = createAsyncThunk<
@@ -0,0 +1,6 @@
// Define error type for rejected actions
export interface RejectedError {
message: string;
status?: number;
}
@@ -1,4 +1,3 @@
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { userData } from "@/features/User/userDataTypes"; import { userData } from "@/features/User/userDataTypes";
import { Box, Typography } from "@linagora/twake-mui"; import { Box, Typography } from "@linagora/twake-mui";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
@@ -8,7 +7,6 @@ import { RSVPButton } from "./RSVPButton";
interface AttendanceValidationProps { interface AttendanceValidationProps {
contextualizedEvent: ContextualizedEvent; contextualizedEvent: ContextualizedEvent;
calendarList: Calendar[];
user: userData | undefined; user: userData | undefined;
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>; setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>; setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
@@ -16,7 +14,6 @@ interface AttendanceValidationProps {
export function AttendanceValidation({ export function AttendanceValidation({
contextualizedEvent, contextualizedEvent,
calendarList,
user, user,
setAfterChoiceFunc, setAfterChoiceFunc,
setOpenEditModePopup, setOpenEditModePopup,
@@ -36,7 +33,6 @@ export function AttendanceValidation({
const commonButtonProps = { const commonButtonProps = {
contextualizedEvent, contextualizedEvent,
user, user,
calendarList,
setAfterChoiceFunc, setAfterChoiceFunc,
setOpenEditModePopup, setOpenEditModePopup,
}; };
@@ -1,5 +1,4 @@
import { useAppDispatch } from "@/app/hooks"; import { useAppDispatch } from "@/app/hooks";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { PartStat } from "@/features/User/models/attendee"; import { PartStat } from "@/features/User/models/attendee";
import { userData } from "@/features/User/userDataTypes"; import { userData } from "@/features/User/userDataTypes";
import { Button } from "@linagora/twake-mui"; import { Button } from "@linagora/twake-mui";
@@ -20,7 +19,6 @@ interface RSVPButtonProps {
rsvpValue: PartStat; rsvpValue: PartStat;
contextualizedEvent: ContextualizedEvent; contextualizedEvent: ContextualizedEvent;
user: userData | undefined; user: userData | undefined;
calendarList: Calendar[];
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>; setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>; setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
} }
@@ -29,7 +27,6 @@ export function RSVPButton({
rsvpValue, rsvpValue,
contextualizedEvent, contextualizedEvent,
user, user,
calendarList,
setAfterChoiceFunc, setAfterChoiceFunc,
setOpenEditModePopup, setOpenEditModePopup,
}: RSVPButtonProps) { }: RSVPButtonProps) {
@@ -54,7 +51,6 @@ export function RSVPButton({
rsvpValue, rsvpValue,
contextualizedEvent, contextualizedEvent,
user, user,
calendarList,
setAfterChoiceFunc, setAfterChoiceFunc,
setOpenEditModePopup, setOpenEditModePopup,
dispatch dispatch
@@ -1,6 +1,5 @@
import { AppDispatch } from "@/app/store"; import { AppDispatch } from "@/app/store";
import { handleRSVP } from "@/components/Event/eventHandlers/eventHandlers"; import { handleRSVP } from "@/components/Event/eventHandlers/eventHandlers";
import { Calendar } from "@/features/Calendars/CalendarTypes";
import { PartStat } from "@/features/User/models/attendee"; import { PartStat } from "@/features/User/models/attendee";
import { userData } from "@/features/User/userDataTypes"; import { userData } from "@/features/User/userDataTypes";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
@@ -10,7 +9,6 @@ export async function handleRSVPClick(
rsvp: PartStat, rsvp: PartStat,
contextualizedEvent: ContextualizedEvent, contextualizedEvent: ContextualizedEvent,
user: userData | undefined, user: userData | undefined,
calendarList: Calendar[],
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>, setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>, setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
dispatch: AppDispatch dispatch: AppDispatch
@@ -19,15 +17,7 @@ export async function handleRSVPClick(
if (isRecurring) { if (isRecurring) {
setAfterChoiceFunc(() => async (type: string) => { setAfterChoiceFunc(() => async (type: string) => {
try { try {
await handleRSVP( await handleRSVP(dispatch, calendar, user, event, rsvp, type);
dispatch,
calendar,
user,
event,
rsvp,
type,
calendarList
);
} catch (error) { } catch (error) {
console.error("Error handling RSVP:", error); console.error("Error handling RSVP:", error);
} }
+1 -19
View File
@@ -1,17 +1,13 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { useAppDispatch, useAppSelector } from "@/app/hooks";
import { CalendarName } from "@/components/Calendar/CalendarName"; import { CalendarName } from "@/components/Calendar/CalendarName";
import { getTimezoneOffset } from "@/components/Calendar/TimezoneSelector"; import { getTimezoneOffset } from "@/components/Calendar/TimezoneSelector";
import { import { formatEventChipTitle } from "@/components/Calendar/utils/calendarUtils";
formatEventChipTitle,
updateTempCalendar,
} from "@/components/Calendar/utils/calendarUtils";
import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog"; import ResponsiveDialog from "@/components/Dialog/ResponsiveDialog";
import { EditModeDialog } from "@/components/Event/EditModeDialog"; import { EditModeDialog } from "@/components/Event/EditModeDialog";
import EventDuplication from "@/components/Event/EventDuplicate"; import EventDuplication from "@/components/Event/EventDuplicate";
import { handleDelete } from "@/components/Event/eventHandlers/eventHandlers"; import { handleDelete } from "@/components/Event/eventHandlers/eventHandlers";
import { InfoRow } from "@/components/Event/InfoRow"; import { InfoRow } from "@/components/Event/InfoRow";
import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils"; import { renderAttendeeBadge } from "@/components/Event/utils/eventUtils";
import { getCalendarRange } from "@/utils/dateUtils";
import { browserDefaultTimeZone } from "@/utils/timezone"; import { browserDefaultTimeZone } from "@/utils/timezone";
import { DateSelectArg } from "@fullcalendar/core"; import { DateSelectArg } from "@fullcalendar/core";
import { import {
@@ -282,18 +278,6 @@ export default function EventPreviewModal({
(a) => a.cal_address === event.organizer?.cal_address (a) => a.cal_address === event.organizer?.cal_address
); );
const updateTempList = async () => {
if (calendars.templist) {
const calendarRange = getCalendarRange(new Date(event.start));
await updateTempCalendar(
calendars.templist,
event,
dispatch,
calendarRange
);
}
};
return ( return (
<> <>
<ResponsiveDialog <ResponsiveDialog
@@ -429,8 +413,6 @@ export default function EventPreviewModal({
if (result && typeof result.unwrap === "function") { if (result && typeof result.unwrap === "function") {
await result.unwrap(); await result.unwrap();
} }
await updateTempList();
} catch (error) { } catch (error) {
console.error("Failed to delete event:", error); console.error("Failed to delete event:", error);
} }
+1 -7
View File
@@ -60,12 +60,11 @@ function EventPopover({
event?: CalendarEvent; event?: CalendarEvent;
}) { }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t, lang } = useI18n(); const { t } = useI18n();
const organizer = useAppSelector((state) => state.user.organiserData); const organizer = useAppSelector((state) => state.user.organiserData);
const userId = const userId =
useAppSelector((state) => state.user.userData?.openpaasId) ?? ""; useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list); const calList = useAppSelector((state) => state.calendars.list);
const selectPersonalCalendars = createSelector( const selectPersonalCalendars = createSelector(
(state: any) => state.calendars, (state: any) => state.calendars,
@@ -838,11 +837,6 @@ function EventPopover({
} }
} }
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
}
// Clear temp data on successful save // Clear temp data on successful save
clearEventFormTempData("create"); clearEventFormTempData("create");
+5 -29
View File
@@ -65,7 +65,6 @@ function EventUpdateModal({
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const tempList = useAppSelector((state) => state.calendars.templist);
const calList = useAppSelector((state) => state.calendars.list); const calList = useAppSelector((state) => state.calendars.list);
// Get event from Redux store (cached data) as fallback // Get event from Redux store (cached data) as fallback
const cachedEvent = useAppSelector( const cachedEvent = useAppSelector(
@@ -78,6 +77,10 @@ function EventUpdateModal({
// Use fresh data if available, otherwise use eventData from props, otherwise use cached data // Use fresh data if available, otherwise use eventData from props, otherwise use cached data
const event = freshEvent || eventData || cachedEvent; const event = freshEvent || eventData || cachedEvent;
useEffect(() => {
setFreshEvent(null);
}, [eventId, calId]);
// Fetch fresh event data when modal opens // Fetch fresh event data when modal opens
useEffect(() => { useEffect(() => {
if (open && cachedEvent && !eventData) { if (open && cachedEvent && !eventData) {
@@ -714,11 +717,6 @@ function EventUpdateModal({
// Clear cache to ensure navigation to other weeks works // Clear cache to ensure navigation to other weeks works
dispatch(clearFetchCache(calId)); dispatch(clearFetchCache(calId));
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 // STEP 5: Remove old recurring instances only after the rest succeeds
removeSeriesInstancesFromUI(); removeSeriesInstancesFromUI();
@@ -834,7 +832,7 @@ function EventUpdateModal({
const repetitionRulesChanged = changes.repetitionRulesChanged; const repetitionRulesChanged = changes.repetitionRulesChanged;
if (repetitionRulesChanged) { if (repetitionRulesChanged) {
// Date/time or repetition rules changed - remove all overrides and refresh // Date/time or repetition rules changed - remove all overrides
const seriesInstancesSnapshot = getSeriesInstances(); const seriesInstancesSnapshot = getSeriesInstances();
@@ -892,23 +890,6 @@ function EventUpdateModal({
} }
} }
// 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 // Clear cache after reload
dispatch(clearFetchCache(calId)); dispatch(clearFetchCache(calId));
@@ -1085,15 +1066,10 @@ function EventUpdateModal({
if (moveResult && typeof moveResult.unwrap === "function") { if (moveResult && typeof moveResult.unwrap === "function") {
await moveResult.unwrap(); await moveResult.unwrap();
} }
dispatch(removeEvent({ calendarUid: calId, eventUid: event.uid }));
// Clear temp data on successful move // Clear temp data on successful move
clearEventFormTempData("update"); 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) // Reset all state to default values only on successful save (after all branches)
clearEventFormTempData("update"); clearEventFormTempData("update");
+3 -3
View File
@@ -8,10 +8,10 @@ export function findCalendarById(
if (calendarId.length === 0) { if (calendarId.length === 0) {
return; return;
} }
if (state.calendars?.list?.[calendarId]) {
return { calendar: state.calendars.list[calendarId] };
}
if (state.calendars?.templist?.[calendarId]) { if (state.calendars?.templist?.[calendarId]) {
return { calendar: state.calendars.templist[calendarId], type: "temp" }; return { calendar: state.calendars.templist[calendarId], type: "temp" };
} }
if (state.calendars?.list?.[calendarId]) {
return { calendar: state.calendars.list[calendarId] };
}
} }
+14
View File
@@ -10,6 +10,7 @@ import { syncCalendarRegistrations } from "./operations";
export function WebSocketGate() { export function WebSocketGate() {
const socketRef = useRef<WebSocketWithCleanup | null>(null); const socketRef = useRef<WebSocketWithCleanup | null>(null);
const previousCalendarListRef = useRef<string[]>([]); const previousCalendarListRef = useRef<string[]>([]);
const previousTempCalendarListRef = useRef<string[]>([]);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isAuthenticated = useAppSelector((state) => const isAuthenticated = useAppSelector((state) =>
Boolean(state.user.userData && state.user.tokens) Boolean(state.user.userData && state.user.tokens)
@@ -18,6 +19,10 @@ export function WebSocketGate() {
const [isSocketOpen, setIsSocketOpen] = useState(false); const [isSocketOpen, setIsSocketOpen] = useState(false);
const calendarList = useSelectedCalendars(); const calendarList = useSelectedCalendars();
const tempCalendarList = Object.keys(
useAppSelector((state) => state?.calendars?.templist) ?? {}
);
const onMessage = useCallback( const onMessage = useCallback(
(message: unknown) => { (message: unknown) => {
updateCalendars(message, dispatch); updateCalendars(message, dispatch);
@@ -76,5 +81,14 @@ export function WebSocketGate() {
); );
}, [isSocketOpen, calendarList]); }, [isSocketOpen, calendarList]);
useEffect(() => {
syncCalendarRegistrations(
isSocketOpen,
socketRef,
tempCalendarList,
previousTempCalendarListRef
);
}, [isSocketOpen, tempCalendarList]);
return null; return null;
} }