diff --git a/__test__/components/Calendar.test.tsx b/__test__/components/Calendar.test.tsx
index e43313e..04ff24e 100644
--- a/__test__/components/Calendar.test.tsx
+++ b/__test__/components/Calendar.test.tsx
@@ -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(
- ,
- preloadedState
- );
+ await act(async () => {
+ renderWithProviders(
+ ,
+ 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(
- ,
- preloadedState
- );
+ await act(async () => {
+ renderWithProviders(
+ ,
+ 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(, preloadedState);
+ it("BUGFIX: remove dots in mini calendar when unselecting personnal calendar", async () => {
+ await act(async () =>
+ renderWithProviders(, 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(, preloadedState);
+ it("BUGFIX: remove dots in mini calendar when unselecting delegated calendar", async () => {
+ await act(async () =>
+ renderWithProviders(, 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(, preloadedState);
+ it("BUGFIX: remove dots in mini calendar when unselecting shared calendar", async () => {
+ await act(async () =>
+ renderWithProviders(, 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(, preloadedState);
+ await act(async () =>
+ renderWithProviders(, 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(, preloadedState);
+ await act(async () =>
+ renderWithProviders(, 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(, preloadedState);
+ await act(async () =>
+ renderWithProviders(, 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(, preloadedState);
+ await act(async () =>
+ renderWithProviders(, 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(, {
- 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(, {
+ 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(, {
- user: preloadedState.user,
- calendars: {
- list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
- pending: false,
- },
- });
+ await act(async () =>
+ renderWithProviders(, {
+ 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(, preloadedState);
+ await act(async () =>
+ renderWithProviders(, 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");
diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx
index 30bae71..34ed2f1 100644
--- a/__test__/components/MiniCalendarColor.test.tsx
+++ b/__test__/components/MiniCalendarColor.test.tsx
@@ -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();
diff --git a/__test__/components/PeopleSearch.test.tsx b/__test__/components/PeopleSearch.test.tsx
index bfaae91..ae3bebd 100644
--- a/__test__/components/PeopleSearch.test.tsx
+++ b/__test__/components/PeopleSearch.test.tsx
@@ -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();
diff --git a/__test__/features/Calendars/TimezoneInGrid.test.tsx b/__test__/features/Calendars/TimezoneInGrid.test.tsx
index 3947444..5fc1e9d 100644
--- a/__test__/features/Calendars/TimezoneInGrid.test.tsx
+++ b/__test__/features/Calendars/TimezoneInGrid.test.tsx
@@ -37,11 +37,6 @@ describe("Calendar - Timezone Integration", () => {
timeZone: "America/New_York",
pending: false,
},
- events: {
- selectedEvent: null,
- isEditMode: false,
- editModeDialogOpen: false,
- },
};
beforeEach(() => {
diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx
index ff1aa09..8ab6129 100644
--- a/__test__/features/Events/EventDisplay.test.tsx
+++ b/__test__/features/Events/EventDisplay.test.tsx
@@ -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(
{
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(, { user: { loading: false } });
+ renderWithProviders(, { user: { error: true } });
expect(dispatch).toHaveBeenCalledWith(push("/error"));
});
});
diff --git a/src/App.tsx b/src/App.tsx
index 955f7fe..482fe61 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 (
}>
@@ -22,6 +32,8 @@ function App() {
} />
+
+
);
diff --git a/src/components/Attendees/AttendeeSearch.tsx b/src/components/Attendees/AttendeeSearch.tsx
index 2616053..91fa0ba 100644
--- a/src/components/Attendees/AttendeeSearch.tsx
+++ b/src/components/Attendees/AttendeeSearch.tsx
@@ -17,7 +17,7 @@ export default function UserSearch({
displayName: a.cn ?? "",
avatarUrl: "",
openpaasId: "",
- }))
+ })) ?? []
);
useEffect(() => {
setSelectedUsers(
diff --git a/src/components/Calendar/CalendarLayout.tsx b/src/components/Calendar/CalendarLayout.tsx
index 82988a3..a3b0c05 100644
--- a/src/components/Calendar/CalendarLayout.tsx
+++ b/src/components/Calendar/CalendarLayout.tsx
@@ -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(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(new Date());
@@ -62,6 +64,7 @@ export default function CalendarLayout() {
onDateChange={handleDateChange}
onViewChange={handleViewChange}
/>
+
);
}
diff --git a/src/components/Error/Error.tsx b/src/components/Error/Error.tsx
index a1034e5..8f0c35a 100644
--- a/src/components/Error/Error.tsx
+++ b/src/components/Error/Error.tsx
@@ -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 Error
;
+ 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 (
+
+
+
+
+
+
+
+
+
+ Something went wrong
+
+
+
+ {errorMessage}
+
+
+ }
+ onClick={() => {
+ window.location.reload();
+ }}
+ sx={{
+ textTransform: "none",
+ fontWeight: 600,
+ borderRadius: 2,
+ px: 3,
+ py: 1,
+ boxShadow: "none",
+ }}
+ >
+ Try Again
+
+
+
+
+
+ );
}
diff --git a/src/components/Error/ErrorSnackbar.tsx b/src/components/Error/ErrorSnackbar.tsx
index fe111c1..11adcc3 100644
--- a/src/components/Error/ErrorSnackbar.tsx
+++ b/src/components/Error/ErrorSnackbar.tsx
@@ -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 (
+
+
+ OK
+
+ }
+ >
+ {error}
+
+
+ );
}
-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
diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts
index 797eb72..35ebb32 100644
--- a/src/features/Calendars/CalendarSlice.ts
+++ b/src/features/Calendars/CalendarSlice.ts
@@ -29,192 +29,283 @@ import {
import { User } from "../../components/Attendees/PeopleSearch";
import { getCalendarVisibility } from "../../components/Calendar/utils/calendarUtils";
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<
- Record // Return type
->("calendars/getCalendars", async () => {
- const importedCalendars: Record = {};
- const user = (await getOpenPaasUser()) as Record;
- const calendars = (await getCalendars(user.id)) as Record;
- const rawCalendars = calendars._embedded["dav:calendar"];
+ { importedCalendars: Record; errors: string }, // Return type
+ void, // Arg type
+ { rejectValue: RejectedError } // ThunkAPI config
+>("calendars/getCalendars", async (_, { rejectWithValue }) => {
+ try {
+ const importedCalendars: Record = {};
+ const user = (await getOpenPaasUser()) as Record;
+ const calendars = (await getCalendars(user.id)) as Record;
+ const rawCalendars = calendars._embedded["dav:calendar"];
+ const errors = [];
+ for (const cal of rawCalendars) {
+ const description = cal["caldav:description"];
+ let delegated = false;
+ let source = cal["calendarserver:source"]
+ ? cal["calendarserver:source"]._links.self.href
+ : cal._links.self.href;
+ const link = cal._links.self.href;
+ if (cal["calendarserver:delegatedsource"]) {
+ source = cal["calendarserver:delegatedsource"];
+ delegated = true;
+ }
+ const id = source.replace("/calendars/", "").replace(".json", "");
+ const ownerId = id.split("/")[0];
+ const visibility = getCalendarVisibility(cal["acl"]);
- for (const cal of rawCalendars) {
- const description = cal["caldav:description"];
- let delegated = false;
- let source = cal["calendarserver:source"]
- ? cal["calendarserver:source"]._links.self.href
- : cal._links.self.href;
- const link = cal._links.self.href;
- if (cal["calendarserver:delegatedsource"]) {
- source = cal["calendarserver:delegatedsource"];
- delegated = true;
+ // 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 =
+ ownerId !== user.id && cal["dav:name"] === "#default"
+ ? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
+ ownerData.lastname
+ }` + "'s calendar"
+ : cal["dav:name"];
+
+ const color = {
+ light: cal["apple:color"] ?? "#006BD8",
+ dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
+ };
+ importedCalendars[id] = {
+ id,
+ name,
+ link,
+ owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
+ ownerData.lastname
+ }`,
+ ownerEmails: ownerData.emails,
+ description,
+ delegated,
+ color,
+ visibility,
+ events: {},
+ };
}
- const id = source.replace("/calendars/", "").replace(".json", "");
- const ownerId = id.split("/")[0];
- const visibility = getCalendarVisibility(cal["acl"]);
- const ownerData: any = await getUserDetails(ownerId);
- const name =
- ownerId !== user.id && cal["dav:name"] === "#default"
- ? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
- ownerData.lastname
- }` + "'s calendar"
- : cal["dav:name"];
- const color = {
- light: cal["apple:color"] ?? "#006BD8",
- dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
- };
- importedCalendars[id] = {
- id,
- name,
- link,
- owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
- ownerData.lastname
- }`,
- ownerEmails: ownerData.emails,
- description,
- delegated,
- color,
- visibility,
- events: {},
- };
+ return { importedCalendars, errors: errors.join("\n") };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
}
-
- return importedCalendars;
});
export const getTempCalendarsListAsync = createAsyncThunk<
Record,
- User
->("calendars/getTempCalendars", async (tempUser) => {
- const importedCalendars: Record = {};
+ User,
+ { rejectValue: RejectedError }
+>("calendars/getTempCalendars", async (tempUser, { rejectWithValue }) => {
+ try {
+ const importedCalendars: Record = {};
- const calendars = (await getCalendars(
- tempUser.openpaasId ?? "",
- "sharedPublic=true&WithRights=true"
- )) as Record;
- const rawCalendars = calendars._embedded["dav:calendar"];
+ const calendars = (await getCalendars(
+ tempUser.openpaasId ?? "",
+ "sharedPublic=true&WithRights=true"
+ )) as Record;
+ const rawCalendars = calendars._embedded["dav:calendar"];
- for (const cal of rawCalendars) {
- const name = cal["dav:name"];
- const description = cal["caldav:description"];
- const delegated = cal["calendarserver:delegatedsource"] ? true : false;
- const source = cal["calendarserver:source"]
- ? cal["calendarserver:source"]._links.self.href
- : cal._links.self.href;
- const link = cal._links.self.href;
+ for (const cal of rawCalendars) {
+ const name = cal["dav:name"];
+ const description = cal["caldav:description"];
+ const delegated = cal["calendarserver:delegatedsource"] ? true : false;
+ const source = cal["calendarserver:source"]
+ ? cal["calendarserver:source"]._links.self.href
+ : cal._links.self.href;
+ const link = cal._links.self.href;
- const id = source.replace("/calendars/", "").replace(".json", "");
- const visibility = getCalendarVisibility(cal["acl"]);
- const ownerData: any = await getUserDetails(id.split("/")[0]);
+ const id = source.replace("/calendars/", "").replace(".json", "");
+ const visibility = getCalendarVisibility(cal["acl"]);
+ const ownerData: any = await getUserDetails(id.split("/")[0]);
- importedCalendars[id] = {
- id,
- name,
- link,
- owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}`,
- ownerEmails: ownerData.emails,
- description,
- delegated,
- color: {
- light: tempUser.color?.light ?? "#a8a8a8ff",
- dark: tempUser.color?.dark ?? "#a8a8a8ff",
- },
- visibility,
- events: {},
- };
+ importedCalendars[id] = {
+ id,
+ name,
+ link,
+ owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}`,
+ ownerEmails: ownerData.emails,
+ description,
+ delegated,
+ color: {
+ light: tempUser.color?.light ?? "#a8a8a8ff",
+ dark: tempUser.color?.dark ?? "#a8a8a8ff",
+ },
+ visibility,
+ events: {},
+ };
+ }
+
+ return importedCalendars;
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
}
-
- return importedCalendars;
});
export const getCalendarDetailAsync = createAsyncThunk<
- { calId: string; events: CalendarEvent[]; calType?: string }, // Return type
- { calId: string; match: { start: string; end: string }; calType?: string } // Arg type
->("calendars/getCalendarDetails", async ({ calId, match, calType }) => {
- const calendar = (await getCalendar(calId, match)) as Record;
- const color = calendar["apple:color"];
- const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
- (eventdata: any) => {
- const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []]
- const valarm = eventdata.data[2][0][2][0];
- const eventURL = eventdata._links.self.href;
- return vevents.map((vevent: any[]) => {
- return parseCalendarEvent(vevent[1], color, calId, eventURL, valarm);
+ { calId: string; events: CalendarEvent[]; calType?: string },
+ { calId: string; match: { start: string; end: string }; calType?: string },
+ { rejectValue: RejectedError }
+>(
+ "calendars/getCalendarDetails",
+ async ({ calId, match, calType }, { rejectWithValue }) => {
+ try {
+ const calendar = (await getCalendar(calId, match)) as Record;
+ const color = calendar["apple:color"];
+ const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap(
+ (eventdata: any) => {
+ const vevents = eventdata.data[2] as any[][];
+ const valarm = eventdata.data[2][0][2][0];
+ const eventURL = eventdata._links.self.href;
+ return vevents.map((vevent: any[]) => {
+ return parseCalendarEvent(
+ vevent[1],
+ color,
+ calId,
+ eventURL,
+ valarm
+ );
+ });
+ }
+ );
+
+ return { calId, events, calType };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
});
}
- );
-
- return { calId, events, calType };
-});
+ }
+);
export const putEventAsync = createAsyncThunk<
- { calId: string; events: CalendarEvent[]; calType?: "temp" }, // Return type
- { cal: Calendars; newEvent: CalendarEvent; calType?: "temp" } // Arg type
->("calendars/putEvent", async ({ cal, newEvent, calType }) => {
- await putEvent(newEvent, cal.ownerEmails ? cal.ownerEmails[0] : undefined);
- const eventDate = new Date(newEvent.start);
+ { calId: string; events: CalendarEvent[]; calType?: "temp" },
+ { cal: Calendars; newEvent: CalendarEvent; calType?: "temp" },
+ { rejectValue: RejectedError }
+>(
+ "calendars/putEvent",
+ async ({ cal, newEvent, calType }, { rejectWithValue }) => {
+ try {
+ await putEvent(
+ newEvent,
+ cal.ownerEmails ? cal.ownerEmails[0] : undefined
+ );
+ 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, {
- start: formatDateToYYYYMMDDTHHMMSS(weekStart),
- end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
- })) as Record;
- const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
- (eventdata: any) => {
- const vevents = eventdata.data[2] as any[][];
- const eventURL = eventdata._links.self.href;
- const valarm = eventdata.data[2][0][2][0];
- return vevents.map((vevent: any[]) => {
- return parseCalendarEvent(
- vevent[1],
- cal.color ?? {},
- cal.id,
- eventURL,
- valarm
- );
+ const calEvents = (await getCalendar(cal.id, {
+ start: formatDateToYYYYMMDDTHHMMSS(weekStart),
+ end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
+ })) as Record;
+ const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
+ (eventdata: any) => {
+ const vevents = eventdata.data[2] as any[][];
+ const eventURL = eventdata._links.self.href;
+ const valarm = eventdata.data[2][0][2][0];
+ return vevents.map((vevent: any[]) => {
+ return parseCalendarEvent(
+ vevent[1],
+ cal.color ?? {},
+ cal.id,
+ eventURL,
+ valarm
+ );
+ });
+ }
+ );
+
+ return {
+ calId: cal.id,
+ events,
+ calType,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
});
}
- );
-
- return {
- calId: cal.id,
- events,
- calType,
- };
-});
+ }
+);
export const getEventAsync = createAsyncThunk<
- { calId: string; event: CalendarEvent }, // Return type
- CalendarEvent // Arg type
->("calendars/getEvent", async (event) => {
- const response: CalendarEvent = await getEvent(event);
- return {
- calId: event.calId,
- event: response,
- };
+ { calId: string; event: CalendarEvent },
+ CalendarEvent,
+ { rejectValue: RejectedError }
+>("calendars/getEvent", async (event, { rejectWithValue }) => {
+ try {
+ const response: CalendarEvent = await getEvent(event);
+ return {
+ calId: event.calId,
+ event: response,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
});
+
export const patchCalendarAsync = createAsyncThunk<
{
calId: string;
calLink: string;
patch: { name: string; desc: string; color: Record };
- }, // Return type
+ },
{
calId: string;
calLink: string;
patch: { name: string; desc: string; color: Record };
- } // Arg type
->("calendars/patchCalendar", async ({ calId, calLink, patch }) => {
- await proppatchCalendar(calLink, patch);
- return {
- calId,
- calLink,
- patch,
- };
-});
+ },
+ { rejectValue: RejectedError }
+>(
+ "calendars/patchCalendar",
+ async ({ calId, calLink, patch }, { rejectWithValue }) => {
+ try {
+ await proppatchCalendar(calLink, patch);
+ return {
+ calId,
+ calLink,
+ patch,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const removeCalendarAsync = createAsyncThunk<
{
@@ -223,95 +314,173 @@ export const removeCalendarAsync = createAsyncThunk<
{
calId: string;
calLink: string;
- }
->("calendars/removeCalendar", async ({ calId, calLink }) => {
- await removeCalendar(calLink);
- return {
- calId,
- calLink,
- };
-});
-
-export const moveEventAsync = createAsyncThunk<
- { calId: string; events: CalendarEvent[] }, // Return type
- { cal: Calendars; newEvent: CalendarEvent; newURL: string } // Arg type
->("calendars/moveEvent", async ({ cal, 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 { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
-
- const calEvents = (await getCalendar(cal.id, {
- start: formatDateToYYYYMMDDTHHMMSS(weekStart),
- end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
- })) as Record;
- const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
- (eventdata: any) => {
- const vevents = eventdata.data[2] as any[][];
- const eventURL = eventdata._links.self.href;
- return vevents.map((vevent: any[]) => {
- return parseCalendarEvent(vevent[1], cal.color ?? {}, cal.id, eventURL);
+ },
+ { rejectValue: RejectedError }
+>(
+ "calendars/removeCalendar",
+ async ({ calId, calLink }, { rejectWithValue }) => {
+ try {
+ await removeCalendar(calLink);
+ return {
+ calId,
+ calLink,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
});
}
- );
+ }
+);
- return {
- calId: cal.id,
- events,
- };
-});
+export const moveEventAsync = createAsyncThunk<
+ { calId: string; events: CalendarEvent[] },
+ { cal: Calendars; newEvent: CalendarEvent; newURL: string },
+ { rejectValue: RejectedError }
+>(
+ "calendars/moveEvent",
+ async ({ cal, newEvent, newURL }, { rejectWithValue }) => {
+ try {
+ await moveEvent(newEvent, newURL);
+
+ const eventDate = new Date(newEvent.start);
+ const { start: weekStart, end: weekEnd } = computeWeekRange(eventDate);
+
+ const calEvents = (await getCalendar(cal.id, {
+ start: formatDateToYYYYMMDDTHHMMSS(weekStart),
+ end: formatDateToYYYYMMDDTHHMMSS(weekEnd),
+ })) as Record;
+ const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
+ (eventdata: any) => {
+ const vevents = eventdata.data[2] as any[][];
+ const eventURL = eventdata._links.self.href;
+ return vevents.map((vevent: any[]) => {
+ return parseCalendarEvent(
+ vevent[1],
+ cal.color ?? {},
+ cal.id,
+ eventURL
+ );
+ });
+ }
+ );
+
+ return {
+ calId: cal.id,
+ events,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const patchACLCalendarAsync = createAsyncThunk<
{
calId: string;
calLink: string;
request: string;
- }, // Return type
+ },
{
calId: string;
calLink: string;
request: string;
- } // Arg type
->("calendars/requestACLCalendar", async ({ calId, calLink, request }) => {
- const response = await updateAclCalendar(calLink, request);
- return {
- calId,
- calLink,
- request,
- };
-});
+ },
+ { rejectValue: RejectedError }
+>(
+ "calendars/requestACLCalendar",
+ async ({ calId, calLink, request }, { rejectWithValue }) => {
+ try {
+ const response = await updateAclCalendar(calLink, request);
+ return {
+ calId,
+ calLink,
+ request,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const deleteEventAsync = createAsyncThunk<
- { calId: string; eventId: string }, // Return type
- { calId: string; eventId: string; eventURL: string } // Arg type
->("calendars/delEvent", async ({ calId, eventId, eventURL }) => {
- await deleteEvent(eventURL);
- return { calId, eventId };
-});
+ { calId: string; eventId: string },
+ { calId: string; eventId: string; eventURL: string },
+ { rejectValue: RejectedError }
+>(
+ "calendars/delEvent",
+ async ({ calId, eventId, eventURL }, { rejectWithValue }) => {
+ try {
+ await deleteEvent(eventURL);
+ return { calId, eventId };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const deleteEventInstanceAsync = createAsyncThunk<
{ calId: string; eventId: string },
- { cal: Calendars; event: CalendarEvent }
->("calendars/delEventInstance", async ({ cal, event }) => {
- await deleteEventInstance(event, cal.ownerEmails?.[0]);
- return { calId: cal.id, eventId: event.uid };
+ { cal: Calendars; event: CalendarEvent },
+ { rejectValue: RejectedError }
+>("calendars/delEventInstance", async ({ cal, event }, { rejectWithValue }) => {
+ try {
+ await deleteEventInstance(event, cal.ownerEmails?.[0]);
+ return { calId: cal.id, eventId: event.uid };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
});
export const updateEventInstanceAsync = createAsyncThunk<
{ calId: string; event: CalendarEvent },
- { cal: Calendars; event: CalendarEvent }
->("calendars/updateEventInstance", async ({ cal, event }) => {
- await putEventWithOverrides(event, cal.ownerEmails?.[0]);
- return { calId: cal.id, event };
-});
+ { cal: Calendars; event: CalendarEvent },
+ { rejectValue: RejectedError }
+>(
+ "calendars/updateEventInstance",
+ async ({ cal, event }, { rejectWithValue }) => {
+ try {
+ await putEventWithOverrides(event, cal.ownerEmails?.[0]);
+ return { calId: cal.id, event };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const updateSeriesAsync = createAsyncThunk<
void,
- { cal: Calendars; event: CalendarEvent; removeOverrides?: boolean }
->("calendars/updateSeries", async ({ cal, event, removeOverrides = true }) => {
- await updateSeries(event, cal.ownerEmails?.[0], removeOverrides);
-});
+ { cal: Calendars; event: CalendarEvent; removeOverrides?: boolean },
+ { rejectValue: RejectedError }
+>(
+ "calendars/updateSeries",
+ async ({ cal, event, removeOverrides = true }, { rejectWithValue }) => {
+ try {
+ await updateSeries(event, cal.ownerEmails?.[0], removeOverrides);
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const createCalendarAsync = createAsyncThunk<
{
@@ -322,30 +491,41 @@ export const createCalendarAsync = createAsyncThunk<
desc: string;
owner: string;
ownerEmails: string[];
- }, // Return type
+ },
{
userId: string;
calId: string;
color: Record;
name: string;
desc: string;
- } // Arg type
->("calendars/createCalendar", async ({ userId, calId, color, name, desc }) => {
- await postCalendar(userId, calId, color, name, desc);
- const ownerData: any = await getUserDetails(userId.split("/")[0]);
+ },
+ { rejectValue: RejectedError }
+>(
+ "calendars/createCalendar",
+ async ({ userId, calId, color, name, desc }, { rejectWithValue }) => {
+ try {
+ await postCalendar(userId, calId, color, name, desc);
+ const ownerData: any = await getUserDetails(userId.split("/")[0]);
- return {
- userId,
- calId,
- color,
- name,
- desc,
- owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
- ownerData.lastname
- }`,
- ownerEmails: ownerData.emails,
- };
-});
+ return {
+ userId,
+ calId,
+ color,
+ name,
+ desc,
+ owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
+ ownerData.lastname
+ }`,
+ ownerEmails: ownerData.emails,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const addSharedCalendarAsync = createAsyncThunk<
{
@@ -356,49 +536,68 @@ export const addSharedCalendarAsync = createAsyncThunk<
desc: string;
owner: string;
ownerEmails: string[];
- }, // Return type
- { userId: string; calId: string; cal: Record } // Arg type
->("calendars/addSharedCalendar", async ({ userId, calId, cal }) => {
- await addSharedCalendar(userId, calId, cal);
- const ownerData: any = await getUserDetails(
- cal.cal._links.self.href
- .replace("/calendars/", "")
- .replace(".json", "")
- .split("/")[0]
- );
+ },
+ { userId: string; calId: string; cal: Record },
+ { rejectValue: RejectedError }
+>(
+ "calendars/addSharedCalendar",
+ async ({ userId, calId, cal }, { rejectWithValue }) => {
+ try {
+ await addSharedCalendar(userId, calId, cal);
+ const ownerData: any = await getUserDetails(
+ cal.cal._links.self.href
+ .replace("/calendars/", "")
+ .replace(".json", "")
+ .split("/")[0]
+ );
- return {
- calId: cal.cal._links.self.href
- .replace("/calendars/", "")
- .replace(".json", ""),
- color: {
- light: cal.cal["apple:color"],
- dark: "#000",
- },
- link: `/calendars/${userId}/${calId}.json`,
- desc: cal.cal["caldav:description"],
- name:
- ownerData.id !== userId && cal.cal["dav:name"] === "#default"
- ? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
- ownerData.lastname
- }` + "'s calendar"
- : cal.cal["dav:name"],
- owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
- ownerData.lastname
- }`,
- ownerEmails: ownerData.emails,
- };
-});
+ return {
+ calId: cal.cal._links.self.href
+ .replace("/calendars/", "")
+ .replace(".json", ""),
+ color: {
+ light: cal.cal["apple:color"],
+ dark: "#000",
+ },
+ link: `/calendars/${userId}/${calId}.json`,
+ desc: cal.cal["caldav:description"],
+ name:
+ ownerData.id !== userId && cal.cal["dav:name"] === "#default"
+ ? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
+ ownerData.lastname
+ }` + "'s calendar"
+ : cal.cal["dav:name"],
+ owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
+ ownerData.lastname
+ }`,
+ ownerEmails: ownerData.emails,
+ };
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
+ }
+ }
+);
export const importEventFromFileAsync = createAsyncThunk<
void,
{
calLink: string;
file: File;
+ },
+ { rejectValue: RejectedError }
+>("calendars/importEvent", async ({ calLink, file }, { rejectWithValue }) => {
+ try {
+ const id = ((await importFile(file)) as Record)._id;
+ const response = await importEventFromFile(id, calLink);
+ } catch (err: any) {
+ return rejectWithValue({
+ message: formatReduxError(err),
+ status: err.response?.status,
+ });
}
->("calendars/importEvent", async ({ calLink, file }) => {
- const id = ((await importFile(file)) as Record)._id;
- const response = await importEventFromFile(id, calLink);
});
const CalendarSlice = createSlice({
@@ -407,11 +606,13 @@ const CalendarSlice = createSlice({
list: {} as Record,
templist: {} as Record,
pending: false,
+ error: null as string | null,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
} as {
list: Record;
templist: Record;
pending: boolean;
+ error: string | null;
timeZone: string;
},
reducers: {
@@ -480,6 +681,9 @@ const CalendarSlice = createSlice({
if (!state.list[action.payload]) return;
state.list[action.payload].lastCacheCleared = Date.now();
},
+ clearError: (state) => {
+ state.error = null;
+ },
updateCalColor: (
state,
action: PayloadAction<{
@@ -492,11 +696,21 @@ const CalendarSlice = createSlice({
},
extraReducers: (builder) => {
builder
+ // Fulfilled cases
.addCase(
getCalendarsListAsync.fulfilled,
- (state, action: PayloadAction>) => {
+ (
+ state,
+ action: PayloadAction<{
+ importedCalendars: Record;
+ errors: string;
+ }>
+ ) => {
state.pending = false;
- state.list = action.payload;
+ state.list = action.payload.importedCalendars;
+ state.error = action.payload.errors.length
+ ? action.payload.errors
+ : null;
}
)
.addCase(
@@ -636,18 +850,22 @@ const CalendarSlice = createSlice({
action.payload.eventId
];
}
+ state.error = null;
})
.addCase(deleteEventInstanceAsync.fulfilled, (state, action) => {
state.pending = false;
delete state.list[action.payload.calId].events[action.payload.eventId];
+ state.error = null;
})
.addCase(updateEventInstanceAsync.fulfilled, (state, action) => {
state.pending = false;
state.list[action.payload.calId].events[action.payload.event.uid] =
action.payload.event;
+ state.error = null;
})
.addCase(updateSeriesAsync.fulfilled, (state) => {
state.pending = false;
+ state.error = null;
})
.addCase(createCalendarAsync.fulfilled, (state, action) => {
state.pending = false;
@@ -661,6 +879,7 @@ const CalendarSlice = createSlice({
ownerEmails: action.payload.ownerEmails,
events: {},
} as Calendars;
+ state.error = null;
})
.addCase(patchCalendarAsync.fulfilled, (state, action) => {
state.pending = false;
@@ -687,6 +906,7 @@ const CalendarSlice = createSlice({
name: action.payload.patch.name,
};
}
+ state.error = null;
})
.addCase(addSharedCalendarAsync.fulfilled, (state, action) => {
state.pending = false;
@@ -700,16 +920,24 @@ const CalendarSlice = createSlice({
owner: action.payload.owner,
ownerEmails: action.payload.ownerEmails,
} as Calendars;
+ state.error = null;
})
.addCase(removeCalendarAsync.fulfilled, (state, action) => {
state.pending = false;
delete state.list[action.payload.calId];
+ state.error = null;
})
.addCase(patchACLCalendarAsync.fulfilled, (state, action) => {
state.pending = false;
state.list[action.payload.calId].visibility =
action.payload.request !== "" ? "public" : "private";
+ state.error = null;
})
+ .addCase(importEventFromFileAsync.fulfilled, (state) => {
+ state.pending = false;
+ state.error = null;
+ })
+ // Pending cases
.addCase(getCalendarDetailAsync.pending, (state) => {
state.pending = true;
})
@@ -728,6 +956,15 @@ const CalendarSlice = createSlice({
.addCase(deleteEventAsync.pending, (state) => {
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) => {
state.pending = true;
})
@@ -745,6 +982,124 @@ const CalendarSlice = createSlice({
})
.addCase(patchACLCalendarAsync.pending, (state) => {
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,
setTimeZone,
clearFetchCache,
+ clearError,
updateCalColor,
} = CalendarSlice.actions;
export default CalendarSlice.reducer;
diff --git a/src/features/Events/EventApi.ts b/src/features/Events/EventApi.ts
index c4030b8..03562b4 100644
--- a/src/features/Events/EventApi.ts
+++ b/src/features/Events/EventApi.ts
@@ -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(
diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx
index ae1a9c8..9c889b8 100644
--- a/src/features/Events/EventDisplay.tsx
+++ b/src/features/Events/EventDisplay.tsx
@@ -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(
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({