* added error snackbar for api fails * improved error page to be shown only for userdata errors and to allow retrying * fixed test breaking only in local because of timezone and reduced warnings for easier reading of tests results * added error management for failure inside calendars imports * added error snackbar for api fails * improved error page to be shown only for userdata errors and to allow retrying * added error management for failure inside calendars imports Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import * as eventThunks from "../../src/features/Calendars/CalendarSlice";
|
|||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
import { searchUsers } from "../../src/features/User/userAPI";
|
import { searchUsers } from "../../src/features/User/userAPI";
|
||||||
import * as calendarThunks from "../../src/features/Calendars/CalendarSlice";
|
import * as calendarThunks from "../../src/features/Calendars/CalendarSlice";
|
||||||
import { useRef } from "react";
|
import { act, useRef } from "react";
|
||||||
|
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import CalendarLayout from "../../src/components/Calendar/CalendarLayout";
|
import CalendarLayout from "../../src/components/Calendar/CalendarLayout";
|
||||||
@@ -137,10 +137,12 @@ describe("CalendarSelection", () => {
|
|||||||
};
|
};
|
||||||
it("renders calendars", async () => {
|
it("renders calendars", async () => {
|
||||||
const mockCalendarRef = { current: null };
|
const mockCalendarRef = { current: null };
|
||||||
|
await act(async () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
});
|
||||||
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
||||||
@@ -149,12 +151,14 @@ describe("CalendarSelection", () => {
|
|||||||
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
|
expect(screen.getByLabelText("Calendar delegated")).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
|
expect(screen.getByLabelText("Calendar shared")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it("open accordeon when clicking on button only", () => {
|
it("open accordeon when clicking on button only", async () => {
|
||||||
const mockCalendarRef = { current: null };
|
const mockCalendarRef = { current: null };
|
||||||
|
await act(async () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
});
|
||||||
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
||||||
@@ -168,14 +172,20 @@ describe("CalendarSelection", () => {
|
|||||||
.closest(".MuiAccordionSummary-root");
|
.closest(".MuiAccordionSummary-root");
|
||||||
|
|
||||||
const addButton = screen.getAllByTestId("AddIcon")[2];
|
const addButton = screen.getAllByTestId("AddIcon")[2];
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
|
});
|
||||||
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
|
});
|
||||||
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||||
});
|
});
|
||||||
it("BUGFIX: remove dots in mini calendar when unselecting personnal calendar", () => {
|
it("BUGFIX: remove dots in mini calendar when unselecting personnal calendar", async () => {
|
||||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
const checkbox = screen.getByLabelText("Calendar personnal");
|
const checkbox = screen.getByLabelText("Calendar personnal");
|
||||||
// checkbox checked : events shown
|
// checkbox checked : events shown
|
||||||
@@ -186,8 +196,9 @@ describe("CalendarSelection", () => {
|
|||||||
).toHaveClass("event-dot");
|
).toHaveClass("event-dot");
|
||||||
|
|
||||||
// checkbox unchecked : events hidden
|
// checkbox unchecked : events hidden
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
screen.getByTestId(
|
screen.getByTestId(
|
||||||
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
||||||
@@ -195,18 +206,24 @@ describe("CalendarSelection", () => {
|
|||||||
).not.toHaveClass("event-dot");
|
).not.toHaveClass("event-dot");
|
||||||
|
|
||||||
// checkbox rechecked : events shown
|
// checkbox rechecked : events shown
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
screen.getByTestId(
|
screen.getByTestId(
|
||||||
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
||||||
)
|
)
|
||||||
).toHaveClass("event-dot");
|
).toHaveClass("event-dot");
|
||||||
});
|
});
|
||||||
it("BUGFIX: remove dots in mini calendar when unselecting delegated calendar", () => {
|
it("BUGFIX: remove dots in mini calendar when unselecting delegated calendar", async () => {
|
||||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
// hide personnal event first
|
// hide personnal event first
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
||||||
|
});
|
||||||
const checkbox = screen.getByLabelText("Calendar delegated");
|
const checkbox = screen.getByLabelText("Calendar delegated");
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -216,18 +233,24 @@ describe("CalendarSelection", () => {
|
|||||||
).not.toHaveClass("event-dot");
|
).not.toHaveClass("event-dot");
|
||||||
|
|
||||||
// checkbox checked : events shown
|
// checkbox checked : events shown
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
screen.getByTestId(
|
screen.getByTestId(
|
||||||
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
||||||
)
|
)
|
||||||
).toHaveClass("event-dot");
|
).toHaveClass("event-dot");
|
||||||
});
|
});
|
||||||
it("BUGFIX: remove dots in mini calendar when unselecting shared calendar", () => {
|
it("BUGFIX: remove dots in mini calendar when unselecting shared calendar", async () => {
|
||||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
// hide personnal event first
|
// hide personnal event first
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
||||||
|
});
|
||||||
const checkbox = screen.getByLabelText("Calendar shared");
|
const checkbox = screen.getByLabelText("Calendar shared");
|
||||||
|
|
||||||
// checkbox unchecked : events hidden
|
// checkbox unchecked : events hidden
|
||||||
@@ -238,7 +261,9 @@ describe("CalendarSelection", () => {
|
|||||||
).not.toHaveClass("event-dot");
|
).not.toHaveClass("event-dot");
|
||||||
|
|
||||||
// checkbox checked : events shown
|
// checkbox checked : events shown
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
screen.getByTestId(
|
screen.getByTestId(
|
||||||
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
||||||
@@ -292,13 +317,18 @@ describe("calendar Availability search", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
renderWithProviders(<CalendarTestWrapper />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarTestWrapper />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
||||||
|
act(() => {
|
||||||
userEvent.type(input, "New");
|
userEvent.type(input, "New");
|
||||||
|
});
|
||||||
const option = await screen.findByText("New User");
|
const option = await screen.findByText("New User");
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(option);
|
fireEvent.click(option);
|
||||||
|
});
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -317,14 +347,17 @@ describe("calendar Availability search", () => {
|
|||||||
.mockImplementation((payload) => {
|
.mockImplementation((payload) => {
|
||||||
return () => Promise.resolve(payload) as any;
|
return () => Promise.resolve(payload) as any;
|
||||||
});
|
});
|
||||||
renderWithProviders(<CalendarTestWrapper />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarTestWrapper />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
||||||
userEvent.type(input, "Alice");
|
await act(async () => userEvent.type(input, "Alice"));
|
||||||
|
|
||||||
const option = await screen.findByText("Alice");
|
const option = await screen.findByText("Alice");
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(option);
|
fireEvent.click(option);
|
||||||
|
});
|
||||||
expect(spy).not.toHaveBeenCalledWith();
|
expect(spy).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,18 +376,22 @@ describe("calendar Availability search", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
renderWithProviders(<CalendarTestWrapper />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarTestWrapper />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
||||||
userEvent.type(input, "New");
|
await act(async () => userEvent.type(input, "New"));
|
||||||
|
|
||||||
const option = await screen.findByText("New User");
|
const option = await screen.findByText("New User");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(option);
|
fireEvent.click(option);
|
||||||
|
});
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /create event/i }));
|
fireEvent.click(screen.getByRole("button", { name: /create event/i }));
|
||||||
|
});
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getAllByText(/Create Event/i)).toHaveLength(2);
|
expect(screen.getAllByText(/Create Event/i)).toHaveLength(2);
|
||||||
expect(screen.getAllByText(/New User/i)).toHaveLength(2);
|
expect(screen.getAllByText(/New User/i)).toHaveLength(2);
|
||||||
@@ -376,17 +413,21 @@ describe("calendar Availability search", () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
renderWithProviders(<CalendarTestWrapper />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarTestWrapper />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
const input = screen.getByPlaceholderText(/start typing a name or email/i);
|
||||||
userEvent.type(input, "New");
|
await act(async () => userEvent.type(input, "New"));
|
||||||
|
|
||||||
const option = await screen.findByText("New User");
|
const option = await screen.findByText("New User");
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(option);
|
fireEvent.click(option);
|
||||||
|
});
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
|
await act(async () => {
|
||||||
fireEvent.keyDown(input, { key: "Enter" });
|
fireEvent.keyDown(input, { key: "Enter" });
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getAllByText(/Create Event/i)).toHaveLength(2);
|
expect(screen.getAllByText(/Create Event/i)).toHaveLength(2);
|
||||||
@@ -394,14 +435,16 @@ describe("calendar Availability search", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("BUGFIX: can untoggle all personnal calendars", () => {
|
it("BUGFIX: can untoggle all personnal calendars", async () => {
|
||||||
|
await act(async () =>
|
||||||
renderWithProviders(<CalendarTestWrapper />, {
|
renderWithProviders(<CalendarTestWrapper />, {
|
||||||
user: preloadedState.user,
|
user: preloadedState.user,
|
||||||
calendars: {
|
calendars: {
|
||||||
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
||||||
pending: false,
|
pending: false,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const checkbox = screen.getByLabelText("Calendar personnal");
|
const checkbox = screen.getByLabelText("Calendar personnal");
|
||||||
expect(checkbox).toBeChecked();
|
expect(checkbox).toBeChecked();
|
||||||
@@ -412,13 +455,15 @@ describe("calendar Availability search", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("BUGFIX: monthview doesn't show days numbers in banner", async () => {
|
it("BUGFIX: monthview doesn't show days numbers in banner", async () => {
|
||||||
|
await act(async () =>
|
||||||
renderWithProviders(<CalendarLayout />, {
|
renderWithProviders(<CalendarLayout />, {
|
||||||
user: preloadedState.user,
|
user: preloadedState.user,
|
||||||
calendars: {
|
calendars: {
|
||||||
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
||||||
pending: false,
|
pending: false,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const calendarRef = (window as any).__calendarRef;
|
const calendarRef = (window as any).__calendarRef;
|
||||||
|
|
||||||
@@ -427,8 +472,9 @@ describe("calendar Availability search", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const calendarApi = calendarRef.current;
|
const calendarApi = calendarRef.current;
|
||||||
|
await act(async () => {
|
||||||
calendarApi.changeView("dayGridMonth");
|
calendarApi.changeView("dayGridMonth");
|
||||||
|
});
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryAllByRole("columnheader").length).toBe(14);
|
expect(screen.queryAllByRole("columnheader").length).toBe(14);
|
||||||
});
|
});
|
||||||
@@ -452,7 +498,9 @@ describe("calendar Availability search", () => {
|
|||||||
return () => Promise.resolve(payload) as any;
|
return () => Promise.resolve(payload) as any;
|
||||||
});
|
});
|
||||||
jest.useFakeTimers().setSystemTime(new Date("2025-01-01"));
|
jest.useFakeTimers().setSystemTime(new Date("2025-01-01"));
|
||||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
await act(async () =>
|
||||||
|
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||||
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
@@ -461,9 +509,10 @@ describe("calendar Availability search", () => {
|
|||||||
const calendarRef = (window as any).__calendarRef;
|
const calendarRef = (window as any).__calendarRef;
|
||||||
const calendarApi = calendarRef.current;
|
const calendarApi = calendarRef.current;
|
||||||
const view = calendarApi?.view;
|
const view = calendarApi?.view;
|
||||||
|
await act(async () => {
|
||||||
calendarApi.changeView("dayGridMonth");
|
calendarApi.changeView("dayGridMonth");
|
||||||
fireEvent.click(screen.getByTestId("ChevronRightIcon"));
|
fireEvent.click(screen.getByTestId("ChevronRightIcon"));
|
||||||
|
});
|
||||||
expect(spy).toHaveBeenCalledTimes(4);
|
expect(spy).toHaveBeenCalledTimes(4);
|
||||||
const callArgs = spy.mock.calls[3][0];
|
const callArgs = spy.mock.calls[3][0];
|
||||||
expect(callArgs.calId).toBe("user1/cal1");
|
expect(callArgs.calId).toBe("user1/cal1");
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { renderWithProviders } from "../utils/Renderwithproviders";
|
import { renderWithProviders } from "../utils/Renderwithproviders";
|
||||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
import { screen } from "@testing-library/react";
|
||||||
import { jest } from "@jest/globals";
|
import { jest } from "@jest/globals";
|
||||||
import CalendarApp from "../../src/components/Calendar/Calendar";
|
import CalendarApp from "../../src/components/Calendar/Calendar";
|
||||||
import * as appHooks from "../../src/app/hooks";
|
import * as appHooks from "../../src/app/hooks";
|
||||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||||
import preview from "jest-preview";
|
|
||||||
|
|
||||||
describe("MiniCalendar", () => {
|
describe("MiniCalendar", () => {
|
||||||
const day = new Date();
|
const day = new Date();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { screen, fireEvent, waitFor } from "@testing-library/react";
|
import { screen, fireEvent, waitFor, act } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
@@ -51,7 +51,9 @@ describe("PeopleSearch", () => {
|
|||||||
|
|
||||||
const input = screen.getByRole("combobox");
|
const input = screen.getByRole("combobox");
|
||||||
await userEvent.type(input, "Test");
|
await userEvent.type(input, "Test");
|
||||||
|
await act(async () => {
|
||||||
jest.advanceTimersByTime(300);
|
jest.advanceTimersByTime(300);
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
|
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
|
||||||
@@ -64,7 +66,9 @@ describe("PeopleSearch", () => {
|
|||||||
|
|
||||||
const input = screen.getByRole("combobox");
|
const input = screen.getByRole("combobox");
|
||||||
await userEvent.type(input, "Test");
|
await userEvent.type(input, "Test");
|
||||||
|
await act(async () => {
|
||||||
jest.advanceTimersByTime(300);
|
jest.advanceTimersByTime(300);
|
||||||
|
});
|
||||||
|
|
||||||
const option = await screen.findByText("Test User");
|
const option = await screen.findByText("Test User");
|
||||||
await userEvent.click(option);
|
await userEvent.click(option);
|
||||||
@@ -79,7 +83,9 @@ describe("PeopleSearch", () => {
|
|||||||
setup([baseUser]);
|
setup([baseUser]);
|
||||||
const input = screen.getByRole("combobox");
|
const input = screen.getByRole("combobox");
|
||||||
await userEvent.type(input, "Test");
|
await userEvent.type(input, "Test");
|
||||||
|
await act(async () => {
|
||||||
jest.advanceTimersByTime(300);
|
jest.advanceTimersByTime(300);
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
|
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -37,11 +37,6 @@ describe("Calendar - Timezone Integration", () => {
|
|||||||
timeZone: "America/New_York",
|
timeZone: "America/New_York",
|
||||||
pending: false,
|
pending: false,
|
||||||
},
|
},
|
||||||
events: {
|
|
||||||
selectedEvent: null,
|
|
||||||
isEditMode: false,
|
|
||||||
editModeDialogOpen: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -104,6 +104,26 @@ describe("Event Preview Display", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("renders correctly event data", () => {
|
it("renders correctly event data", () => {
|
||||||
|
const originalToLocaleString = Date.prototype.toLocaleString;
|
||||||
|
|
||||||
|
jest.spyOn(Date.prototype, "toLocaleString").mockImplementation(function (
|
||||||
|
this: Date,
|
||||||
|
locales?: Intl.LocalesArgument,
|
||||||
|
options?: Intl.DateTimeFormatOptions
|
||||||
|
) {
|
||||||
|
return originalToLocaleString.call(this, "en-US", options);
|
||||||
|
});
|
||||||
|
const originalToLocaleTimeString = Date.prototype.toLocaleTimeString;
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(Date.prototype, "toLocaleTimeString")
|
||||||
|
.mockImplementation(function (
|
||||||
|
this: Date,
|
||||||
|
locales?: Intl.LocalesArgument,
|
||||||
|
options?: Intl.DateTimeFormatOptions
|
||||||
|
) {
|
||||||
|
return originalToLocaleTimeString.call(this, "en-US", options);
|
||||||
|
});
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventPreviewModal
|
<EventPreviewModal
|
||||||
open={true}
|
open={true}
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ describe("HandleLogin", () => {
|
|||||||
|
|
||||||
expect(screen.getByAltText("loading")).toBeInTheDocument();
|
expect(screen.getByAltText("loading")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
test("goes to error page when userData doesnt exists after loading and calendars pending is false", () => {
|
test("goes to error page when there is error in user data", () => {
|
||||||
const dispatch = appHooks.useAppDispatch();
|
const dispatch = appHooks.useAppDispatch();
|
||||||
renderWithProviders(<HandleLogin />, { user: { loading: false } });
|
renderWithProviders(<HandleLogin />, { user: { error: true } });
|
||||||
expect(dispatch).toHaveBeenCalledWith(push("/error"));
|
expect(dispatch).toHaveBeenCalledWith(push("/error"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-1
@@ -1,4 +1,4 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense, useEffect } from "react";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import { HistoryRouter as Router } from "redux-first-history/rr6";
|
import { HistoryRouter as Router } from "redux-first-history/rr6";
|
||||||
import { CallbackResume } from "./features/User/LoginCallback";
|
import { CallbackResume } from "./features/User/LoginCallback";
|
||||||
@@ -9,8 +9,18 @@ import HandleLogin from "./features/User/HandleLogin";
|
|||||||
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
||||||
import { Error } from "./components/Error/Error";
|
import { Error } from "./components/Error/Error";
|
||||||
import { CustomThemeProvider } from "./theme/ThemeProvider";
|
import { CustomThemeProvider } from "./theme/ThemeProvider";
|
||||||
|
import { useAppDispatch, useAppSelector } from "./app/hooks";
|
||||||
|
import { push } from "redux-first-history";
|
||||||
|
import { ErrorSnackbar } from "./components/Error/ErrorSnackbar";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const error = useAppSelector((state) => state.user.error);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
dispatch(push("/error"));
|
||||||
|
}
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<CustomThemeProvider>
|
<CustomThemeProvider>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
@@ -22,6 +32,8 @@ function App() {
|
|||||||
<Route path="/error" element={<Error />} />
|
<Route path="/error" element={<Error />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
<ErrorSnackbar error={error} type="user" />
|
||||||
|
<ErrorSnackbar error={error} type="user" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</CustomThemeProvider>
|
</CustomThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function UserSearch({
|
|||||||
displayName: a.cn ?? "",
|
displayName: a.cn ?? "",
|
||||||
avatarUrl: "",
|
avatarUrl: "",
|
||||||
openpaasId: "",
|
openpaasId: "",
|
||||||
}))
|
})) ?? []
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedUsers(
|
setSelectedUsers(
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { useAppDispatch } from "../../app/hooks";
|
|||||||
import { getCalendarRange } from "../../utils/dateUtils";
|
import { getCalendarRange } from "../../utils/dateUtils";
|
||||||
import { useAppSelector } from "../../app/hooks";
|
import { useAppSelector } from "../../app/hooks";
|
||||||
import { refreshCalendars } from "../Event/utils/eventUtils";
|
import { refreshCalendars } from "../Event/utils/eventUtils";
|
||||||
|
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||||
|
|
||||||
export default function CalendarLayout() {
|
export default function CalendarLayout() {
|
||||||
const calendarRef = useRef<any>(null);
|
const calendarRef = useRef<any>(null);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const error = useAppSelector((state) => state.calendars.error);
|
||||||
const selectedCalendars = useAppSelector((state) => state.calendars.list);
|
const selectedCalendars = useAppSelector((state) => state.calendars.list);
|
||||||
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
||||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||||
@@ -62,6 +64,7 @@ export default function CalendarLayout() {
|
|||||||
onDateChange={handleDateChange}
|
onDateChange={handleDateChange}
|
||||||
onViewChange={handleViewChange}
|
onViewChange={handleViewChange}
|
||||||
/>
|
/>
|
||||||
|
<ErrorSnackbar error={error} type="calendar" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,89 @@
|
|||||||
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||||
|
import ReplayIcon from "@mui/icons-material/Replay";
|
||||||
|
import { Box, Button, Fade, Paper, Stack, Typography } from "@mui/material";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { push } from "redux-first-history";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
|
||||||
export function Error() {
|
export function Error() {
|
||||||
return <p>Error</p>;
|
const dispatch = useAppDispatch();
|
||||||
|
const userError = useAppSelector((state) => state.user.error);
|
||||||
|
const calendarError = useAppSelector((state) => state.calendars.error);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!userError) {
|
||||||
|
dispatch(push("/"));
|
||||||
|
}
|
||||||
|
}, [calendarError, dispatch]);
|
||||||
|
|
||||||
|
const errorMessage = userError || calendarError || "Unknown error";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fade in timeout={500}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
bgcolor: "background.default",
|
||||||
|
p: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: 6,
|
||||||
|
textAlign: "center",
|
||||||
|
maxWidth: 420,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2} alignItems="center">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
color: "error.main",
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorOutlineIcon sx={{ fontSize: 40 }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h5" fontWeight={600}>
|
||||||
|
Something went wrong
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
startIcon={<ReplayIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 3,
|
||||||
|
py: 1,
|
||||||
|
boxShadow: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,51 @@
|
|||||||
import Snackbar from "@mui/material/Snackbar";
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
|
import { useAppDispatch } from "../../app/hooks";
|
||||||
|
import { clearError as calendarClearError } from "../../features/Calendars/CalendarSlice";
|
||||||
|
import { clearError as userClearError } from "../../features/User/userSlice";
|
||||||
|
|
||||||
interface Props {
|
export function ErrorSnackbar({
|
||||||
messages: string[];
|
error,
|
||||||
onClose: () => void;
|
type,
|
||||||
|
}: {
|
||||||
|
error: string | null;
|
||||||
|
type: "user" | "calendar";
|
||||||
|
}) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleCloseSnackbar = () => {
|
||||||
|
dispatch(type === "calendar" ? calendarClearError() : userClearError());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
open={!!error}
|
||||||
|
onClose={handleCloseSnackbar}
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
onClose={handleCloseSnackbar}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
action={
|
||||||
|
<Button color="inherit" size="small" onClick={handleCloseSnackbar}>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventErrorSnackbar({ messages, onClose }: Props) {
|
export function EventErrorSnackbar({
|
||||||
|
messages,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
messages: string[];
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
const open = messages.length > 0;
|
const open = messages.length > 0;
|
||||||
const summary =
|
const summary =
|
||||||
messages.length === 1
|
messages.length === 1
|
||||||
|
|||||||
@@ -29,15 +29,25 @@ import {
|
|||||||
import { User } from "../../components/Attendees/PeopleSearch";
|
import { User } from "../../components/Attendees/PeopleSearch";
|
||||||
import { getCalendarVisibility } from "../../components/Calendar/utils/calendarUtils";
|
import { getCalendarVisibility } from "../../components/Calendar/utils/calendarUtils";
|
||||||
import { importFile } from "../../utils/apiUtils";
|
import { importFile } from "../../utils/apiUtils";
|
||||||
|
import { formatReduxError } from "../../utils/errorUtils";
|
||||||
|
|
||||||
|
// Define error type for rejected actions
|
||||||
|
interface RejectedError {
|
||||||
|
message: string;
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const getCalendarsListAsync = createAsyncThunk<
|
export const getCalendarsListAsync = createAsyncThunk<
|
||||||
Record<string, Calendars> // Return type
|
{ importedCalendars: Record<string, Calendars>; errors: string }, // Return type
|
||||||
>("calendars/getCalendars", async () => {
|
void, // Arg type
|
||||||
|
{ rejectValue: RejectedError } // ThunkAPI config
|
||||||
|
>("calendars/getCalendars", async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
const importedCalendars: Record<string, Calendars> = {};
|
const importedCalendars: Record<string, Calendars> = {};
|
||||||
const user = (await getOpenPaasUser()) as Record<string, string>;
|
const user = (await getOpenPaasUser()) as Record<string, string>;
|
||||||
const calendars = (await getCalendars(user.id)) as Record<string, any>;
|
const calendars = (await getCalendars(user.id)) as Record<string, any>;
|
||||||
const rawCalendars = calendars._embedded["dav:calendar"];
|
const rawCalendars = calendars._embedded["dav:calendar"];
|
||||||
|
const errors = [];
|
||||||
for (const cal of rawCalendars) {
|
for (const cal of rawCalendars) {
|
||||||
const description = cal["caldav:description"];
|
const description = cal["caldav:description"];
|
||||||
let delegated = false;
|
let delegated = false;
|
||||||
@@ -52,7 +62,24 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
const id = source.replace("/calendars/", "").replace(".json", "");
|
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||||
const ownerId = id.split("/")[0];
|
const ownerId = id.split("/")[0];
|
||||||
const visibility = getCalendarVisibility(cal["acl"]);
|
const visibility = getCalendarVisibility(cal["acl"]);
|
||||||
const ownerData: any = await getUserDetails(ownerId);
|
|
||||||
|
// Safely fetch owner data with fallback
|
||||||
|
let ownerData: any;
|
||||||
|
try {
|
||||||
|
ownerData = await getUserDetails(ownerId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to fetch user details for ${id.split("/")[0]}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
// Provide fallback data
|
||||||
|
ownerData = {
|
||||||
|
firstname: "",
|
||||||
|
lastname: "Unknown User",
|
||||||
|
emails: [],
|
||||||
|
};
|
||||||
|
errors.push(error);
|
||||||
|
}
|
||||||
const name =
|
const name =
|
||||||
ownerId !== user.id && cal["dav:name"] === "#default"
|
ownerId !== user.id && cal["dav:name"] === "#default"
|
||||||
? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
||||||
@@ -80,13 +107,21 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return importedCalendars;
|
return { importedCalendars, errors: errors.join("\n") };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getTempCalendarsListAsync = createAsyncThunk<
|
export const getTempCalendarsListAsync = createAsyncThunk<
|
||||||
Record<string, Calendars>,
|
Record<string, Calendars>,
|
||||||
User
|
User,
|
||||||
>("calendars/getTempCalendars", async (tempUser) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
const importedCalendars: Record<string, Calendars> = {};
|
const importedCalendars: Record<string, Calendars> = {};
|
||||||
|
|
||||||
const calendars = (await getCalendars(
|
const calendars = (await getCalendars(
|
||||||
@@ -126,36 +161,65 @@ export const getTempCalendarsListAsync = createAsyncThunk<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return importedCalendars;
|
return importedCalendars;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getCalendarDetailAsync = createAsyncThunk<
|
export const getCalendarDetailAsync = createAsyncThunk<
|
||||||
{ calId: string; events: CalendarEvent[]; calType?: string }, // Return type
|
{ calId: string; events: CalendarEvent[]; calType?: string },
|
||||||
{ calId: string; match: { start: string; end: string }; calType?: string } // Arg type
|
{ calId: string; match: { start: string; end: string }; calType?: string },
|
||||||
>("calendars/getCalendarDetails", async ({ calId, match, calType }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/getCalendarDetails",
|
||||||
|
async ({ calId, match, calType }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
const calendar = (await getCalendar(calId, match)) as Record<string, any>;
|
const calendar = (await getCalendar(calId, match)) as Record<string, any>;
|
||||||
const color = calendar["apple:color"];
|
const color = calendar["apple:color"];
|
||||||
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
|
||||||
(eventdata: any) => {
|
(eventdata: any) => {
|
||||||
const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []]
|
const vevents = eventdata.data[2] as any[][];
|
||||||
const valarm = eventdata.data[2][0][2][0];
|
const valarm = eventdata.data[2][0][2][0];
|
||||||
const eventURL = eventdata._links.self.href;
|
const eventURL = eventdata._links.self.href;
|
||||||
return vevents.map((vevent: any[]) => {
|
return vevents.map((vevent: any[]) => {
|
||||||
return parseCalendarEvent(vevent[1], color, calId, eventURL, valarm);
|
return parseCalendarEvent(
|
||||||
|
vevent[1],
|
||||||
|
color,
|
||||||
|
calId,
|
||||||
|
eventURL,
|
||||||
|
valarm
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { calId, events, calType };
|
return { calId, events, calType };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const putEventAsync = createAsyncThunk<
|
export const putEventAsync = createAsyncThunk<
|
||||||
{ calId: string; events: CalendarEvent[]; calType?: "temp" }, // Return type
|
{ calId: string; events: CalendarEvent[]; calType?: "temp" },
|
||||||
{ cal: Calendars; newEvent: CalendarEvent; calType?: "temp" } // Arg type
|
{ cal: Calendars; newEvent: CalendarEvent; calType?: "temp" },
|
||||||
>("calendars/putEvent", async ({ cal, newEvent, calType }) => {
|
{ rejectValue: RejectedError }
|
||||||
await putEvent(newEvent, cal.ownerEmails ? cal.ownerEmails[0] : undefined);
|
>(
|
||||||
|
"calendars/putEvent",
|
||||||
|
async ({ cal, newEvent, calType }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
await putEvent(
|
||||||
|
newEvent,
|
||||||
|
cal.ownerEmails ? cal.ownerEmails[0] : undefined
|
||||||
|
);
|
||||||
const eventDate = new Date(newEvent.start);
|
const eventDate = new Date(newEvent.start);
|
||||||
|
|
||||||
// Calculate week range based on Monday as first day (consistent with FullCalendar firstDay={1})
|
|
||||||
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
|
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
|
||||||
|
|
||||||
const calEvents = (await getCalendar(cal.id, {
|
const calEvents = (await getCalendar(cal.id, {
|
||||||
@@ -184,37 +248,64 @@ export const putEventAsync = createAsyncThunk<
|
|||||||
events,
|
events,
|
||||||
calType,
|
calType,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const getEventAsync = createAsyncThunk<
|
export const getEventAsync = createAsyncThunk<
|
||||||
{ calId: string; event: CalendarEvent }, // Return type
|
{ calId: string; event: CalendarEvent },
|
||||||
CalendarEvent // Arg type
|
CalendarEvent,
|
||||||
>("calendars/getEvent", async (event) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>("calendars/getEvent", async (event, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
const response: CalendarEvent = await getEvent(event);
|
const response: CalendarEvent = await getEvent(event);
|
||||||
return {
|
return {
|
||||||
calId: event.calId,
|
calId: event.calId,
|
||||||
event: response,
|
event: response,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const patchCalendarAsync = createAsyncThunk<
|
export const patchCalendarAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
calLink: string;
|
calLink: string;
|
||||||
patch: { name: string; desc: string; color: Record<string, string> };
|
patch: { name: string; desc: string; color: Record<string, string> };
|
||||||
}, // Return type
|
},
|
||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
calLink: string;
|
calLink: string;
|
||||||
patch: { name: string; desc: string; color: Record<string, string> };
|
patch: { name: string; desc: string; color: Record<string, string> };
|
||||||
} // Arg type
|
},
|
||||||
>("calendars/patchCalendar", async ({ calId, calLink, patch }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/patchCalendar",
|
||||||
|
async ({ calId, calLink, patch }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await proppatchCalendar(calLink, patch);
|
await proppatchCalendar(calLink, patch);
|
||||||
return {
|
return {
|
||||||
calId,
|
calId,
|
||||||
calLink,
|
calLink,
|
||||||
patch,
|
patch,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const removeCalendarAsync = createAsyncThunk<
|
export const removeCalendarAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
@@ -223,22 +314,36 @@ export const removeCalendarAsync = createAsyncThunk<
|
|||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
calLink: string;
|
calLink: string;
|
||||||
}
|
},
|
||||||
>("calendars/removeCalendar", async ({ calId, calLink }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/removeCalendar",
|
||||||
|
async ({ calId, calLink }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await removeCalendar(calLink);
|
await removeCalendar(calLink);
|
||||||
return {
|
return {
|
||||||
calId,
|
calId,
|
||||||
calLink,
|
calLink,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const moveEventAsync = createAsyncThunk<
|
export const moveEventAsync = createAsyncThunk<
|
||||||
{ calId: string; events: CalendarEvent[] }, // Return type
|
{ calId: string; events: CalendarEvent[] },
|
||||||
{ cal: Calendars; newEvent: CalendarEvent; newURL: string } // Arg type
|
{ cal: Calendars; newEvent: CalendarEvent; newURL: string },
|
||||||
>("calendars/moveEvent", async ({ cal, newEvent, newURL }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/moveEvent",
|
||||||
|
async ({ cal, newEvent, newURL }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await moveEvent(newEvent, newURL);
|
await moveEvent(newEvent, newURL);
|
||||||
|
|
||||||
// Calculate week range based on Monday as first day (consistent with FullCalendar firstDay={1})
|
|
||||||
const eventDate = new Date(newEvent.start);
|
const eventDate = new Date(newEvent.start);
|
||||||
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
|
const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
|
||||||
|
|
||||||
@@ -251,7 +356,12 @@ export const moveEventAsync = createAsyncThunk<
|
|||||||
const vevents = eventdata.data[2] as any[][];
|
const vevents = eventdata.data[2] as any[][];
|
||||||
const eventURL = eventdata._links.self.href;
|
const eventURL = eventdata._links.self.href;
|
||||||
return vevents.map((vevent: any[]) => {
|
return vevents.map((vevent: any[]) => {
|
||||||
return parseCalendarEvent(vevent[1], cal.color ?? {}, cal.id, eventURL);
|
return parseCalendarEvent(
|
||||||
|
vevent[1],
|
||||||
|
cal.color ?? {},
|
||||||
|
cal.id,
|
||||||
|
eventURL
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -260,58 +370,117 @@ export const moveEventAsync = createAsyncThunk<
|
|||||||
calId: cal.id,
|
calId: cal.id,
|
||||||
events,
|
events,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const patchACLCalendarAsync = createAsyncThunk<
|
export const patchACLCalendarAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
calLink: string;
|
calLink: string;
|
||||||
request: string;
|
request: string;
|
||||||
}, // Return type
|
},
|
||||||
{
|
{
|
||||||
calId: string;
|
calId: string;
|
||||||
calLink: string;
|
calLink: string;
|
||||||
request: string;
|
request: string;
|
||||||
} // Arg type
|
},
|
||||||
>("calendars/requestACLCalendar", async ({ calId, calLink, request }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/requestACLCalendar",
|
||||||
|
async ({ calId, calLink, request }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
const response = await updateAclCalendar(calLink, request);
|
const response = await updateAclCalendar(calLink, request);
|
||||||
return {
|
return {
|
||||||
calId,
|
calId,
|
||||||
calLink,
|
calLink,
|
||||||
request,
|
request,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const deleteEventAsync = createAsyncThunk<
|
export const deleteEventAsync = createAsyncThunk<
|
||||||
{ calId: string; eventId: string }, // Return type
|
{ calId: string; eventId: string },
|
||||||
{ calId: string; eventId: string; eventURL: string } // Arg type
|
{ calId: string; eventId: string; eventURL: string },
|
||||||
>("calendars/delEvent", async ({ calId, eventId, eventURL }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/delEvent",
|
||||||
|
async ({ calId, eventId, eventURL }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await deleteEvent(eventURL);
|
await deleteEvent(eventURL);
|
||||||
return { calId, eventId };
|
return { calId, eventId };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const deleteEventInstanceAsync = createAsyncThunk<
|
export const deleteEventInstanceAsync = createAsyncThunk<
|
||||||
{ calId: string; eventId: string },
|
{ calId: string; eventId: string },
|
||||||
{ cal: Calendars; event: CalendarEvent }
|
{ cal: Calendars; event: CalendarEvent },
|
||||||
>("calendars/delEventInstance", async ({ cal, event }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await deleteEventInstance(event, cal.ownerEmails?.[0]);
|
await deleteEventInstance(event, cal.ownerEmails?.[0]);
|
||||||
return { calId: cal.id, eventId: event.uid };
|
return { calId: cal.id, eventId: event.uid };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateEventInstanceAsync = createAsyncThunk<
|
export const updateEventInstanceAsync = createAsyncThunk<
|
||||||
{ calId: string; event: CalendarEvent },
|
{ calId: string; event: CalendarEvent },
|
||||||
{ cal: Calendars; event: CalendarEvent }
|
{ cal: Calendars; event: CalendarEvent },
|
||||||
>("calendars/updateEventInstance", async ({ cal, event }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/updateEventInstance",
|
||||||
|
async ({ cal, event }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await putEventWithOverrides(event, cal.ownerEmails?.[0]);
|
await putEventWithOverrides(event, cal.ownerEmails?.[0]);
|
||||||
return { calId: cal.id, event };
|
return { calId: cal.id, event };
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const updateSeriesAsync = createAsyncThunk<
|
export const updateSeriesAsync = createAsyncThunk<
|
||||||
void,
|
void,
|
||||||
{ cal: Calendars; event: CalendarEvent; removeOverrides?: boolean }
|
{ cal: Calendars; event: CalendarEvent; removeOverrides?: boolean },
|
||||||
>("calendars/updateSeries", async ({ cal, event, removeOverrides = true }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/updateSeries",
|
||||||
|
async ({ cal, event, removeOverrides = true }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await updateSeries(event, cal.ownerEmails?.[0], removeOverrides);
|
await updateSeries(event, cal.ownerEmails?.[0], removeOverrides);
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const createCalendarAsync = createAsyncThunk<
|
export const createCalendarAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
@@ -322,15 +491,19 @@ export const createCalendarAsync = createAsyncThunk<
|
|||||||
desc: string;
|
desc: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
ownerEmails: string[];
|
ownerEmails: string[];
|
||||||
}, // Return type
|
},
|
||||||
{
|
{
|
||||||
userId: string;
|
userId: string;
|
||||||
calId: string;
|
calId: string;
|
||||||
color: Record<string, string>;
|
color: Record<string, string>;
|
||||||
name: string;
|
name: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
} // Arg type
|
},
|
||||||
>("calendars/createCalendar", async ({ userId, calId, color, name, desc }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/createCalendar",
|
||||||
|
async ({ userId, calId, color, name, desc }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await postCalendar(userId, calId, color, name, desc);
|
await postCalendar(userId, calId, color, name, desc);
|
||||||
const ownerData: any = await getUserDetails(userId.split("/")[0]);
|
const ownerData: any = await getUserDetails(userId.split("/")[0]);
|
||||||
|
|
||||||
@@ -345,7 +518,14 @@ export const createCalendarAsync = createAsyncThunk<
|
|||||||
}`,
|
}`,
|
||||||
ownerEmails: ownerData.emails,
|
ownerEmails: ownerData.emails,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const addSharedCalendarAsync = createAsyncThunk<
|
export const addSharedCalendarAsync = createAsyncThunk<
|
||||||
{
|
{
|
||||||
@@ -356,9 +536,13 @@ export const addSharedCalendarAsync = createAsyncThunk<
|
|||||||
desc: string;
|
desc: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
ownerEmails: string[];
|
ownerEmails: string[];
|
||||||
}, // Return type
|
},
|
||||||
{ userId: string; calId: string; cal: Record<string, any> } // Arg type
|
{ userId: string; calId: string; cal: Record<string, any> },
|
||||||
>("calendars/addSharedCalendar", async ({ userId, calId, cal }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>(
|
||||||
|
"calendars/addSharedCalendar",
|
||||||
|
async ({ userId, calId, cal }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
await addSharedCalendar(userId, calId, cal);
|
await addSharedCalendar(userId, calId, cal);
|
||||||
const ownerData: any = await getUserDetails(
|
const ownerData: any = await getUserDetails(
|
||||||
cal.cal._links.self.href
|
cal.cal._links.self.href
|
||||||
@@ -388,17 +572,32 @@ export const addSharedCalendarAsync = createAsyncThunk<
|
|||||||
}`,
|
}`,
|
||||||
ownerEmails: ownerData.emails,
|
ownerEmails: ownerData.emails,
|
||||||
};
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const importEventFromFileAsync = createAsyncThunk<
|
export const importEventFromFileAsync = createAsyncThunk<
|
||||||
void,
|
void,
|
||||||
{
|
{
|
||||||
calLink: string;
|
calLink: string;
|
||||||
file: File;
|
file: File;
|
||||||
}
|
},
|
||||||
>("calendars/importEvent", async ({ calLink, file }) => {
|
{ rejectValue: RejectedError }
|
||||||
|
>("calendars/importEvent", async ({ calLink, file }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
const id = ((await importFile(file)) as Record<string, string>)._id;
|
const id = ((await importFile(file)) as Record<string, string>)._id;
|
||||||
const response = await importEventFromFile(id, calLink);
|
const response = await importEventFromFile(id, calLink);
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const CalendarSlice = createSlice({
|
const CalendarSlice = createSlice({
|
||||||
@@ -407,11 +606,13 @@ const CalendarSlice = createSlice({
|
|||||||
list: {} as Record<string, Calendars>,
|
list: {} as Record<string, Calendars>,
|
||||||
templist: {} as Record<string, Calendars>,
|
templist: {} as Record<string, Calendars>,
|
||||||
pending: false,
|
pending: false,
|
||||||
|
error: null as string | null,
|
||||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
} as {
|
} as {
|
||||||
list: Record<string, Calendars>;
|
list: Record<string, Calendars>;
|
||||||
templist: Record<string, Calendars>;
|
templist: Record<string, Calendars>;
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
|
error: string | null;
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
@@ -480,6 +681,9 @@ const CalendarSlice = createSlice({
|
|||||||
if (!state.list[action.payload]) return;
|
if (!state.list[action.payload]) return;
|
||||||
state.list[action.payload].lastCacheCleared = Date.now();
|
state.list[action.payload].lastCacheCleared = Date.now();
|
||||||
},
|
},
|
||||||
|
clearError: (state) => {
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
updateCalColor: (
|
updateCalColor: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
@@ -492,11 +696,21 @@ const CalendarSlice = createSlice({
|
|||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
|
// Fulfilled cases
|
||||||
.addCase(
|
.addCase(
|
||||||
getCalendarsListAsync.fulfilled,
|
getCalendarsListAsync.fulfilled,
|
||||||
(state, action: PayloadAction<Record<string, Calendars>>) => {
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
importedCalendars: Record<string, Calendars>;
|
||||||
|
errors: string;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
state.list = action.payload;
|
state.list = action.payload.importedCalendars;
|
||||||
|
state.error = action.payload.errors.length
|
||||||
|
? action.payload.errors
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addCase(
|
.addCase(
|
||||||
@@ -636,18 +850,22 @@ const CalendarSlice = createSlice({
|
|||||||
action.payload.eventId
|
action.payload.eventId
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(deleteEventInstanceAsync.fulfilled, (state, action) => {
|
.addCase(deleteEventInstanceAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
delete state.list[action.payload.calId].events[action.payload.eventId];
|
delete state.list[action.payload.calId].events[action.payload.eventId];
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(updateEventInstanceAsync.fulfilled, (state, action) => {
|
.addCase(updateEventInstanceAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
state.list[action.payload.calId].events[action.payload.event.uid] =
|
state.list[action.payload.calId].events[action.payload.event.uid] =
|
||||||
action.payload.event;
|
action.payload.event;
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(updateSeriesAsync.fulfilled, (state) => {
|
.addCase(updateSeriesAsync.fulfilled, (state) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(createCalendarAsync.fulfilled, (state, action) => {
|
.addCase(createCalendarAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
@@ -661,6 +879,7 @@ const CalendarSlice = createSlice({
|
|||||||
ownerEmails: action.payload.ownerEmails,
|
ownerEmails: action.payload.ownerEmails,
|
||||||
events: {},
|
events: {},
|
||||||
} as Calendars;
|
} as Calendars;
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(patchCalendarAsync.fulfilled, (state, action) => {
|
.addCase(patchCalendarAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
@@ -687,6 +906,7 @@ const CalendarSlice = createSlice({
|
|||||||
name: action.payload.patch.name,
|
name: action.payload.patch.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(addSharedCalendarAsync.fulfilled, (state, action) => {
|
.addCase(addSharedCalendarAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
@@ -700,16 +920,24 @@ const CalendarSlice = createSlice({
|
|||||||
owner: action.payload.owner,
|
owner: action.payload.owner,
|
||||||
ownerEmails: action.payload.ownerEmails,
|
ownerEmails: action.payload.ownerEmails,
|
||||||
} as Calendars;
|
} as Calendars;
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(removeCalendarAsync.fulfilled, (state, action) => {
|
.addCase(removeCalendarAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
delete state.list[action.payload.calId];
|
delete state.list[action.payload.calId];
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
.addCase(patchACLCalendarAsync.fulfilled, (state, action) => {
|
.addCase(patchACLCalendarAsync.fulfilled, (state, action) => {
|
||||||
state.pending = false;
|
state.pending = false;
|
||||||
state.list[action.payload.calId].visibility =
|
state.list[action.payload.calId].visibility =
|
||||||
action.payload.request !== "" ? "public" : "private";
|
action.payload.request !== "" ? "public" : "private";
|
||||||
|
state.error = null;
|
||||||
})
|
})
|
||||||
|
.addCase(importEventFromFileAsync.fulfilled, (state) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
// Pending cases
|
||||||
.addCase(getCalendarDetailAsync.pending, (state) => {
|
.addCase(getCalendarDetailAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
@@ -728,6 +956,15 @@ const CalendarSlice = createSlice({
|
|||||||
.addCase(deleteEventAsync.pending, (state) => {
|
.addCase(deleteEventAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
|
.addCase(deleteEventInstanceAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
|
})
|
||||||
|
.addCase(updateEventInstanceAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
|
})
|
||||||
|
.addCase(updateSeriesAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
|
})
|
||||||
.addCase(patchCalendarAsync.pending, (state) => {
|
.addCase(patchCalendarAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
})
|
})
|
||||||
@@ -745,6 +982,124 @@ const CalendarSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(patchACLCalendarAsync.pending, (state) => {
|
.addCase(patchACLCalendarAsync.pending, (state) => {
|
||||||
state.pending = true;
|
state.pending = true;
|
||||||
|
})
|
||||||
|
.addCase(importEventFromFileAsync.pending, (state) => {
|
||||||
|
state.pending = true;
|
||||||
|
})
|
||||||
|
// Rejected cases
|
||||||
|
.addCase(getCalendarsListAsync.rejected, (state, action) => {
|
||||||
|
if (action.payload?.status !== 401) {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to load calendars";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(getTempCalendarsListAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to load temporary calendars";
|
||||||
|
})
|
||||||
|
.addCase(getCalendarDetailAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to load calendar details";
|
||||||
|
})
|
||||||
|
.addCase(putEventAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to create event";
|
||||||
|
})
|
||||||
|
.addCase(getEventAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to load event";
|
||||||
|
})
|
||||||
|
.addCase(moveEventAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to move event";
|
||||||
|
})
|
||||||
|
.addCase(deleteEventAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to delete event";
|
||||||
|
})
|
||||||
|
.addCase(deleteEventInstanceAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to delete event instance";
|
||||||
|
})
|
||||||
|
.addCase(updateEventInstanceAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to update event instance";
|
||||||
|
})
|
||||||
|
.addCase(updateSeriesAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to update event series";
|
||||||
|
})
|
||||||
|
.addCase(patchCalendarAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to update calendar";
|
||||||
|
})
|
||||||
|
.addCase(createCalendarAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to create calendar";
|
||||||
|
})
|
||||||
|
.addCase(addSharedCalendarAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to add shared calendar";
|
||||||
|
})
|
||||||
|
.addCase(removeCalendarAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to remove calendar";
|
||||||
|
})
|
||||||
|
.addCase(patchACLCalendarAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to update calendar permissions";
|
||||||
|
})
|
||||||
|
.addCase(importEventFromFileAsync.rejected, (state, action) => {
|
||||||
|
state.pending = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message ||
|
||||||
|
action.error.message ||
|
||||||
|
"Failed to import event from file";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -758,6 +1113,7 @@ export const {
|
|||||||
emptyEventsCal,
|
emptyEventsCal,
|
||||||
setTimeZone,
|
setTimeZone,
|
||||||
clearFetchCache,
|
clearFetchCache,
|
||||||
|
clearError,
|
||||||
updateCalColor,
|
updateCalColor,
|
||||||
} = CalendarSlice.actions;
|
} = CalendarSlice.actions;
|
||||||
export default CalendarSlice.reducer;
|
export default CalendarSlice.reducer;
|
||||||
|
|||||||
@@ -14,24 +14,24 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
|||||||
const eventData = await response.text();
|
const eventData = await response.text();
|
||||||
|
|
||||||
const eventical = ICAL.parse(eventData);
|
const eventical = ICAL.parse(eventData);
|
||||||
|
const vevents = (eventical[2] || []).filter(
|
||||||
|
([name]: [string]) => name.toLowerCase() === "vevent"
|
||||||
|
);
|
||||||
let targetVevent;
|
let targetVevent;
|
||||||
if (isMaster) {
|
if (isMaster) {
|
||||||
// Find master VEVENT (the one without recurrence-id)
|
// Find master VEVENT (the one without recurrence-id)
|
||||||
const vevents = eventical[2].filter(
|
|
||||||
([name]: string[]) => name === "vevent"
|
|
||||||
);
|
|
||||||
targetVevent = vevents.find(
|
targetVevent = vevents.find(
|
||||||
([, props]: [string, any[]]) =>
|
([, props]: [string, any[]]) =>
|
||||||
!props.find(([k]: string[]) => k.toLowerCase() === "recurrence-id")
|
!props.find(([k]: string[]) => k.toLowerCase() === "recurrence-id")
|
||||||
);
|
);
|
||||||
if (!targetVevent) {
|
if (!targetVevent) {
|
||||||
// Fallback to first VEVENT if no master found
|
// Fallback to first VEVENT if no master found
|
||||||
targetVevent = eventical[2][1];
|
targetVevent = vevents[0];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For non-master, use first VEVENT as before
|
// For non-master, use first VEVENT as before
|
||||||
targetVevent = eventical[2][1];
|
targetVevent = vevents[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventjson = parseCalendarEvent(
|
const eventjson = parseCalendarEvent(
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default function EventDisplayModal({
|
|||||||
const [end, setEnd] = useState(
|
const [end, setEnd] = useState(
|
||||||
formatLocalDateTime(new Date(event?.end ?? Date.now()))
|
formatLocalDateTime(new Date(event?.end ?? Date.now()))
|
||||||
);
|
);
|
||||||
const [allday, setAllDay] = useState(event?.allday);
|
const [allday, setAllDay] = useState(event?.allday ?? false);
|
||||||
const [repetition, setRepetition] = useState<RepetitionObject>(
|
const [repetition, setRepetition] = useState<RepetitionObject>(
|
||||||
event?.repetition ?? ({} as RepetitionObject)
|
event?.repetition ?? ({} as RepetitionObject)
|
||||||
);
|
);
|
||||||
@@ -280,7 +280,7 @@ export default function EventDisplayModal({
|
|||||||
fullWidth
|
fullWidth
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
label="Title"
|
label="Title"
|
||||||
value={title}
|
value={title ?? ""}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
size="small"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
@@ -366,7 +366,7 @@ export default function EventDisplayModal({
|
|||||||
<Select
|
<Select
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
labelId="calendar-select-label"
|
labelId="calendar-select-label"
|
||||||
value={calendarid.toString()}
|
value={calendarid.toString() ?? ""}
|
||||||
label="Calendar"
|
label="Calendar"
|
||||||
onChange={(e: SelectChangeEvent) => {
|
onChange={(e: SelectChangeEvent) => {
|
||||||
const newId = Number(e.target.value);
|
const newId = Number(e.target.value);
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ export function HandleLogin() {
|
|||||||
initiateLogin();
|
initiateLogin();
|
||||||
}, [userData, dispatch]);
|
}, [userData, dispatch]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!calendars.pending && !userData.loading) {
|
if (userData.error) {
|
||||||
dispatch(push("/error"));
|
dispatch(push("/error"));
|
||||||
}
|
}
|
||||||
if (!calendars.pending && !userData.loading) {
|
if (!calendars.pending && !userData.loading && !userData.error) {
|
||||||
dispatch(push("/calendar"));
|
dispatch(push("/calendar"));
|
||||||
}
|
}
|
||||||
}, [calendars.pending, userData.loading]);
|
}, [calendars.pending, userData.loading]);
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||||
import { userData, userOrganiser } from "./userDataTypes";
|
import { userData, userOrganiser } from "./userDataTypes";
|
||||||
import { getOpenPaasUser } from "./userAPI";
|
import { getOpenPaasUser } from "./userAPI";
|
||||||
|
import { formatReduxError } from "../../utils/errorUtils";
|
||||||
|
|
||||||
export const getOpenPaasUserDataAsync = createAsyncThunk<any>(
|
export const getOpenPaasUserDataAsync = createAsyncThunk<
|
||||||
"user/getOpenPaasUserData",
|
Record<string, string>,
|
||||||
async () => {
|
void,
|
||||||
|
{ rejectValue: { message: string; status?: number } }
|
||||||
|
>("user/getOpenPaasUserData", async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
const user = (await getOpenPaasUser()) as Record<string, string>;
|
const user = (await getOpenPaasUser()) as Record<string, string>;
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue({
|
||||||
|
message: formatReduxError(err),
|
||||||
|
status: err.response?.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
export const userSlice = createSlice({
|
export const userSlice = createSlice({
|
||||||
name: "user",
|
name: "user",
|
||||||
@@ -18,6 +26,7 @@ export const userSlice = createSlice({
|
|||||||
organiserData: null as unknown as userOrganiser,
|
organiserData: null as unknown as userOrganiser,
|
||||||
tokens: null as unknown as Record<string, string>,
|
tokens: null as unknown as Record<string, string>,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
error: null as unknown as string | null,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setUserData: (state, action) => {
|
setUserData: (state, action) => {
|
||||||
@@ -32,6 +41,9 @@ export const userSlice = createSlice({
|
|||||||
setTokens: (state, action) => {
|
setTokens: (state, action) => {
|
||||||
state.tokens = action.payload;
|
state.tokens = action.payload;
|
||||||
},
|
},
|
||||||
|
clearError: (state) => {
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@@ -54,13 +66,17 @@ export const userSlice = createSlice({
|
|||||||
.addCase(getOpenPaasUserDataAsync.pending, (state) => {
|
.addCase(getOpenPaasUserDataAsync.pending, (state) => {
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
})
|
})
|
||||||
.addCase(getOpenPaasUserDataAsync.rejected, (state) => {
|
.addCase(getOpenPaasUserDataAsync.rejected, (state, action) => {
|
||||||
|
if (action.payload?.status !== 401) {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
state.error =
|
||||||
|
action.payload?.message || "Failed to fetch user information";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setUserData, setTokens } = userSlice.actions;
|
export const { setUserData, setTokens, clearError } = userSlice.actions;
|
||||||
|
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export function formatReduxError(error: unknown): string {
|
||||||
|
if (!error) return "Unknown error";
|
||||||
|
if (typeof error === "string") return error;
|
||||||
|
if (typeof error === "object") {
|
||||||
|
const err = error as any;
|
||||||
|
if (err?.message) return err.message;
|
||||||
|
}
|
||||||
|
return "Unexpected error occurred";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user