* 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 { searchUsers } from "../../src/features/User/userAPI";
|
||||
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 CalendarLayout from "../../src/components/Calendar/CalendarLayout";
|
||||
@@ -137,10 +137,12 @@ describe("CalendarSelection", () => {
|
||||
};
|
||||
it("renders calendars", async () => {
|
||||
const mockCalendarRef = { current: null };
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
||||
@@ -149,12 +151,14 @@ describe("CalendarSelection", () => {
|
||||
expect(screen.getByLabelText("Calendar delegated")).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 };
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CalendarApp calendarRef={mockCalendarRef} />,
|
||||
preloadedState
|
||||
);
|
||||
});
|
||||
expect(screen.getByText("Personnal Calendars")).toBeInTheDocument();
|
||||
expect(screen.getByText("Delegated Calendars")).toBeInTheDocument();
|
||||
expect(screen.getByText("Other Calendars")).toBeInTheDocument();
|
||||
@@ -168,14 +172,20 @@ describe("CalendarSelection", () => {
|
||||
.closest(".MuiAccordionSummary-root");
|
||||
|
||||
const addButton = screen.getAllByTestId("AddIcon")[2];
|
||||
fireEvent.click(addButton);
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||
|
||||
fireEvent.click(addButton);
|
||||
await act(async () => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
expect(sharedAccordionSummary).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
it("BUGFIX: remove dots in mini calendar when unselecting personnal calendar", () => {
|
||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
||||
it("BUGFIX: remove dots in mini calendar when unselecting personnal calendar", async () => {
|
||||
await act(async () =>
|
||||
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||
);
|
||||
|
||||
const checkbox = screen.getByLabelText("Calendar personnal");
|
||||
// checkbox checked : events shown
|
||||
@@ -186,8 +196,9 @@ describe("CalendarSelection", () => {
|
||||
).toHaveClass("event-dot");
|
||||
|
||||
// checkbox unchecked : events hidden
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId(
|
||||
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
||||
@@ -195,18 +206,24 @@ describe("CalendarSelection", () => {
|
||||
).not.toHaveClass("event-dot");
|
||||
|
||||
// checkbox rechecked : events shown
|
||||
fireEvent.click(checkbox);
|
||||
await act(async () => {
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId(
|
||||
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
||||
)
|
||||
).toHaveClass("event-dot");
|
||||
});
|
||||
it("BUGFIX: remove dots in mini calendar when unselecting delegated calendar", () => {
|
||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
||||
it("BUGFIX: remove dots in mini calendar when unselecting delegated calendar", async () => {
|
||||
await act(async () =>
|
||||
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||
);
|
||||
|
||||
// hide personnal event first
|
||||
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
||||
});
|
||||
const checkbox = screen.getByLabelText("Calendar delegated");
|
||||
|
||||
expect(
|
||||
@@ -216,18 +233,24 @@ describe("CalendarSelection", () => {
|
||||
).not.toHaveClass("event-dot");
|
||||
|
||||
// checkbox checked : events shown
|
||||
fireEvent.click(checkbox);
|
||||
await act(async () => {
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId(
|
||||
`date-${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`
|
||||
)
|
||||
).toHaveClass("event-dot");
|
||||
});
|
||||
it("BUGFIX: remove dots in mini calendar when unselecting shared calendar", () => {
|
||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
||||
it("BUGFIX: remove dots in mini calendar when unselecting shared calendar", async () => {
|
||||
await act(async () =>
|
||||
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||
);
|
||||
|
||||
// hide personnal event first
|
||||
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText("Calendar personnal"));
|
||||
});
|
||||
const checkbox = screen.getByLabelText("Calendar shared");
|
||||
|
||||
// checkbox unchecked : events hidden
|
||||
@@ -238,7 +261,9 @@ describe("CalendarSelection", () => {
|
||||
).not.toHaveClass("event-dot");
|
||||
|
||||
// checkbox checked : events shown
|
||||
fireEvent.click(checkbox);
|
||||
await act(async () => {
|
||||
fireEvent.click(checkbox);
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId(
|
||||
`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);
|
||||
userEvent.type(input, "New");
|
||||
|
||||
act(() => {
|
||||
userEvent.type(input, "New");
|
||||
});
|
||||
const option = await screen.findByText("New User");
|
||||
fireEvent.click(option);
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -317,14 +347,17 @@ describe("calendar Availability search", () => {
|
||||
.mockImplementation((payload) => {
|
||||
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);
|
||||
userEvent.type(input, "Alice");
|
||||
await act(async () => userEvent.type(input, "Alice"));
|
||||
|
||||
const option = await screen.findByText("Alice");
|
||||
fireEvent.click(option);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
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);
|
||||
userEvent.type(input, "New");
|
||||
await act(async () => userEvent.type(input, "New"));
|
||||
|
||||
const option = await screen.findByText("New User");
|
||||
fireEvent.click(option);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /create event/i }));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: /create event/i }));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText(/Create Event/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);
|
||||
userEvent.type(input, "New");
|
||||
await act(async () => userEvent.type(input, "New"));
|
||||
|
||||
const option = await screen.findByText("New User");
|
||||
fireEvent.click(option);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(option);
|
||||
});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText(/Create Event/i)).toHaveLength(2);
|
||||
@@ -394,14 +435,16 @@ describe("calendar Availability search", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("BUGFIX: can untoggle all personnal calendars", () => {
|
||||
renderWithProviders(<CalendarTestWrapper />, {
|
||||
user: preloadedState.user,
|
||||
calendars: {
|
||||
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
||||
pending: false,
|
||||
},
|
||||
});
|
||||
it("BUGFIX: can untoggle all personnal calendars", async () => {
|
||||
await act(async () =>
|
||||
renderWithProviders(<CalendarTestWrapper />, {
|
||||
user: preloadedState.user,
|
||||
calendars: {
|
||||
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
||||
pending: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const checkbox = screen.getByLabelText("Calendar personnal");
|
||||
expect(checkbox).toBeChecked();
|
||||
@@ -412,13 +455,15 @@ describe("calendar Availability search", () => {
|
||||
});
|
||||
|
||||
it("BUGFIX: monthview doesn't show days numbers in banner", async () => {
|
||||
renderWithProviders(<CalendarLayout />, {
|
||||
user: preloadedState.user,
|
||||
calendars: {
|
||||
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
||||
pending: false,
|
||||
},
|
||||
});
|
||||
await act(async () =>
|
||||
renderWithProviders(<CalendarLayout />, {
|
||||
user: preloadedState.user,
|
||||
calendars: {
|
||||
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
|
||||
pending: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const calendarRef = (window as any).__calendarRef;
|
||||
|
||||
@@ -427,8 +472,9 @@ describe("calendar Availability search", () => {
|
||||
});
|
||||
|
||||
const calendarApi = calendarRef.current;
|
||||
|
||||
calendarApi.changeView("dayGridMonth");
|
||||
await act(async () => {
|
||||
calendarApi.changeView("dayGridMonth");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByRole("columnheader").length).toBe(14);
|
||||
});
|
||||
@@ -452,7 +498,9 @@ describe("calendar Availability search", () => {
|
||||
return () => Promise.resolve(payload) as any;
|
||||
});
|
||||
jest.useFakeTimers().setSystemTime(new Date("2025-01-01"));
|
||||
renderWithProviders(<CalendarLayout />, preloadedState);
|
||||
await act(async () =>
|
||||
renderWithProviders(<CalendarLayout />, preloadedState)
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
@@ -461,9 +509,10 @@ describe("calendar Availability search", () => {
|
||||
const calendarRef = (window as any).__calendarRef;
|
||||
const calendarApi = calendarRef.current;
|
||||
const view = calendarApi?.view;
|
||||
calendarApi.changeView("dayGridMonth");
|
||||
fireEvent.click(screen.getByTestId("ChevronRightIcon"));
|
||||
|
||||
await act(async () => {
|
||||
calendarApi.changeView("dayGridMonth");
|
||||
fireEvent.click(screen.getByTestId("ChevronRightIcon"));
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(4);
|
||||
const callArgs = spy.mock.calls[3][0];
|
||||
expect(callArgs.calId).toBe("user1/cal1");
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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 CalendarApp from "../../src/components/Calendar/Calendar";
|
||||
import * as appHooks from "../../src/app/hooks";
|
||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||
import preview from "jest-preview";
|
||||
|
||||
describe("MiniCalendar", () => {
|
||||
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 {
|
||||
User,
|
||||
@@ -51,7 +51,9 @@ describe("PeopleSearch", () => {
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
jest.advanceTimersByTime(300);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedSearchUsers).toHaveBeenCalledWith("Test", ["user"]);
|
||||
@@ -64,7 +66,9 @@ describe("PeopleSearch", () => {
|
||||
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
jest.advanceTimersByTime(300);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
const option = await screen.findByText("Test User");
|
||||
await userEvent.click(option);
|
||||
@@ -79,7 +83,9 @@ describe("PeopleSearch", () => {
|
||||
setup([baseUser]);
|
||||
const input = screen.getByRole("combobox");
|
||||
await userEvent.type(input, "Test");
|
||||
jest.advanceTimersByTime(300);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("test@example.com")).not.toBeInTheDocument();
|
||||
|
||||
@@ -37,11 +37,6 @@ describe("Calendar - Timezone Integration", () => {
|
||||
timeZone: "America/New_York",
|
||||
pending: false,
|
||||
},
|
||||
events: {
|
||||
selectedEvent: null,
|
||||
isEditMode: false,
|
||||
editModeDialogOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -104,6 +104,26 @@ describe("Event Preview Display", () => {
|
||||
};
|
||||
|
||||
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(
|
||||
<EventPreviewModal
|
||||
open={true}
|
||||
|
||||
@@ -73,9 +73,9 @@ describe("HandleLogin", () => {
|
||||
|
||||
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();
|
||||
renderWithProviders(<HandleLogin />, { user: { loading: false } });
|
||||
renderWithProviders(<HandleLogin />, { user: { error: true } });
|
||||
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 { HistoryRouter as Router } from "redux-first-history/rr6";
|
||||
import { CallbackResume } from "./features/User/LoginCallback";
|
||||
@@ -9,8 +9,18 @@ import HandleLogin from "./features/User/HandleLogin";
|
||||
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
||||
import { Error } from "./components/Error/Error";
|
||||
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() {
|
||||
const error = useAppSelector((state) => state.user.error);
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
dispatch(push("/error"));
|
||||
}
|
||||
});
|
||||
return (
|
||||
<CustomThemeProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
@@ -22,6 +32,8 @@ function App() {
|
||||
<Route path="/error" element={<Error />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<ErrorSnackbar error={error} type="user" />
|
||||
<ErrorSnackbar error={error} type="user" />
|
||||
</Suspense>
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function UserSearch({
|
||||
displayName: a.cn ?? "",
|
||||
avatarUrl: "",
|
||||
openpaasId: "",
|
||||
}))
|
||||
})) ?? []
|
||||
);
|
||||
useEffect(() => {
|
||||
setSelectedUsers(
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useAppDispatch } from "../../app/hooks";
|
||||
import { getCalendarRange } from "../../utils/dateUtils";
|
||||
import { useAppSelector } from "../../app/hooks";
|
||||
import { refreshCalendars } from "../Event/utils/eventUtils";
|
||||
import { ErrorSnackbar } from "../Error/ErrorSnackbar";
|
||||
|
||||
export default function CalendarLayout() {
|
||||
const calendarRef = useRef<any>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const error = useAppSelector((state) => state.calendars.error);
|
||||
const selectedCalendars = useAppSelector((state) => state.calendars.list);
|
||||
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||
@@ -62,6 +64,7 @@ export default function CalendarLayout() {
|
||||
onDateChange={handleDateChange}
|
||||
onViewChange={handleViewChange}
|
||||
/>
|
||||
<ErrorSnackbar error={error} type="calendar" />
|
||||
</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() {
|
||||
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 Alert from "@mui/material/Alert";
|
||||
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 {
|
||||
messages: string[];
|
||||
onClose: () => void;
|
||||
export function ErrorSnackbar({
|
||||
error,
|
||||
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 summary =
|
||||
messages.length === 1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,24 +14,24 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
||||
const eventData = await response.text();
|
||||
|
||||
const eventical = ICAL.parse(eventData);
|
||||
|
||||
const vevents = (eventical[2] || []).filter(
|
||||
([name]: [string]) => name.toLowerCase() === "vevent"
|
||||
);
|
||||
let targetVevent;
|
||||
if (isMaster) {
|
||||
// Find master VEVENT (the one without recurrence-id)
|
||||
const vevents = eventical[2].filter(
|
||||
([name]: string[]) => name === "vevent"
|
||||
);
|
||||
|
||||
targetVevent = vevents.find(
|
||||
([, props]: [string, any[]]) =>
|
||||
!props.find(([k]: string[]) => k.toLowerCase() === "recurrence-id")
|
||||
);
|
||||
if (!targetVevent) {
|
||||
// Fallback to first VEVENT if no master found
|
||||
targetVevent = eventical[2][1];
|
||||
targetVevent = vevents[0];
|
||||
}
|
||||
} else {
|
||||
// For non-master, use first VEVENT as before
|
||||
targetVevent = eventical[2][1];
|
||||
targetVevent = vevents[0];
|
||||
}
|
||||
|
||||
const eventjson = parseCalendarEvent(
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function EventDisplayModal({
|
||||
const [end, setEnd] = useState(
|
||||
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>(
|
||||
event?.repetition ?? ({} as RepetitionObject)
|
||||
);
|
||||
@@ -280,7 +280,7 @@ export default function EventDisplayModal({
|
||||
fullWidth
|
||||
disabled={!isOwn}
|
||||
label="Title"
|
||||
value={title}
|
||||
value={title ?? ""}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
size="small"
|
||||
margin="dense"
|
||||
@@ -366,7 +366,7 @@ export default function EventDisplayModal({
|
||||
<Select
|
||||
disabled={!isOwn}
|
||||
labelId="calendar-select-label"
|
||||
value={calendarid.toString()}
|
||||
value={calendarid.toString() ?? ""}
|
||||
label="Calendar"
|
||||
onChange={(e: SelectChangeEvent) => {
|
||||
const newId = Number(e.target.value);
|
||||
|
||||
@@ -47,10 +47,10 @@ export function HandleLogin() {
|
||||
initiateLogin();
|
||||
}, [userData, dispatch]);
|
||||
useEffect(() => {
|
||||
if (!calendars.pending && !userData.loading) {
|
||||
if (userData.error) {
|
||||
dispatch(push("/error"));
|
||||
}
|
||||
if (!calendars.pending && !userData.loading) {
|
||||
if (!calendars.pending && !userData.loading && !userData.error) {
|
||||
dispatch(push("/calendar"));
|
||||
}
|
||||
}, [calendars.pending, userData.loading]);
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { userData, userOrganiser } from "./userDataTypes";
|
||||
import { getOpenPaasUser } from "./userAPI";
|
||||
import { formatReduxError } from "../../utils/errorUtils";
|
||||
|
||||
export const getOpenPaasUserDataAsync = createAsyncThunk<any>(
|
||||
"user/getOpenPaasUserData",
|
||||
async () => {
|
||||
export const getOpenPaasUserDataAsync = createAsyncThunk<
|
||||
Record<string, string>,
|
||||
void,
|
||||
{ rejectValue: { message: string; status?: number } }
|
||||
>("user/getOpenPaasUserData", async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const user = (await getOpenPaasUser()) as Record<string, string>;
|
||||
|
||||
return user;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const userSlice = createSlice({
|
||||
name: "user",
|
||||
@@ -18,6 +26,7 @@ export const userSlice = createSlice({
|
||||
organiserData: null as unknown as userOrganiser,
|
||||
tokens: null as unknown as Record<string, string>,
|
||||
loading: true,
|
||||
error: null as unknown as string | null,
|
||||
},
|
||||
reducers: {
|
||||
setUserData: (state, action) => {
|
||||
@@ -32,6 +41,9 @@ export const userSlice = createSlice({
|
||||
setTokens: (state, action) => {
|
||||
state.tokens = action.payload;
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
@@ -54,13 +66,17 @@ export const userSlice = createSlice({
|
||||
.addCase(getOpenPaasUserDataAsync.pending, (state) => {
|
||||
state.loading = true;
|
||||
})
|
||||
.addCase(getOpenPaasUserDataAsync.rejected, (state) => {
|
||||
state.loading = false;
|
||||
.addCase(getOpenPaasUserDataAsync.rejected, (state, action) => {
|
||||
if (action.payload?.status !== 401) {
|
||||
state.loading = false;
|
||||
state.error =
|
||||
action.payload?.message || "Failed to fetch user information";
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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