[#25] changed loading order + added condition to keep loading screen while loading calendar list
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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("/"));
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface userData {
|
||||
email: string;
|
||||
sid: string;
|
||||
sub: string;
|
||||
openpaasId?: string;
|
||||
}
|
||||
|
||||
export interface userOrganiser {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user