[#25] changed loading order + added condition to keep loading screen while loading calendar list

This commit is contained in:
Camille Moussu
2025-07-04 15:27:25 +02:00
parent 1b55f82e45
commit aae4d729f6
11 changed files with 216 additions and 86 deletions
+34 -32
View File
@@ -6,40 +6,51 @@ import interactionPlugin from "@fullcalendar/interaction";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import ReactCalendar from "react-calendar";
import "./Calendar.css";
import { useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import EventPopover from "../../features/Events/EventModal";
import CalendarPopover from "../../features/Calendars/CalendarModal";
import { CalendarEvent } from "../../features/Events/EventsTypes";
import {
getCalendar,
getCalendars,
} from "../../features/Calendars/CalendarApi";
import { access } from "node:fs";
import getOpenPaasUserId from "../../features/User/userAPI";
import { getCalendarsAsync } from "../../features/Calendars/CalendarSlice";
import { Calendars } from "../../features/Calendars/CalendarTypes";
import { parseCalendarEvent } from "../../features/Events/eventUtils";
import CalendarSelection from "./CalendarSelection";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
export default function CalendarApp() {
const calendarRef = useRef<CalendarApi | null>(null);
const [selectedDate, setSelectedDate] = useState(new Date());
const tokens = useAppSelector((state) => state.user.tokens);
const calendars = useAppSelector((state) => state.calendars);
const [selectedCalendars, setSelectedCalendars] = useState(
Object.keys(calendars).map((id) => id)
);
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars.list);
const pending = useAppSelector((state) => state.calendars.pending);
const userId = useAppSelector((state) => state.user.userData.openpaasId);
const [selectedCalendars, setSelectedCalendars] = useState<string[]>(
Object.keys(calendars).filter((id) => id.split("/")[0] === userId)
);
const fetchedIdsRef = useRef<Set<string>>(new Set());
let filteredEvents: CalendarEvent[] = [];
selectedCalendars.forEach((id) => {
filteredEvents = filteredEvents.concat(calendars[id].events);
});
const handleCalendarToggle = (name: string) => {
setSelectedCalendars((prev) =>
prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
);
};
useEffect(() => {
selectedCalendars.forEach((id) => {
const events = calendars[id]?.events ?? [];
const isEmpty = events.length === 0;
const notFetched = !fetchedIdsRef.current.has(id);
if (isEmpty && notFetched && !pending) {
dispatch(
getCalendarDetailAsync({
access_token: tokens.access_token,
calId: id,
})
);
fetchedIdsRef.current.add(id);
}
});
}, [selectedCalendars, calendars, pending, tokens.access_token, dispatch]);
const [date, setDate] = useState(new Date());
@@ -113,19 +124,10 @@ export default function CalendarApp() {
nextLabel={null}
showNavigation={false}
/>
{Object.keys(calendars).map((id) => (
<div key={id}>
<label>
<input
type="checkbox"
style={{ backgroundColor: calendars[id].color }}
checked={selectedCalendars.includes(id)}
onChange={() => handleCalendarToggle(id)}
/>
{calendars[id].name}
</label>
</div>
))}
<CalendarSelection
selectedCalendars={selectedCalendars}
setSelectedCalendars={setSelectedCalendars}
/>
<button onClick={() => setAnchorElCal(document.body)}>+</button>
</div>
<div className="calendar">
@@ -0,0 +1,61 @@
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
export default function CalendarSelection({
selectedCalendars,
setSelectedCalendars,
}: {
selectedCalendars: string[];
setSelectedCalendars: Function;
}) {
const tokens = useAppSelector((state) => state.user.tokens);
const userId = useAppSelector((state) => state.user.userData.openpaasId);
const dispatch = useAppDispatch();
const calendars = useAppSelector((state) => state.calendars.list);
const handleCalendarToggle = (name: string) => {
setSelectedCalendars((prev: string[]) =>
prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name]
);
};
return (
<div>
<h3>personnalCalendars</h3>
{Object.keys(calendars)
.filter((id) => id.split("/")[0] === userId)
.map((id) => {
return (
<div key={id}>
<label>
<input
type="checkbox"
style={{ backgroundColor: calendars[id].color }}
checked={selectedCalendars.includes(id)}
onChange={() => handleCalendarToggle(id)}
/>
{calendars[id].name}
</label>
</div>
);
})}
<h3>sharedCalendars</h3>
{Object.keys(calendars)
.filter((id) => id.split("/")[0] !== userId)
.map((id) => (
<div key={id}>
<label>
<input
type="checkbox"
style={{ backgroundColor: calendars[id].color }}
checked={selectedCalendars.includes(id)}
onChange={() => handleCalendarToggle(id)}
/>
{calendars[id].name}
</label>
</div>
))}
</div>
);
}
+1 -1
View File
@@ -26,7 +26,7 @@ function CalendarPopover({
const [description, setDescription] = useState("");
const [color, setColor] = useState("");
const [timeZone, setTimeZone] = useState("");
const timezones = Intl.supportedValuesOf?.("timeZone") || [];
const timezones = Intl.supportedValuesOf?.("timeZone") ?? [];
const handleSave = () => {
dispatch(createCalendar({ name, description, color }));
+71 -30
View File
@@ -5,7 +5,7 @@ import { getCalendar, getCalendars } from "./CalendarApi";
import getOpenPaasUserId from "../User/userAPI";
import { parseCalendarEvent } from "../Events/eventUtils";
export const getCalendarsAsync = createAsyncThunk<
export const getCalendarsListAsync = createAsyncThunk<
Record<string, Calendars>, // Return type
string // Arg type (access_token)
>("calendars/getCalendars", async (access_token: string) => {
@@ -21,51 +21,65 @@ export const getCalendarsAsync = createAsyncThunk<
? cal["calendarserver:source"]._links.self.href
.replace("/calendars/", "")
.replace(".json", "")
: cal._links.self.href
.replace("/calendars/", "")
.replace(".json", "")
: cal._links.self.href.replace("/calendars/", "").replace(".json", "");
const color = cal["apple:color"];
const calendarDetails = await getCalendar(id, access_token);
const events = calendarDetails._embedded["dav:item"].map(
(eventdata: any) => {
const datas = eventdata.data[2][0][1];
return parseCalendarEvent(datas, color);
}
);
// const calendarDetails = await getCalendar(id, access_token);
// const events = calendarDetails._embedded["dav:item"].map(
// (eventdata: any) => {
// const datas = eventdata.data[2][0][1];
// return parseCalendarEvent(datas, color);
// }
// );
importedCalendars[id] = { id, name, description, color, events };
importedCalendars[id] = { id, name, description, color, events: [] };
}
return importedCalendars;
});
export const getCalendarDetailAsync = createAsyncThunk<
{ calId: string; events: CalendarEvent[] }, // Return type
{ access_token: string; calId: string } // Arg type
>("calendars/getCalendarDetails", async ({ access_token, calId }) => {
const calendar = await getCalendar(calId, access_token);
const color = calendar["apple:color"];
const events: CalendarEvent[] = calendar._embedded["dav:item"].map(
(eventdata: any) => {
const datas = eventdata.data[2][0][1];
return parseCalendarEvent(datas, color);
}
);
return { calId, events };
});
const CalendarSlice = createSlice({
name: "calendars",
initialState: {} as Record<string, Calendars>,
initialState: { list: {} as Record<string, Calendars>, pending: false },
reducers: {
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
const id = Date.now().toString(36);
state[id] = {} as Calendars;
state[id].name = action.payload.name;
state[id].color = action.payload.color;
state[id].description = action.payload.description;
state[id].events = [];
state.list[id] = {} as Calendars;
state.list[id].name = action.payload.name;
state.list[id].color = action.payload.color;
state.list[id].description = action.payload.description;
state.list[id].events = [];
},
addEvent: (
state,
action: PayloadAction<{ calendarUid: string; event: CalendarEvent }>
) => {
if (!state[action.payload.calendarUid].events) {
state[action.payload.calendarUid].events = [];
if (!state.list[action.payload.calendarUid].events) {
state.list[action.payload.calendarUid].events = [];
}
state[action.payload.calendarUid].events.push(action.payload.event);
state.list[action.payload.calendarUid].events.push(action.payload.event);
},
removeEvent: (
state,
action: PayloadAction<{ calendarUid: string; eventUid: string }>
) => {
state[action.payload.calendarUid as string].events = state[
state.list[action.payload.calendarUid as string].events = state.list[
action.payload.calendarUid
].events.filter((event) => {
if (event.uid !== action.payload.calendarUid) {
@@ -75,14 +89,41 @@ const CalendarSlice = createSlice({
},
},
extraReducers: (builder) => {
builder.addCase(
getCalendarsAsync.fulfilled,
(state, action: PayloadAction<Record<string, Calendars>>) => {
Object.keys(action.payload).forEach((id) => {
state[id] = action.payload[id];
});
}
);
builder
.addCase(
getCalendarsListAsync.fulfilled,
(state, action: PayloadAction<Record<string, Calendars>>) => {
state.pending = false;
Object.keys(action.payload).forEach((id) => {
state.list[id] = action.payload[id];
});
}
)
.addCase(
getCalendarDetailAsync.fulfilled,
(
state,
action: PayloadAction<{ calId: string; events: CalendarEvent[] }>
) => {
state.pending = false;
if (!state.list[action.payload.calId]) {
state.list[action.payload.calId] = {
id: action.payload.calId,
events: [] as CalendarEvent[],
} as Calendars;
}
state.list[action.payload.calId].events = action.payload.events;
state.list[action.payload.calId].events.forEach((event) => {
event.color = state.list[action.payload.calId].color;
});
}
)
.addCase(getCalendarDetailAsync.pending, (state) => {
state.pending = true;
})
.addCase(getCalendarsListAsync.pending, (state) => {
state.pending = true;
});
},
});
+3 -3
View File
@@ -28,7 +28,7 @@ function EventPopover({
const dispatch = useAppDispatch();
const organizer = useAppSelector((state) => state.user.organiserData);
const calendars = useAppSelector((state) => state.calendars);
const calendars = useAppSelector((state) => state.calendars.list);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [location, setLocation] = useState("");
@@ -46,8 +46,8 @@ function EventPopover({
const handleSave = () => {
const newEvent: CalendarEvent = {
title,
start: new Date(start || ""),
end: new Date(end || ""),
start: new Date(start ?? ""),
end: new Date(end ?? ""),
uid: Date.now().toString(36),
description,
location,
+7 -7
View File
@@ -40,22 +40,22 @@ export function parseCalendarEvent(
break;
case "organizer":
event.organizer = {
cn: params?.cn || "",
cn: params?.cn ?? "",
cal_address: value.replace(/^mailto:/, ""),
};
break;
case "attendee":
(event.attendee as userAttendee[]).push({
cn: params?.cn || "",
cn: params?.cn ?? "",
cal_address: value.replace(/^mailto:/, ""),
partstat: params?.partstat || "",
rsvp: params?.rsvp || "",
role: params?.role || "",
cutype: params?.cutype || "",
partstat: params?.partstat ?? "",
rsvp: params?.rsvp ?? "",
role: params?.role ?? "",
cutype: params?.cutype ?? "",
});
break;
case "dtstamp":
event.stamp = new Date(value);
event.stamp = value;
break;
case "sequence":
event.sequence = Number(value);
+4 -1
View File
@@ -7,6 +7,7 @@ import { push } from "redux-first-history";
export function HandleLogin() {
const userData = useAppSelector((state) => state.user.userData);
const calendars = useAppSelector((state) => state.calendars);
const dispatch = useAppDispatch();
useEffect(() => {
const initiateLogin = async () => {
@@ -31,7 +32,9 @@ export function HandleLogin() {
if (!userData) {
return <Error />;
}
dispatch(push("/calendar"));
if (!calendars.pending) {
dispatch(push("/calendar"));
}
return <Loading />;
}
+11 -4
View File
@@ -1,14 +1,19 @@
import { useEffect, useRef } from "react";
import { Callback } from "./oidcAuth";
import { useAppDispatch } from "../../app/hooks";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { push } from "redux-first-history";
import { setTokens, setUserData } from "./userSlice";
import { getOpenPaasUserIdAsync, setTokens, setUserData } from "./userSlice";
import { Loading } from "../../components/Loading/Loading";
import { getCalendarsAsync } from "../Calendars/CalendarSlice";
import {
getCalendarDetailAsync,
getCalendarsListAsync,
} from "../Calendars/CalendarSlice";
export function CallbackResume() {
const dispatch = useAppDispatch();
const hasRun = useRef(false);
const calendars = useAppSelector((state) => state.calendars);
const userId = useAppSelector((state) => state.user.userData?.openpaasId);
const saved = sessionStorage.getItem("redirectState")
? JSON.parse(sessionStorage.getItem("redirectState")!)
: null;
@@ -22,7 +27,9 @@ export function CallbackResume() {
const data = await Callback(saved?.code_verifier, saved?.state);
dispatch(setUserData(data?.userinfo));
dispatch(setTokens(data?.tokenSet));
dispatch(getCalendarsAsync(data?.tokenSet.access_token || ""));
dispatch(getOpenPaasUserIdAsync(data?.tokenSet.access_token ?? ""));
dispatch(getCalendarsListAsync(data?.tokenSet.access_token ?? ""));
sessionStorage.removeItem("redirectState");
// Redirect to main page after successful callback
dispatch(push("/"));
+7 -7
View File
@@ -1,13 +1,13 @@
import * as client from "openid-client";
export const clientConfig = {
url: process.env.PUBLIC_SSO_BASE_URL || "",
client_id: process.env.PUBLIC_SSO_CLIENT_ID || "",
scope: process.env.PUBLIC_SSO_SCOPE || "",
redirect_uri: process.env.PUBLIC_SSO_REDIRECT_URI || "",
response_type: process.env.PUBLIC_SSO_RESPONSE_TYPE || "",
code_challenge_method: process.env.PUBLIC_SSO_CODE_CHALLENGE_METHOD || "",
post_logout_redirect_uri: process.env.PUBLIC_SSO_POST_LOGOUT_REDIRECT || "",
url: process.env.PUBLIC_SSO_BASE_URL ?? "",
client_id: process.env.PUBLIC_SSO_CLIENT_ID ?? "",
scope: process.env.PUBLIC_SSO_SCOPE ?? "",
redirect_uri: process.env.PUBLIC_SSO_REDIRECT_URI ?? "",
response_type: process.env.PUBLIC_SSO_RESPONSE_TYPE ?? "",
code_challenge_method: process.env.PUBLIC_SSO_CODE_CHALLENGE_METHOD ?? "",
post_logout_redirect_uri: process.env.PUBLIC_SSO_POST_LOGOUT_REDIRECT ?? "",
};
export async function getClientConfig() {
+1
View File
@@ -2,6 +2,7 @@ export interface userData {
email: string;
sid: string;
sub: string;
openpaasId?: string;
}
export interface userOrganiser {
+16 -1
View File
@@ -1,5 +1,15 @@
import { createSlice } from "@reduxjs/toolkit";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { userData, userOrganiser } from "./userDataTypes";
import getOpenPaasUserId from "./userAPI";
export const getOpenPaasUserIdAsync = createAsyncThunk<
string, // Return type
string // Arg type
>("user/getOpenPaasUserId", async (access_token) => {
const user = await getOpenPaasUserId(access_token);
return user.id;
});
export const userSlice = createSlice({
name: "user",
@@ -21,6 +31,11 @@ export const userSlice = createSlice({
state.tokens = action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(getOpenPaasUserIdAsync.fulfilled, (state, action) => {
state.userData.openpaasId = action.payload;
});
},
});
// Action creators are generated for each case reducer function