#204 Error management (#237)

* 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:
Camille Moussu
2025-10-27 11:23:38 +01:00
committed by GitHub
parent 4774e78543
commit 42f5d8e1a9
17 changed files with 960 additions and 371 deletions
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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(
+3 -3
View File
@@ -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);
+2 -2
View File
@@ -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]);
+24 -8
View File
@@ -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;