Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -28,8 +28,6 @@ describe("CalendarApp integration", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const dispatch = jest.fn() as AppDispatch;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
});
|
||||
|
||||
const renderCalendar = () => {
|
||||
@@ -89,7 +87,8 @@ describe("CalendarApp integration", () => {
|
||||
};
|
||||
|
||||
it("renders the event on the calendar and calendarRef works", async () => {
|
||||
const dispatch = appHooks.useAppDispatch();
|
||||
const dispatch = jest.fn() as AppDispatch;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
|
||||
renderCalendar();
|
||||
const calendarRef: React.RefObject<CalendarApi | null> = (window as any)
|
||||
@@ -334,7 +333,13 @@ describe("CalendarApp integration", () => {
|
||||
};
|
||||
|
||||
it("keeps all attendees event participation on title update", async () => {
|
||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
||||
const updateSpy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const onClose = jest.fn();
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -387,7 +392,13 @@ describe("CalendarApp integration", () => {
|
||||
});
|
||||
|
||||
it("changes normal attendee to need action on time update and no organizer changes", async () => {
|
||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
||||
const updateSpy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const onClose = jest.fn();
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
@@ -450,9 +461,22 @@ describe("CalendarApp integration", () => {
|
||||
// Verify SEQUENCE is incremented
|
||||
expect(updatedEvent?.sequence).toBe(3); // 2 + 1
|
||||
});
|
||||
|
||||
it("update event attendees on drag", async () => {
|
||||
// Mock dispatch locally — this test calls createEventHandlers directly
|
||||
// and does not go through the Redux store or useAppDispatch.
|
||||
const mockDispatch = jest.fn();
|
||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
||||
jest
|
||||
.spyOn(appHooks, "useAppDispatch")
|
||||
.mockReturnValue(mockDispatch as unknown as AppDispatch);
|
||||
|
||||
const updateSpy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const eventHandlers = createEventHandlers({
|
||||
setSelectedRange: jest.fn(),
|
||||
@@ -513,8 +537,20 @@ describe("CalendarApp integration", () => {
|
||||
});
|
||||
|
||||
it("update event attendees on resize", async () => {
|
||||
// Mock dispatch locally — this test calls createEventHandlers directly
|
||||
// and does not go through the Redux store or useAppDispatch.
|
||||
const mockDispatch = jest.fn();
|
||||
const updateSpy = jest.spyOn(eventThunks, "putEventAsync");
|
||||
jest
|
||||
.spyOn(appHooks, "useAppDispatch")
|
||||
.mockReturnValue(mockDispatch as unknown as AppDispatch);
|
||||
|
||||
const updateSpy = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const eventHandlers = createEventHandlers({
|
||||
setSelectedRange: jest.fn(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as appHooks from "@/app/hooks";
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { InfoRow } from "@/components/Event/InfoRow";
|
||||
import { LONG_DATE_FORMAT } from "@/components/Event/utils/dateTimeFormatters";
|
||||
import {
|
||||
stringAvatar,
|
||||
stringToColor,
|
||||
@@ -9,15 +8,7 @@ import {
|
||||
import { DelegationAccess } from "@/features/Calendars/CalendarTypes";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventPreviewModal from "@/features/Events/EventDisplayPreview";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import dayjs from "dayjs";
|
||||
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
|
||||
describe("Event Preview Display", () => {
|
||||
@@ -1752,422 +1743,6 @@ describe("Event Preview Display", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Event Full Display", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
const day = new Date("2025-01-15T10:00:00.000Z"); // Fixed date in UTC: Jan 15, 2025 10:00 AM UTC
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "mailto:test@test.com",
|
||||
},
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "First Calendar",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: day.toISOString(),
|
||||
end: day.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [
|
||||
{
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
{
|
||||
cn: "John",
|
||||
cal_address: "john@test.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"otherCal/cal": {
|
||||
id: "otherCal/cal",
|
||||
name: "Calendar 1",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
calId: "otherCal/cal",
|
||||
title: "Test Event Other cal",
|
||||
start: day.toISOString(),
|
||||
end: day.toISOString(),
|
||||
organizer: { cn: "john", cal_address: "john@test.com" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
|
||||
it("renders correctly event data with fixed timezone", () => {
|
||||
// Use fixed timezone UTC for consistent test results across all environments
|
||||
const fixedDate = new Date("2025-01-15T10:00:00.000Z"); // 10AM UTC
|
||||
const endDate = new Date(fixedDate.getTime() + 3600000); // 11AM UTC
|
||||
|
||||
// With UTC timezone set, formatLocalDateTime produces predictable values
|
||||
const expectedStart = "2025-01-15T10:00"; // 10:00 AM UTC
|
||||
const expectedEnd = "2025-01-15T11:00"; // 11:00 AM UTC
|
||||
|
||||
const stateWithFixedDate = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
start: fixedDate.toISOString(),
|
||||
end: endDate.toISOString(),
|
||||
timezone: "UTC",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
stateWithFixedDate
|
||||
);
|
||||
|
||||
expect(screen.getByDisplayValue("Test Event")).toBeInTheDocument();
|
||||
|
||||
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const endTimeInput = screen.getByTestId("end-time-input");
|
||||
|
||||
expect(startDateInput).toBeInTheDocument();
|
||||
expect(startTimeInput).toBeInTheDocument();
|
||||
expect(endTimeInput).toBeInTheDocument();
|
||||
|
||||
expect(startDateInput).toHaveValue(
|
||||
dayjs(expectedStart).format(LONG_DATE_FORMAT)
|
||||
);
|
||||
expect(startTimeInput).toHaveValue(dayjs(expectedStart).format("HH:mm"));
|
||||
|
||||
expect(endTimeInput).toHaveValue(dayjs(expectedEnd).format("HH:mm"));
|
||||
|
||||
expect(screen.getByText("First Calendar")).toBeInTheDocument();
|
||||
});
|
||||
it("calls onClose when Cancel clicked", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
fireEvent.click(screen.getAllByTestId("CloseIcon")[0]);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
it("toggle Show More reveals extra fields", async () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
act(() => {
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("event.form.notification")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Debug: Print DOM to see what's rendered
|
||||
console.log("DOM after Show More clicked:", document.body.innerHTML);
|
||||
|
||||
// EventDisplay modal doesn't have Repeat checkbox, only RepeatEvent component
|
||||
// which shows repetition settings when repetition data exists
|
||||
// Since test event has no repetition data, RepeatEvent component won't show Repeat checkbox
|
||||
fireEvent.click(screen.getByRole("button", { name: /Show Less/i }));
|
||||
});
|
||||
|
||||
it("can edit title when user is organizer", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
const titleField = screen.getByLabelText("event.form.title");
|
||||
fireEvent.change(titleField, { target: { value: "New Title" } });
|
||||
expect(screen.getByDisplayValue("New Title")).toBeInTheDocument();
|
||||
});
|
||||
it("toggle all-day updates end date correctly", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
fireEvent.click(allDayCheckbox);
|
||||
expect(allDayCheckbox).toBeChecked();
|
||||
|
||||
const expectedDate = dayjs(day).format(LONG_DATE_FORMAT);
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const endDateInput = screen.getByTestId("end-date-input");
|
||||
|
||||
expect(startDateInput).toHaveValue(expectedDate);
|
||||
expect(endDateInput).toHaveValue(expectedDate);
|
||||
});
|
||||
|
||||
it("saves event and moves it when calendar is changed", async () => {
|
||||
const spyPut = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const spyMove = jest
|
||||
.spyOn(eventThunks, "moveEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
||||
const preloadedTwoCals = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Calendar One",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: testDate.toISOString(),
|
||||
end: testEndDate.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
"667037022b752d0026472254/cal2": {
|
||||
id: "667037022b752d0026472254/cal2",
|
||||
name: "Calendar Two",
|
||||
color: "#00FF00",
|
||||
events: {},
|
||||
},
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedTwoCals
|
||||
)
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||
|
||||
const option = await screen.findByText("Calendar Two");
|
||||
fireEvent.click(option);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyPut).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyMove).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
|
||||
it("edit modal displays event time in original event timezone", () => {
|
||||
// GIVEN user timezone is UTC+2
|
||||
// WHEN the user edits an event at 2PM UTC+7 (Asia/Bangkok)
|
||||
// THEN the update modal displays the time as 2PM in Asia/Bangkok timezone
|
||||
|
||||
const eventDateUTC7 = new Date("2025-01-15T07:00:00.000Z"); // 7AM UTC = 2PM UTC+7
|
||||
|
||||
const stateWithTimezone = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Test Calendar",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Timezone Edit Test",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: eventDateUTC7.toISOString(),
|
||||
end: new Date(eventDateUTC7.getTime() + 3600000).toISOString(),
|
||||
timezone: "Asia/Bangkok",
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
stateWithTimezone
|
||||
);
|
||||
|
||||
// Expand to show timezone selector (normal mode hides it)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
// The timezone select should have Asia/Bangkok selected
|
||||
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i);
|
||||
expect(timeZone).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("InfoRow renders error style when error prop is true", () => {
|
||||
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
|
||||
expect(screen.getByText("Bad")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can remove an attendee with the close button", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
|
||||
const removeBtn = screen.getAllByTestId("CloseIcon").pop()!;
|
||||
fireEvent.click(removeBtn);
|
||||
|
||||
expect(screen.queryByText(/John/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders video conference info when x_openpass_videoconference exists", () => {
|
||||
const videoState = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
x_openpass_videoconference: "https://meet.test/video",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
videoState
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("event.form.joinVisioConference")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Helper functions", () => {
|
||||
it("stringToColor generates consistent color", () => {
|
||||
expect(stringToColor("Alice")).toMatch(/^#[0-9a-f]{6}$/);
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
|
||||
describe("Event Full Display — delegated calendar move", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
||||
|
||||
const baseUserState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "user1",
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const preloadedDelegatedCals = {
|
||||
...baseUserState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"user1/cal1": {
|
||||
id: "user1/cal1",
|
||||
name: "Calendar One",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "user1/cal1",
|
||||
start: testDate.toISOString(),
|
||||
end: testEndDate.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
URL: "/calendar/user1/cal1/event1.ics",
|
||||
},
|
||||
},
|
||||
link: "/calendar/user1/cal1.json",
|
||||
},
|
||||
"user2/cal2": {
|
||||
id: "user2/cal2",
|
||||
name: "Delegated Calendar",
|
||||
color: "#00FF00",
|
||||
delegated: true,
|
||||
owner: {
|
||||
emails: ["delegate@test.com"],
|
||||
name: "Delegate User",
|
||||
},
|
||||
events: {},
|
||||
link: "/calendar/user2/cal2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("calls deleteEventAsync and putEventAsync — and NOT moveEventAsync — when moving to a delegated calendar", async () => {
|
||||
const spyPut = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const spyDelete = jest
|
||||
.spyOn(eventThunks, "deleteEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const spyMove = jest
|
||||
.spyOn(eventThunks, "moveEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"user1/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedDelegatedCals
|
||||
)
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
|
||||
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||
const option = await screen.findByText("Delegated Calendar");
|
||||
fireEvent.click(option);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
});
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyPut).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyDelete).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
expect(spyMove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,422 @@
|
||||
import { InfoRow } from "@/components/Event/InfoRow";
|
||||
import { LONG_DATE_FORMAT } from "@/components/Event/utils/dateTimeFormatters";
|
||||
import * as eventThunks from "@/features/Calendars/services";
|
||||
import EventUpdateModal from "@/features/Events/EventUpdateModal";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import dayjs from "dayjs";
|
||||
import { renderWithProviders } from "../../utils/Renderwithproviders";
|
||||
|
||||
describe("Event Full Display", () => {
|
||||
const mockOnClose = jest.fn();
|
||||
const day = new Date("2025-01-15T10:00:00.000Z"); // Fixed date in UTC: Jan 15, 2025 10:00 AM UTC
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const preloadedState = {
|
||||
user: {
|
||||
userData: {
|
||||
sub: "test",
|
||||
email: "test@test.com",
|
||||
sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
|
||||
openpaasId: "667037022b752d0026472254",
|
||||
},
|
||||
organiserData: {
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
},
|
||||
},
|
||||
calendars: {
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "First Calendar",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: day.toISOString(),
|
||||
end: day.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [
|
||||
{
|
||||
cn: "test",
|
||||
cal_address: "test@test.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
{
|
||||
cn: "John",
|
||||
cal_address: "john@test.com",
|
||||
partstat: "NEEDS-ACTION",
|
||||
rsvp: "TRUE",
|
||||
role: "REQ-PARTICIPANT",
|
||||
cutype: "INDIVIDUAL",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"otherCal/cal": {
|
||||
id: "otherCal/cal",
|
||||
name: "Calendar 1",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
calId: "otherCal/cal",
|
||||
title: "Test Event Other cal",
|
||||
start: day.toISOString(),
|
||||
end: day.toISOString(),
|
||||
organizer: { cn: "john", cal_address: "john@test.com" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("renders correctly event data with fixed timezone", () => {
|
||||
// Use fixed timezone UTC for consistent test results across all environments
|
||||
const fixedDate = new Date("2025-01-15T10:00:00.000Z"); // 10AM UTC
|
||||
const endDate = new Date(fixedDate.getTime() + 3600000); // 11AM UTC
|
||||
|
||||
// With UTC timezone set, formatLocalDateTime produces predictable values
|
||||
const expectedStart = "2025-01-15T10:00"; // 10:00 AM UTC
|
||||
const expectedEnd = "2025-01-15T11:00"; // 11:00 AM UTC
|
||||
|
||||
const stateWithFixedDate = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
start: fixedDate.toISOString(),
|
||||
end: endDate.toISOString(),
|
||||
timezone: "UTC",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
stateWithFixedDate
|
||||
);
|
||||
|
||||
expect(screen.getByDisplayValue("Test Event")).toBeInTheDocument();
|
||||
|
||||
// Expand to show date/time inputs (normal mode shows DateTimeSummary)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const startTimeInput = screen.getByTestId("start-time-input");
|
||||
const endTimeInput = screen.getByTestId("end-time-input");
|
||||
|
||||
expect(startDateInput).toBeInTheDocument();
|
||||
expect(startTimeInput).toBeInTheDocument();
|
||||
expect(endTimeInput).toBeInTheDocument();
|
||||
|
||||
expect(startDateInput).toHaveValue(
|
||||
dayjs(expectedStart).format(LONG_DATE_FORMAT)
|
||||
);
|
||||
expect(startTimeInput).toHaveValue(dayjs(expectedStart).format("HH:mm"));
|
||||
|
||||
expect(endTimeInput).toHaveValue(dayjs(expectedEnd).format("HH:mm"));
|
||||
|
||||
expect(screen.getByText("First Calendar")).toBeInTheDocument();
|
||||
});
|
||||
it("calls onClose when Cancel clicked", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
fireEvent.click(screen.getAllByTestId("CloseIcon")[0]);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
|
||||
});
|
||||
it("toggle Show More reveals extra fields", async () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
act(() => {
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "common.moreOptions" })
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("event.form.notification")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// EventDisplay modal doesn't have Repeat checkbox, only RepeatEvent component
|
||||
// which shows repetition settings when repetition data exists
|
||||
// Since test event has no repetition data, RepeatEvent component won't show Repeat checkbox
|
||||
fireEvent.click(screen.getByRole("button", { name: /Show Less/i }));
|
||||
});
|
||||
|
||||
it("can edit title when user is organizer", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
const titleField = screen.getByLabelText("event.form.title");
|
||||
fireEvent.change(titleField, { target: { value: "New Title" } });
|
||||
expect(screen.getByDisplayValue("New Title")).toBeInTheDocument();
|
||||
});
|
||||
it("toggle all-day updates end date correctly", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
const allDayCheckbox = screen.getByLabelText("event.form.allDay");
|
||||
fireEvent.click(allDayCheckbox);
|
||||
expect(allDayCheckbox).toBeChecked();
|
||||
|
||||
const expectedDate = dayjs(day).format(LONG_DATE_FORMAT);
|
||||
|
||||
const startDateInput = screen.getByTestId("start-date-input");
|
||||
const endDateInput = screen.getByTestId("end-date-input");
|
||||
|
||||
expect(startDateInput).toHaveValue(expectedDate);
|
||||
expect(endDateInput).toHaveValue(expectedDate);
|
||||
});
|
||||
|
||||
it("saves event and moves it when calendar is changed", async () => {
|
||||
const spyPut = jest
|
||||
.spyOn(eventThunks, "putEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
const spyMove = jest
|
||||
.spyOn(eventThunks, "moveEventAsync")
|
||||
.mockImplementation((payload) => {
|
||||
const promise = Promise.resolve(payload);
|
||||
(promise as any).unwrap = () => promise;
|
||||
return () => promise as any;
|
||||
});
|
||||
|
||||
const testDate = new Date("2025-01-15T10:00:00.000Z");
|
||||
const testEndDate = new Date("2025-01-15T11:00:00.000Z");
|
||||
const preloadedTwoCals = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Calendar One",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Test Event",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: testDate.toISOString(),
|
||||
end: testEndDate.toISOString(),
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
"667037022b752d0026472254/cal2": {
|
||||
id: "667037022b752d0026472254/cal2",
|
||||
name: "Calendar Two",
|
||||
color: "#00FF00",
|
||||
events: {},
|
||||
},
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
|
||||
await act(async () =>
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedTwoCals
|
||||
)
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
const calendarSelect = screen.getByLabelText("event.form.calendar");
|
||||
await act(async () => fireEvent.mouseDown(calendarSelect));
|
||||
|
||||
const option = await screen.findByText("Calendar Two");
|
||||
fireEvent.click(option);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "actions.save" }));
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyPut).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(spyMove).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
|
||||
it("edit modal displays event time in original event timezone", () => {
|
||||
// GIVEN user timezone is UTC+2
|
||||
// WHEN the user edits an event at 2PM UTC+7 (Asia/Bangkok)
|
||||
// THEN the update modal displays the time as 2PM in Asia/Bangkok timezone
|
||||
const eventDateUTC7 = new Date("2025-01-15T07:00:00.000Z"); // 7AM UTC = 2PM UTC+7
|
||||
|
||||
const stateWithTimezone = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
id: "667037022b752d0026472254/cal1",
|
||||
name: "Test Calendar",
|
||||
color: "#FF0000",
|
||||
events: {
|
||||
event1: {
|
||||
uid: "event1",
|
||||
title: "Timezone Edit Test",
|
||||
calId: "667037022b752d0026472254/cal1",
|
||||
start: eventDateUTC7.toISOString(),
|
||||
end: new Date(eventDateUTC7.getTime() + 3600000).toISOString(),
|
||||
timezone: "Asia/Bangkok",
|
||||
allday: false,
|
||||
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||
attendee: [{ cn: "test", cal_address: "test@test.com" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
stateWithTimezone
|
||||
);
|
||||
|
||||
// Expand to show timezone selector (normal mode hides it)
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.moreOptions" }));
|
||||
// The timezone select should have Asia/Bangkok selected
|
||||
const timeZone = screen.getByDisplayValue(/Asia\/Bangkok/i);
|
||||
expect(timeZone).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("InfoRow renders error style when error prop is true", () => {
|
||||
renderWithProviders(<InfoRow icon={<span>i</span>} text="Bad" error />);
|
||||
expect(screen.getByText("Bad")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can remove an attendee with the close button", () => {
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
|
||||
const removeBtn = screen.getAllByTestId("CloseIcon").pop()!;
|
||||
fireEvent.click(removeBtn);
|
||||
|
||||
expect(screen.queryByText(/John/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders video conference info when x_openpass_videoconference exists", () => {
|
||||
const videoState = {
|
||||
...preloadedState,
|
||||
calendars: {
|
||||
pending: false,
|
||||
list: {
|
||||
"667037022b752d0026472254/cal1": {
|
||||
...preloadedState.calendars.list["667037022b752d0026472254/cal1"],
|
||||
events: {
|
||||
event1: {
|
||||
...preloadedState.calendars.list[
|
||||
"667037022b752d0026472254/cal1"
|
||||
].events.event1,
|
||||
x_openpass_videoconference: "https://meet.test/video",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventUpdateModal
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
videoState
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("event.form.joinVisioConference")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,6 @@ import { addDays } from "@/components/Event/utils/dateRules";
|
||||
import { formatDateTimeInTimezone } from "@/components/Event/utils/dateTimeFormatters";
|
||||
import { convertFormDateTimeToISO } from "@/components/Event/utils/dateTimeHelpers";
|
||||
import {
|
||||
moveEventAsync,
|
||||
putEventAsync,
|
||||
updateEventInstanceAsync,
|
||||
updateSeriesAsync,
|
||||
@@ -45,6 +44,7 @@ import { userAttendee } from "../User/models/attendee";
|
||||
import { deleteEvent, getEvent, putEvent } from "./EventApi";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { detectRecurringEventChanges } from "./eventUtils";
|
||||
import { moveEventBetweenCalendars } from "./updateEventHelpers/moveEventBetweenCalendars";
|
||||
|
||||
function EventUpdateModal({
|
||||
eventId,
|
||||
@@ -79,14 +79,12 @@ function EventUpdateModal({
|
||||
// if the event's calendar is delegated then it shall be the only calendar accessible from the event update modal
|
||||
const userPersonalCalendars: Calendar[] = useMemo(() => {
|
||||
const allCalendars = Object.values(calList) as Calendar[];
|
||||
if (calList[calId]?.delegated) {
|
||||
return [calList[calId]];
|
||||
}
|
||||
return allCalendars.filter(
|
||||
(calendar: Calendar) =>
|
||||
calendar.id?.split("/")[0] === user.userData?.openpaasId
|
||||
calendar.id?.split("/")[0] === user.userData?.openpaasId ||
|
||||
calendar.delegated
|
||||
);
|
||||
}, [calList, calId, user.userData?.openpaasId]);
|
||||
}, [calList, user.userData?.openpaasId]);
|
||||
|
||||
const timezoneList = useMemo(() => {
|
||||
const zones = Object.keys(TIMEZONES.zones).sort();
|
||||
@@ -947,7 +945,7 @@ function EventUpdateModal({
|
||||
);
|
||||
|
||||
// Handle result of updateSeriesAsync
|
||||
assertThunkSuccess(result);
|
||||
await assertThunkSuccess(result);
|
||||
|
||||
// Clear cache to ensure navigation shows updated data
|
||||
dispatch(clearFetchCache(calId));
|
||||
@@ -969,7 +967,7 @@ function EventUpdateModal({
|
||||
);
|
||||
|
||||
// Handle result of putEventAsync - check if rejected first
|
||||
assertThunkSuccess(result);
|
||||
await assertThunkSuccess(result);
|
||||
|
||||
// Remove old single event AFTER new recurring instances are added to store
|
||||
// This prevents empty grid during the transition
|
||||
@@ -980,68 +978,30 @@ function EventUpdateModal({
|
||||
|
||||
// Clear temp data on successful save
|
||||
clearEventFormTempData("update");
|
||||
} else {
|
||||
// Normal non-recurring event update
|
||||
// If calendar is changing, we'll handle it separately with moveEventAsync
|
||||
// So only call putEventAsync if calendar is NOT changing
|
||||
if (newCalId === calId) {
|
||||
const result = await dispatch(
|
||||
putEventAsync({ cal: targetCalendar, newEvent })
|
||||
);
|
||||
} else if (newCalId === calId) {
|
||||
// Normal non-recurring event update (same calendar)
|
||||
const result = await dispatch(
|
||||
putEventAsync({ cal: targetCalendar, newEvent })
|
||||
);
|
||||
|
||||
// Handle result of putEventAsync - check if rejected first
|
||||
const typedResult = result as AsyncThunkResult;
|
||||
if (typedResult.type && typedResult.type.endsWith("/rejected")) {
|
||||
throw new Error(
|
||||
typedResult.error?.message ||
|
||||
typedResult.payload?.message ||
|
||||
"API call failed"
|
||||
);
|
||||
}
|
||||
if (typedResult && typeof typedResult.unwrap === "function") {
|
||||
await typedResult.unwrap();
|
||||
}
|
||||
await assertThunkSuccess(result);
|
||||
|
||||
// Clear temp data on successful save
|
||||
clearEventFormTempData("update");
|
||||
}
|
||||
// Clear temp data on successful save
|
||||
clearEventFormTempData("update");
|
||||
}
|
||||
// Note: when newCalId !== calId, the move is handled below in the
|
||||
// "Handle calendar change" block, so we intentionally skip putEventAsync here.
|
||||
}
|
||||
|
||||
// Handle calendar change
|
||||
// Handle calendar change (move to a different calendar)
|
||||
if (newCalId !== calId) {
|
||||
// Get the old calendar for updating
|
||||
const oldCalendar = calList[calId];
|
||||
if (!oldCalendar) {
|
||||
console.error("Old calendar not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// First update the event in the old calendar, then move it
|
||||
const putResult = await dispatch(
|
||||
putEventAsync({ cal: oldCalendar, newEvent: { ...newEvent, calId } })
|
||||
);
|
||||
|
||||
// Handle result of putEventAsync
|
||||
const typedPutResult = putResult as AsyncThunkResult;
|
||||
if (typedPutResult && typeof typedPutResult.unwrap === "function") {
|
||||
await typedPutResult.unwrap();
|
||||
}
|
||||
|
||||
// Then move it to the new calendar
|
||||
const moveResult = await dispatch(
|
||||
moveEventAsync({
|
||||
cal: targetCalendar,
|
||||
newEvent,
|
||||
newURL: `/calendars/${newCalId}/${extractEventBaseUuid(event.uid)}.ics`,
|
||||
})
|
||||
);
|
||||
|
||||
// Handle result of moveEventAsync
|
||||
const typedMoveResult = moveResult as AsyncThunkResult;
|
||||
if (typedMoveResult && typeof typedMoveResult.unwrap === "function") {
|
||||
await typedMoveResult.unwrap();
|
||||
}
|
||||
await moveEventBetweenCalendars({
|
||||
dispatch,
|
||||
calList,
|
||||
newEvent,
|
||||
oldCalId: calId,
|
||||
newCalId,
|
||||
});
|
||||
|
||||
// Clear temp data on successful move
|
||||
clearEventFormTempData("update");
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { AppDispatch } from "@/app/store";
|
||||
import { Calendar } from "@/features/Calendars/CalendarTypes";
|
||||
import {
|
||||
deleteEventAsync,
|
||||
moveEventAsync,
|
||||
putEventAsync,
|
||||
} from "@/features/Calendars/services";
|
||||
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
|
||||
import { userAttendee } from "@/features/User/models/attendee";
|
||||
import { userOrganiser } from "@/features/User/userDataTypes";
|
||||
import { assertThunkSuccess } from "@/utils/assertThunkSuccess";
|
||||
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
|
||||
import { makeDisplayName } from "@/utils/makeDisplayName";
|
||||
import { CalendarEvent } from "../EventsTypes";
|
||||
import { buildDelegatedEventURL } from "../eventUtils";
|
||||
|
||||
export interface MoveEventBetweenCalendarsParams {
|
||||
dispatch: AppDispatch;
|
||||
calList: Record<string, Calendar>;
|
||||
newEvent: CalendarEvent;
|
||||
oldCalId: string;
|
||||
newCalId: string;
|
||||
}
|
||||
|
||||
function resolveOrganizerForCalendar(
|
||||
calendar: Calendar,
|
||||
originalOrganizer: CalendarEvent["organizer"]
|
||||
): CalendarEvent["organizer"] {
|
||||
const ownerEmail = calendar.owner?.emails?.[0];
|
||||
if (!ownerEmail) {
|
||||
return originalOrganizer;
|
||||
}
|
||||
|
||||
return {
|
||||
...originalOrganizer,
|
||||
cal_address: ownerEmail,
|
||||
cn: makeDisplayName(calendar) ?? originalOrganizer?.cn ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function rewriteAttendeesForOrganizerChange(
|
||||
attendees: userAttendee[],
|
||||
oldOrganizer?: userOrganiser,
|
||||
newOrganizer?: userOrganiser
|
||||
): userAttendee[] {
|
||||
if (!newOrganizer) {
|
||||
return attendees;
|
||||
}
|
||||
const normalise = (addr: string | undefined) => (addr ?? "").toLowerCase();
|
||||
const oldAddr = normalise(oldOrganizer?.cal_address);
|
||||
const newAddr = normalise(newOrganizer.cal_address);
|
||||
|
||||
// Remove the old organizer from the attendee list if there is one
|
||||
const filtered = attendees.filter(
|
||||
(a) => normalise(a.cal_address) !== oldAddr
|
||||
);
|
||||
|
||||
// Add the new organizer as CHAIR if they are not already listed
|
||||
const alreadyPresent = filtered.some(
|
||||
(a) => normalise(a.cal_address) === newAddr
|
||||
);
|
||||
|
||||
if (!alreadyPresent && newAddr) {
|
||||
filtered.push({
|
||||
cal_address: newOrganizer.cal_address,
|
||||
partstat: "ACCEPTED",
|
||||
role: "CHAIR",
|
||||
rsvp: "FALSE",
|
||||
cn: newOrganizer.cn || newOrganizer.cal_address,
|
||||
cutype: "INDIVIDUAL",
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export async function moveEventBetweenCalendars({
|
||||
dispatch,
|
||||
calList,
|
||||
newEvent,
|
||||
oldCalId,
|
||||
newCalId,
|
||||
}: MoveEventBetweenCalendarsParams): Promise<void> {
|
||||
const oldCalendar = calList[oldCalId];
|
||||
if (!oldCalendar) {
|
||||
throw new Error(`Old calendar not found: ${oldCalId}`);
|
||||
}
|
||||
|
||||
const targetCalendar = calList[newCalId];
|
||||
if (!targetCalendar) {
|
||||
throw new Error(`Target calendar not found: ${newCalId}`);
|
||||
}
|
||||
|
||||
const isDelegatedMove = oldCalendar.delegated || targetCalendar.delegated;
|
||||
|
||||
if (isDelegatedMove) {
|
||||
await moveDelegatedEvent({
|
||||
dispatch,
|
||||
newEvent,
|
||||
oldCalendar,
|
||||
targetCalendar,
|
||||
});
|
||||
} else {
|
||||
await moveStandardEvent({
|
||||
dispatch,
|
||||
newEvent,
|
||||
targetCalendar,
|
||||
oldCalendar,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface StandardMoveParams {
|
||||
dispatch: AppDispatch;
|
||||
newEvent: CalendarEvent;
|
||||
targetCalendar: Calendar;
|
||||
oldCalendar: Calendar;
|
||||
}
|
||||
|
||||
async function moveStandardEvent({
|
||||
dispatch,
|
||||
newEvent,
|
||||
targetCalendar,
|
||||
oldCalendar,
|
||||
}: StandardMoveParams): Promise<void> {
|
||||
const newCalId = targetCalendar.id;
|
||||
|
||||
const putResult = await dispatch(
|
||||
putEventAsync({
|
||||
cal: oldCalendar,
|
||||
newEvent: { ...newEvent, calId: oldCalendar.id },
|
||||
})
|
||||
);
|
||||
await assertThunkSuccess(putResult);
|
||||
const newURL = `/calendars/${newCalId}/${extractEventBaseUuid(newEvent.uid)}.ics`;
|
||||
|
||||
const moveResult = await dispatch(
|
||||
moveEventAsync({
|
||||
cal: targetCalendar,
|
||||
newEvent,
|
||||
newURL,
|
||||
})
|
||||
);
|
||||
await assertThunkSuccess(moveResult);
|
||||
}
|
||||
|
||||
interface DelegatedMoveParams {
|
||||
dispatch: AppDispatch;
|
||||
newEvent: CalendarEvent;
|
||||
oldCalendar: Calendar;
|
||||
targetCalendar: Calendar;
|
||||
}
|
||||
|
||||
async function moveDelegatedEvent({
|
||||
dispatch,
|
||||
newEvent,
|
||||
oldCalendar,
|
||||
targetCalendar,
|
||||
}: DelegatedMoveParams): Promise<void> {
|
||||
const newCalId = targetCalendar.id;
|
||||
|
||||
const newOrganizer = resolveOrganizerForCalendar(
|
||||
targetCalendar,
|
||||
newEvent.organizer
|
||||
);
|
||||
|
||||
const newAttendees = rewriteAttendeesForOrganizerChange(
|
||||
newEvent.attendee ?? [],
|
||||
newEvent.organizer,
|
||||
newOrganizer
|
||||
);
|
||||
|
||||
const newURL = `/calendars/${newCalId}/${extractEventBaseUuid(newEvent.uid)}.ics`;
|
||||
|
||||
const eventForTargetCalendar: CalendarEvent = {
|
||||
...newEvent,
|
||||
calId: newCalId,
|
||||
URL: targetCalendar.delegated
|
||||
? buildDelegatedEventURL(targetCalendar, newURL)
|
||||
: newURL,
|
||||
organizer: newOrganizer,
|
||||
attendee: newAttendees,
|
||||
};
|
||||
|
||||
const putResult = await dispatch(
|
||||
putEventAsync({ cal: targetCalendar, newEvent: eventForTargetCalendar })
|
||||
);
|
||||
|
||||
const typedPutResult = putResult as AsyncThunkResult;
|
||||
if (typedPutResult && typeof typedPutResult.unwrap === "function") {
|
||||
await typedPutResult.unwrap();
|
||||
} else {
|
||||
await assertThunkSuccess(putResult);
|
||||
}
|
||||
|
||||
const deleteResult = await dispatch(
|
||||
deleteEventAsync({
|
||||
calId: oldCalendar.id,
|
||||
eventId: newEvent.uid,
|
||||
eventURL: newEvent.URL,
|
||||
})
|
||||
);
|
||||
|
||||
const typedDeleteResult = deleteResult as AsyncThunkResult;
|
||||
if (typedDeleteResult && typeof typedDeleteResult.unwrap === "function") {
|
||||
await typedDeleteResult.unwrap();
|
||||
} else {
|
||||
await assertThunkSuccess(deleteResult);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { AsyncThunkResult } from "@/features/Calendars/types/AsyncThunkResult";
|
||||
|
||||
export async function assertThunkSuccess(result: unknown): Promise<void> {
|
||||
if (result === undefined || result === null) {
|
||||
return;
|
||||
}
|
||||
const typed = result as AsyncThunkResult;
|
||||
if (typed?.type?.endsWith("/rejected")) {
|
||||
if (typed.type && typed.type.endsWith("/rejected")) {
|
||||
throw new Error(
|
||||
typed.error?.message || typed.payload?.message || "API call failed"
|
||||
typed.error?.message ?? typed.payload?.message ?? "Thunk was rejected"
|
||||
);
|
||||
}
|
||||
if (typeof typed.unwrap === "function") {
|
||||
|
||||
Reference in New Issue
Block a user