[#168] changed temp calendar reload to reload only thoses that are impacted

This commit is contained in:
Camille Moussu
2025-10-16 09:46:47 +02:00
committed by Benoit TELLIER
parent 29f984b347
commit f278caf041
9 changed files with 1119 additions and 67 deletions
@@ -14,6 +14,8 @@ import {
stringToColor,
stringAvatar,
} from "../../../src/components/Event/utils/eventUtils";
import * as appHooks from "../../../src/app/hooks";
import { ThunkDispatch } from "@reduxjs/toolkit";
describe("Event Preview Display", () => {
const mockOnClose = jest.fn();
@@ -561,6 +563,200 @@ describe("Event Preview Display", () => {
expect(mockOpen).toHaveBeenCalledWith(expectedUrl);
});
describe("Owner Email Permissions", () => {
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);
beforeEach(() => {
jest.clearAllMocks();
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
});
const createPreloadedState = (
userEmail: string,
ownerEmails: string[],
organizerEmail: string
) => ({
user: {
userData: {
sub: "test",
email: userEmail,
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
},
tokens: {
accessToken: "token",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
ownerEmails: ownerEmails,
events: {
event1: {
id: "event1",
calId: "667037022b752d0026472254/cal1",
uid: "event1",
title: "Test Event",
start: start.toISOString(),
end: end.toISOString(),
partstat: "ACCEPTED",
organizer: {
cn: "Organizer",
cal_address: organizerEmail,
},
attendee: [
{
cn: "Organizer",
partstat: "ACCEPTED",
rsvp: "TRUE",
role: "REQ-PARTICIPANT",
cutype: "INDIVIDUAL",
cal_address: organizerEmail,
},
],
},
},
},
},
pending: false,
},
});
it("should show edit button when user email matches organizer and is in ownerEmails", async () => {
const preloadedState = createPreloadedState(
"owner@test.com",
["owner@test.com"],
"owner@test.com"
);
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedState
);
await waitFor(() => {
const editButton = screen.queryByTestId("EditIcon");
expect(editButton).toBeInTheDocument();
});
});
it("should NOT show edit button when user is organizer but not in owner of calendar", async () => {
const preloadedState = createPreloadedState(
"organizer@test.com",
["other@test.com"], // organizer not in list
"organizer@test.com"
);
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedState
);
await waitFor(() => {
const editButton = screen.queryByTestId("EditIcon");
expect(editButton).not.toBeInTheDocument();
});
});
it("should NOT show edit button when user is in ownerEmails but not organizer", async () => {
const preloadedState = createPreloadedState(
"owner@test.com",
["owner@test.com"],
"other@test.com" // different organizer
);
const mockOnClose = jest.fn();
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedState
);
await waitFor(() => {
const editButton = screen.queryByTestId("EditIcon");
expect(editButton).not.toBeInTheDocument();
});
});
it("should handle calendars without ownerEmails property gracefully", async () => {
const preloadedStateWithoutOwnerEmails = {
user: {
userData: {
sub: "test",
email: "test@test.com",
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
},
tokens: {
accessToken: "token",
},
},
calendars: {
list: {
"667037022b752d0026472254/cal1": {
name: "Calendar 1",
id: "667037022b752d0026472254/cal1",
color: "#FF0000",
// ownerEmails missing
events: {
event1: {
id: "event1",
calId: "667037022b752d0026472254/cal1",
uid: "event1",
title: "Test Event",
start: start.toISOString(),
end: end.toISOString(),
organizer: {
cn: "Test",
cal_address: "test@test.com",
},
},
},
},
},
pending: false,
},
};
const mockOnClose = jest.fn();
expect(() => {
renderWithProviders(
<EventPreviewModal
eventId="event1"
calId="667037022b752d0026472254/cal1"
open={true}
onClose={mockOnClose}
/>,
preloadedStateWithoutOwnerEmails
);
}).not.toThrow();
});
});
});
describe("Event Full Display", () => {
@@ -0,0 +1,826 @@
import { jest } from "@jest/globals";
import { ThunkDispatch } from "@reduxjs/toolkit";
import "@testing-library/jest-dom";
import { screen, waitFor, fireEvent } from "@testing-library/react";
import * as appHooks from "../../../src/app/hooks";
import * as eventUtils from "../../../src/components/Event/utils/eventUtils";
import * as userApi from "../../../src/features/User/userAPI";
import * as calendarUtils from "../../../src/components/Calendar/utils/calendarUtils";
import * as eventThunks from "../../../src/features/Calendars/CalendarSlice";
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
import EventPopover from "../../../src/features/Events/EventModal";
import EventUpdateModal from "../../../src/features/Events/EventUpdateModal";
import CalendarLayout from "../../../src/components/Calendar/CalendarLayout";
import { renderWithProviders } from "../../utils/Renderwithproviders";
import { SpiedFunction } from "jest-mock";
import { Calendars } from "../../../src/features/Calendars/CalendarTypes";
import { CalendarEvent } from "../../../src/features/Events/EventsTypes";
import { DateSelectArg } from "@fullcalendar/core";
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: Calendars[],
calendarRange: { start: Date; end: Date },
calType?: "temp"
) => Promise<void>
>;
let refreshSingularCalendarSpy: SpiedFunction<
(
dispatch: ThunkDispatch<any, any, any>,
calendar: Calendars,
calendarRange: { start: Date; end: Date },
calType?: "temp"
) => Promise<void>
>;
let updateTempCalendarSpy: SpiedFunction<
(
tempcalendars: Record<string, Calendars>,
event: CalendarEvent,
dispatch: ThunkDispatch<any, any, any>,
calendarRange: { start: Date; end: Date }
) => Promise<void>
>;
beforeEach(() => {
jest.clearAllMocks();
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
refreshCalendarsSpy = jest.spyOn(eventUtils, "refreshCalendars");
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("Delete event")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("Delete event");
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("Delete event")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("Delete event");
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");
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(
/Start typing a name or email/i
);
fireEvent.change(attendeeInput, { target: { value: "attendee@test.com" } });
fireEvent.keyDown(attendeeInput, { key: "Enter", code: "Enter" });
const saveButton = screen.getByText(/save/i);
fireEvent.click(saveButton);
await waitFor(() =>
expect(updateTempCalendarSpy).toHaveBeenCalledWith(
expect.objectContaining({
temp1: expect.objectContaining({ id: "temp1" }),
}),
expect.objectContaining({
title: "New Event",
}),
expect.any(Function),
expect.objectContaining({
start: expect.any(Date),
end: expect.any(Date),
})
)
);
await waitFor(() =>
expect(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("Delete event")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("Delete event");
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(eventThunks, "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("Delete event")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("Delete event");
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("Delete event")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("Delete event");
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("Delete event")).toBeInTheDocument();
});
const deleteMenuItem = screen.getByText("Delete event");
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");
});
});
});
@@ -13,6 +13,7 @@ import {
import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
import { getEvent } from "../../../features/Events/EventApi";
import { refreshCalendars } from "../../Event/utils/eventUtils";
import { updateTempCalendar } from "../utils/calendarUtils";
export interface EventHandlersProps {
setSelectedRange: (range: DateSelectArg | null) => void;
@@ -159,14 +160,12 @@ export const createEventHandlers = (props: EventHandlersProps) => {
Object.values(calendars),
calendarRange
);
if (tempcalendars) {
await refreshCalendars(
dispatch,
Object.values(tempcalendars),
calendarRange,
"temp"
);
}
await updateTempCalendar(
tempcalendars,
event,
dispatch,
calendarRange
);
}
}
);
@@ -176,14 +175,7 @@ export const createEventHandlers = (props: EventHandlersProps) => {
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
);
}
if (tempcalendars) {
await refreshCalendars(
dispatch,
Object.values(tempcalendars),
calendarRange,
"temp"
);
}
await updateTempCalendar(tempcalendars, event, dispatch, calendarRange);
};
const handleEventResize = async (arg: any) => {
@@ -243,14 +235,12 @@ export const createEventHandlers = (props: EventHandlersProps) => {
Object.values(calendars),
calendarRange
);
if (tempcalendars) {
await refreshCalendars(
dispatch,
Object.values(tempcalendars),
calendarRange,
"temp"
);
}
await updateTempCalendar(
tempcalendars,
event,
dispatch,
calendarRange
);
}
}
);
@@ -259,14 +249,7 @@ export const createEventHandlers = (props: EventHandlersProps) => {
putEventAsync({ cal: calendars[newEvent.calId], newEvent })
);
}
if (tempcalendars) {
await refreshCalendars(
dispatch,
Object.values(tempcalendars),
calendarRange,
"temp"
);
}
await updateTempCalendar(tempcalendars, event, dispatch, calendarRange);
};
return {
@@ -4,6 +4,8 @@ import { formatDateToYYYYMMDDTHHMMSS } from "../../../utils/dateUtils";
import { getCalendarDetailAsync } from "../../../features/Calendars/CalendarSlice";
import { SlotLabelContentArg } from "@fullcalendar/core";
import moment from "moment-timezone";
import { refreshSingularCalendar } from "../../Event/utils/eventUtils";
import { ThunkDispatch } from "@reduxjs/toolkit";
export const updateSlotLabelVisibility = (
currentTime: Date,
@@ -154,3 +156,35 @@ export function getCalendarVisibility(acl: AclEntry[]): "private" | "public" {
if (hasRead) return "public";
return "private";
}
export async function updateTempCalendar(
tempcalendars: Record<string, Calendars>,
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"
);
}
}
}
}
+22 -2
View File
@@ -6,7 +6,7 @@ import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { ThunkDispatch } from "@reduxjs/toolkit";
import {
emptyTempCal,
emptyEventsCal,
getCalendarDetailAsync,
getCalendarsListAsync,
} from "../../../features/Calendars/CalendarSlice";
@@ -119,7 +119,7 @@ export async function refreshCalendars(
calType?: "temp"
) {
!calType && (await dispatch(getCalendarsListAsync()));
calType && dispatch(emptyTempCal());
calType && dispatch(emptyEventsCal({ calType }));
calendars.map(
async (cal) =>
@@ -135,3 +135,23 @@ export async function refreshCalendars(
)
);
}
export async function refreshSingularCalendar(
dispatch: ThunkDispatch<any, any, any>,
calendar: Calendars,
calendarRange: { start: Date; end: Date },
calType?: "temp"
) {
dispatch(emptyEventsCal({ calId: calendar.id, calType }));
await dispatch(
getCalendarDetailAsync({
calId: calendar.id,
match: {
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
},
calType,
})
);
}
+14 -5
View File
@@ -422,10 +422,19 @@ const CalendarSlice = createSlice({
removeTempCal: (state, action: PayloadAction<string>) => {
delete state.templist[action.payload];
},
emptyTempCal: (state) => {
Object.keys(state.templist).forEach(
(calId) => (state.templist[calId].events = {})
);
emptyEventsCal: (
state,
action: PayloadAction<{ calId?: string; calType?: "temp" }>
) => {
const cals =
action.payload.calType === "temp" ? state.templist : state.list;
if (action.payload.calId) {
cals[action.payload.calId].events = {};
} else {
Object.keys(state.templist).forEach(
(calId) => (cals[calId].events = {})
);
}
},
updateEventLocal: (
state,
@@ -707,7 +716,7 @@ export const {
createCalendar,
updateEventLocal,
removeTempCal,
emptyTempCal,
emptyEventsCal,
setTimeZone,
clearFetchCache,
} = CalendarSlice.actions;
+7 -9
View File
@@ -36,12 +36,10 @@ import {
handleRSVP,
} from "../../components/Event/eventHandlers/eventHandlers";
import { InfoRow } from "../../components/Event/InfoRow";
import {
refreshCalendars,
renderAttendeeBadge,
} from "../../components/Event/utils/eventUtils";
import { renderAttendeeBadge } from "../../components/Event/utils/eventUtils";
import { getTimezoneOffset } from "../../components/Calendar/TimezoneSelector";
import { getCalendarRange } from "../../utils/dateUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
export default function EventPreviewModal({
eventId,
calId,
@@ -114,14 +112,14 @@ export default function EventPreviewModal({
(a) => a.cal_address === event.organizer?.cal_address
);
const updateTempList = () => {
const updateTempList = async () => {
if (calendars.templist) {
const calendarRange = getCalendarRange(new Date(event.start));
refreshCalendars(
await updateTempCalendar(
calendars.templist,
event,
dispatch,
Object.values(calendars.templist),
calendarRange,
"temp"
calendarRange
);
}
};
+2 -7
View File
@@ -49,7 +49,7 @@ import {
resolveTimezone,
} from "../../components/Calendar/TimezoneSelector";
import { getCalendarRange } from "../../utils/dateUtils";
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
// Helper component for field with label
const FieldWithLabel = React.memo(
@@ -409,12 +409,7 @@ function EventPopover({
);
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
refreshCalendars(
dispatch,
Object.values(tempList),
calendarRange,
"temp"
);
await updateTempCalendar(tempList, newEvent, dispatch, calendarRange);
}
};
+3 -12
View File
@@ -27,6 +27,7 @@ import {
combineMasterDateWithFormTime,
detectRecurringEventChanges,
} from "./eventUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
const showErrorNotification = (message: string) => {
console.error(`[ERROR] ${message}`);
@@ -478,12 +479,7 @@ function EventUpdateModal({
}
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
refreshCalendars(
dispatch,
Object.values(tempList),
calendarRange,
"temp"
);
await updateTempCalendar(tempList, event, dispatch, calendarRange);
}
return;
}
@@ -658,12 +654,7 @@ function EventUpdateModal({
}
if (tempList) {
const calendarRange = getCalendarRange(new Date(start));
refreshCalendars(
dispatch,
Object.values(tempList),
calendarRange,
"temp"
);
await updateTempCalendar(tempList, event, dispatch, calendarRange);
}
};