refactor: stabilize calendar loading effects and menubar redirect
- Memoize calendar range strings and stabilize effect deps - Guard updateDarkColor to avoid dispatch loops - Add resilient cache-clear handling with refs - Memoize calendar/temp ids to prevent rerenders - Update event handler hooks with missing deps - Add guards and dependency fixes across Calendar effects
This commit is contained in:
committed by
Benoit TELLIER
parent
990e290066
commit
ffe204d60b
@@ -581,4 +581,188 @@ describe("calendar Availability search", () => {
|
|||||||
expect(startDate.getTime()).toBeLessThanOrEqual(viewStart.getTime());
|
expect(startDate.getTime()).toBeLessThanOrEqual(viewStart.getTime());
|
||||||
expect(endDate.getTime()).toBeGreaterThanOrEqual(viewEnd.getTime());
|
expect(endDate.getTime()).toBeGreaterThanOrEqual(viewEnd.getTime());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Batch loading and prefetching", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads active calendars in batches of 5", async () => {
|
||||||
|
const manyCalendars = Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
[`user1/cal${i + 1}`]: {
|
||||||
|
name: `Calendar ${i + 1}`,
|
||||||
|
id: `user1/cal${i + 1}`,
|
||||||
|
color: { light: "#FF0000", dark: "#000" },
|
||||||
|
events: {},
|
||||||
|
},
|
||||||
|
})).reduce((acc, cal) => ({ ...acc, ...cal }), {});
|
||||||
|
|
||||||
|
const stateWithManyCalendars = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
...preloadedState.calendars,
|
||||||
|
list: manyCalendars,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(calendarThunks, "getCalendarDetailAsync")
|
||||||
|
.mockImplementation(() => ({
|
||||||
|
type: "getCalendarDetailAsync",
|
||||||
|
unwrap: () => Promise.resolve({}),
|
||||||
|
})) as any;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={{ current: null }} />,
|
||||||
|
stateWithManyCalendars
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const callCount = spy.mock.calls.length;
|
||||||
|
expect(callCount).toBeGreaterThan(0);
|
||||||
|
expect(callCount).toBeLessThanOrEqual(24);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefetches hidden calendars only after active load completes", async () => {
|
||||||
|
const calendarsWithSelected = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
...preloadedState.calendars,
|
||||||
|
list: {
|
||||||
|
"user1/cal1": {
|
||||||
|
name: "Selected Calendar",
|
||||||
|
id: "user1/cal1",
|
||||||
|
color: { light: "#FF0000", dark: "#000" },
|
||||||
|
events: {},
|
||||||
|
},
|
||||||
|
"user1/cal2": {
|
||||||
|
name: "Hidden Calendar 1",
|
||||||
|
id: "user1/cal2",
|
||||||
|
color: { light: "#00FF00", dark: "#000" },
|
||||||
|
events: {},
|
||||||
|
},
|
||||||
|
"user1/cal3": {
|
||||||
|
name: "Hidden Calendar 2",
|
||||||
|
id: "user1/cal3",
|
||||||
|
color: { light: "#0000FF", dark: "#000" },
|
||||||
|
events: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(calendarThunks, "getCalendarDetailAsync")
|
||||||
|
.mockImplementation(() => ({
|
||||||
|
type: "getCalendarDetailAsync",
|
||||||
|
unwrap: () => Promise.resolve({}),
|
||||||
|
})) as any;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={{ current: null }} />,
|
||||||
|
calendarsWithSelected
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCalls = spy.mock.calls.filter(
|
||||||
|
(call) => call[0].calId === "user1/cal1"
|
||||||
|
);
|
||||||
|
const hiddenCalls = spy.mock.calls.filter(
|
||||||
|
(call) =>
|
||||||
|
call[0].calId === "user1/cal2" || call[0].calId === "user1/cal3"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(selectedCalls.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not make duplicate API calls for same calendar and range", async () => {
|
||||||
|
const spy = jest
|
||||||
|
.spyOn(calendarThunks, "getCalendarDetailAsync")
|
||||||
|
.mockImplementation(() => ({
|
||||||
|
type: "getCalendarDetailAsync",
|
||||||
|
unwrap: () => Promise.resolve({}),
|
||||||
|
})) as any;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={{ current: null }} />,
|
||||||
|
preloadedState
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const callsForCal1 = spy.mock.calls.filter(
|
||||||
|
(call) => call[0].calId === "user1/cal1"
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniqueRanges = new Set(
|
||||||
|
callsForCal1.map(
|
||||||
|
(call) => `${call[0].match.start}_${call[0].match.end}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(uniqueRanges.size).toBeLessThanOrEqual(callsForCal1.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles undefined calendars gracefully", async () => {
|
||||||
|
const stateWithUndefinedCalendars = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
list: undefined as any,
|
||||||
|
templist: undefined as any,
|
||||||
|
pending: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={{ current: null }} />,
|
||||||
|
stateWithUndefinedCalendars
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("calendar.personal")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles undefined tempcalendars gracefully", async () => {
|
||||||
|
const stateWithUndefinedTemp = {
|
||||||
|
...preloadedState,
|
||||||
|
calendars: {
|
||||||
|
...preloadedState.calendars,
|
||||||
|
templist: undefined as any,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderWithProviders(
|
||||||
|
<CalendarApp calendarRef={{ current: null }} />,
|
||||||
|
stateWithUndefinedTemp
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("calendar.personal")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,6 +140,203 @@ describe("CalendarSlice", () => {
|
|||||||
expect(state.list).toEqual({});
|
expect(state.list).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("getCalendarsListAsync loads user details in parallel for multiple owners", async () => {
|
||||||
|
const mockCalendars = [
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||||
|
"dav:name": "Calendar 1",
|
||||||
|
"apple:color": "#FF0000",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u2/cal2.json" } },
|
||||||
|
"dav:name": "Calendar 2",
|
||||||
|
"apple:color": "#00FF00",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u3/cal3.json" } },
|
||||||
|
"dav:name": "Calendar 3",
|
||||||
|
"apple:color": "#0000FF",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||||
|
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||||
|
_embedded: { "dav:calendar": mockCalendars },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||||
|
getUserDetailsMock
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
firstname: "Alice",
|
||||||
|
lastname: "Smith",
|
||||||
|
emails: ["alice@example.com"],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
firstname: "Bob",
|
||||||
|
lastname: "Jones",
|
||||||
|
emails: ["bob@example.com"],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
firstname: "Charlie",
|
||||||
|
lastname: "Brown",
|
||||||
|
emails: ["charlie@example.com"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = storeFactory();
|
||||||
|
await store.dispatch(getCalendarsListAsync() as any);
|
||||||
|
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledTimes(3);
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledWith("u1");
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledWith("u2");
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledWith("u3");
|
||||||
|
|
||||||
|
const state = store.getState().calendars;
|
||||||
|
expect(state.list["u1/cal1"].owner).toContain("Alice");
|
||||||
|
expect(state.list["u2/cal2"].owner).toContain("Bob");
|
||||||
|
expect(state.list["u3/cal3"].owner).toContain("Charlie");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getCalendarsListAsync deduplicates getUserDetails calls for same ownerId", async () => {
|
||||||
|
const mockCalendars = [
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||||
|
"dav:name": "Calendar 1",
|
||||||
|
"apple:color": "#FF0000",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u1/cal2.json" } },
|
||||||
|
"dav:name": "Calendar 2",
|
||||||
|
"apple:color": "#00FF00",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u1/cal3.json" } },
|
||||||
|
"dav:name": "Calendar 3",
|
||||||
|
"apple:color": "#0000FF",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||||
|
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||||
|
_embedded: { "dav:calendar": mockCalendars },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||||
|
getUserDetailsMock.mockResolvedValue({
|
||||||
|
firstname: "Alice",
|
||||||
|
lastname: "Smith",
|
||||||
|
emails: ["alice@example.com"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = storeFactory();
|
||||||
|
await store.dispatch(getCalendarsListAsync() as any);
|
||||||
|
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledWith("u1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getCalendarsListAsync processes owners in batches of 20", async () => {
|
||||||
|
const mockCalendars = Array.from({ length: 45 }, (_, i) => ({
|
||||||
|
_links: { self: { href: `/calendars/u${i + 1}/cal${i + 1}.json` } },
|
||||||
|
"dav:name": `Calendar ${i + 1}`,
|
||||||
|
"apple:color": "#FF0000",
|
||||||
|
acl: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||||
|
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||||
|
_embedded: { "dav:calendar": mockCalendars },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||||
|
getUserDetailsMock.mockImplementation((ownerId: string) =>
|
||||||
|
Promise.resolve({
|
||||||
|
firstname: "User",
|
||||||
|
lastname: ownerId,
|
||||||
|
emails: [`${ownerId}@example.com`],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = storeFactory();
|
||||||
|
await store.dispatch(getCalendarsListAsync() as any);
|
||||||
|
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledTimes(45);
|
||||||
|
const state = store.getState().calendars;
|
||||||
|
expect(Object.keys(state.list)).toHaveLength(45);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getCalendarsListAsync returns early if calendars already exist in store", async () => {
|
||||||
|
const existingCalendars = {
|
||||||
|
"u1/cal1": {
|
||||||
|
id: "u1/cal1",
|
||||||
|
name: "Existing Calendar",
|
||||||
|
events: {},
|
||||||
|
} as Calendars,
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = storeFactory();
|
||||||
|
store.dispatch({
|
||||||
|
type: "calendars/getCalendars/fulfilled",
|
||||||
|
payload: { importedCalendars: existingCalendars, errors: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||||
|
const getCalendarsMock = calAPI.getCalendars as jest.Mock;
|
||||||
|
|
||||||
|
await store.dispatch(getCalendarsListAsync() as any);
|
||||||
|
|
||||||
|
expect(getCalendarsMock).not.toHaveBeenCalled();
|
||||||
|
expect(getUserDetailsMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const state = store.getState().calendars;
|
||||||
|
expect(state.list).toEqual(existingCalendars);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getCalendarsListAsync handles errors in getUserDetails gracefully", async () => {
|
||||||
|
const mockCalendars = [
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u1/cal1.json" } },
|
||||||
|
"dav:name": "Calendar 1",
|
||||||
|
"apple:color": "#FF0000",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_links: { self: { href: "/calendars/u2/cal2.json" } },
|
||||||
|
"dav:name": "Calendar 2",
|
||||||
|
"apple:color": "#00FF00",
|
||||||
|
acl: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
(userAPI.getOpenPaasUser as jest.Mock).mockResolvedValue({ id: "u1" });
|
||||||
|
(calAPI.getCalendars as jest.Mock).mockResolvedValue({
|
||||||
|
_embedded: { "dav:calendar": mockCalendars },
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserDetailsMock = userAPI.getUserDetails as jest.Mock;
|
||||||
|
getUserDetailsMock
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
firstname: "Alice",
|
||||||
|
lastname: "Smith",
|
||||||
|
emails: ["alice@example.com"],
|
||||||
|
})
|
||||||
|
.mockRejectedValueOnce(new Error("Failed to fetch user"));
|
||||||
|
|
||||||
|
const store = storeFactory();
|
||||||
|
const result = await store.dispatch(getCalendarsListAsync() as any);
|
||||||
|
|
||||||
|
expect(getUserDetailsMock).toHaveBeenCalledTimes(2);
|
||||||
|
const state = store.getState().calendars;
|
||||||
|
expect(state.list["u1/cal1"].owner).toContain("Alice");
|
||||||
|
expect(state.list["u2/cal2"].owner).toContain("Unknown User");
|
||||||
|
expect(result.payload.errors).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("patchCalendarAsync.fulfilled updates calendar fields", () => {
|
it("patchCalendarAsync.fulfilled updates calendar fields", () => {
|
||||||
const calId = "c1";
|
const calId = "c1";
|
||||||
const prev = {
|
const prev = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import interactionPlugin from "@fullcalendar/interaction";
|
|||||||
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
||||||
import "./Calendar.styl";
|
import "./Calendar.styl";
|
||||||
import "./CustomCalendar.styl";
|
import "./CustomCalendar.styl";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import EventPopover from "../../features/Events/EventModal";
|
import EventPopover from "../../features/Events/EventModal";
|
||||||
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
@@ -44,7 +44,7 @@ import { updateDarkColor } from "./utils/calendarColorsUtils";
|
|||||||
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||||
|
|
||||||
interface CalendarAppProps {
|
interface CalendarAppProps {
|
||||||
calendarRef: React.RefObject<CalendarApi | null>;
|
calendarRef: MutableRefObject<CalendarApi | null>;
|
||||||
onDateChange?: (date: Date) => void;
|
onDateChange?: (date: Date) => void;
|
||||||
onViewChange?: (view: string) => void;
|
onViewChange?: (view: string) => void;
|
||||||
}
|
}
|
||||||
@@ -65,13 +65,31 @@ export default function CalendarApp({
|
|||||||
if (!tokens || !userId) {
|
if (!tokens || !userId) {
|
||||||
dispatch(push("/"));
|
dispatch(push("/"));
|
||||||
}
|
}
|
||||||
}, [tokens, userId]);
|
}, [dispatch, tokens, userId]);
|
||||||
|
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
const tempcalendars =
|
const tempcalendars = useAppSelector((state) => state.calendars.templist);
|
||||||
useAppSelector((state) => state.calendars.templist) ?? {};
|
|
||||||
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
|
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const calendarLightSignature = useMemo(() => {
|
||||||
|
return Object.values(calendars || {})
|
||||||
|
.map((cal) => `${cal.id}:${cal.color?.light ?? ""}`)
|
||||||
|
.sort()
|
||||||
|
.join("|");
|
||||||
|
}, [calendars]);
|
||||||
|
|
||||||
|
const calendarIdsString = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.keys(calendars || {})
|
||||||
|
.sort()
|
||||||
|
.join(","),
|
||||||
|
[calendars]
|
||||||
|
);
|
||||||
|
const calendarIds = useMemo(
|
||||||
|
() => (calendarIdsString ? calendarIdsString.split(",") : []),
|
||||||
|
[calendarIdsString]
|
||||||
|
);
|
||||||
|
|
||||||
const dottedEvents: CalendarEvent[] = selectedCalendars.flatMap((calId) => {
|
const dottedEvents: CalendarEvent[] = selectedCalendars.flatMap((calId) => {
|
||||||
const calendar = calendars[calId];
|
const calendar = calendars[calId];
|
||||||
if (!calendar?.events) return [];
|
if (!calendar?.events) return [];
|
||||||
@@ -100,65 +118,99 @@ export default function CalendarApp({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialLoadRef.current && Object.keys(calendars || {}).length > 0 && userId) {
|
if (initialLoadRef.current && calendarIds.length > 0 && userId) {
|
||||||
const cached = localStorage.getItem("selectedCalendars");
|
const cached = localStorage.getItem("selectedCalendars");
|
||||||
if (cached && cached.length > 0) {
|
if (cached && cached.length > 0) {
|
||||||
const parsed = JSON.parse(cached) as string[];
|
const parsed = JSON.parse(cached) as string[];
|
||||||
const valid = parsed.filter((id) => calendars[id]);
|
const valid = parsed.filter((id) => calendars[id]);
|
||||||
setSelectedCalendars(valid);
|
setSelectedCalendars(valid);
|
||||||
} else {
|
} else {
|
||||||
const personalCalendarIds = Object.keys(calendars).filter(
|
const personalCalendarIds = calendarIds.filter(
|
||||||
(id) => id.split("/")[0] === userId
|
(id) => id.split("/")[0] === userId
|
||||||
);
|
);
|
||||||
setSelectedCalendars(personalCalendarIds);
|
setSelectedCalendars(personalCalendarIds);
|
||||||
}
|
}
|
||||||
initialLoadRef.current = false;
|
initialLoadRef.current = false;
|
||||||
}
|
}
|
||||||
}, [calendars, userId]);
|
}, [calendarIds, calendars, userId]);
|
||||||
|
|
||||||
// Save selected cals to cache
|
// Save selected cals to cache
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(calendars || {}).length > 0) {
|
if (calendarIds.length > 0) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"selectedCalendars",
|
"selectedCalendars",
|
||||||
JSON.stringify(selectedCalendars)
|
JSON.stringify(selectedCalendars)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [selectedCalendars]);
|
}, [selectedCalendars, calendarIds.length]);
|
||||||
|
|
||||||
|
const prevCalendarLightSignature = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!calendarLightSignature) {
|
||||||
|
prevCalendarLightSignature.current = calendarLightSignature;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevCalendarLightSignature.current === calendarLightSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevCalendarLightSignature.current = calendarLightSignature;
|
||||||
updateDarkColor(calendars || {}, theme, dispatch);
|
updateDarkColor(calendars || {}, theme, dispatch);
|
||||||
}, [
|
}, [calendarLightSignature, calendars, theme, dispatch]);
|
||||||
theme,
|
|
||||||
Object.values(calendars || {})
|
|
||||||
.map((c) => c.color?.dark)
|
|
||||||
.join(","),
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validCalendarIds = new Set(Object.keys(calendars || {}));
|
if (calendarIds.length === 0) return;
|
||||||
setSelectedCalendars((prev) =>
|
const validCalendarIds = new Set(calendarIds);
|
||||||
prev.filter((calId) => validCalendarIds.has(calId))
|
setSelectedCalendars((prev) => {
|
||||||
);
|
const filtered = prev.filter((calId) => validCalendarIds.has(calId));
|
||||||
}, [calendars]);
|
if (filtered.length === prev.length) {
|
||||||
|
const unchanged = filtered.every((id, index) => id === prev[index]);
|
||||||
|
if (unchanged) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
});
|
||||||
|
}, [calendarIds]);
|
||||||
|
|
||||||
const calendarRange = useMemo(
|
const calendarRange = useMemo(
|
||||||
() => getCalendarRange(selectedDate),
|
() => getCalendarRange(selectedDate),
|
||||||
[selectedDate]
|
[selectedDate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const calendarRangeStart = calendarRange.start.getTime();
|
||||||
|
const calendarRangeEnd = calendarRange.end.getTime();
|
||||||
|
|
||||||
|
const rangeStart = useMemo(
|
||||||
|
() => formatDateToYYYYMMDDTHHMMSS(new Date(calendarRangeStart)),
|
||||||
|
[calendarRangeStart]
|
||||||
|
);
|
||||||
|
|
||||||
|
const rangeEnd = useMemo(
|
||||||
|
() => formatDateToYYYYMMDDTHHMMSS(new Date(calendarRangeEnd)),
|
||||||
|
[calendarRangeEnd]
|
||||||
|
);
|
||||||
|
|
||||||
// Create a stable string key for the range
|
// Create a stable string key for the range
|
||||||
const rangeKey = `${formatDateToYYYYMMDDTHHMMSS(
|
const rangeKey = useMemo(
|
||||||
calendarRange.start
|
() => `${rangeStart}_${rangeEnd}`,
|
||||||
)}_${formatDateToYYYYMMDDTHHMMSS(calendarRange.end)}`;
|
[rangeStart, rangeEnd]
|
||||||
|
);
|
||||||
|
|
||||||
let filteredEvents: CalendarEvent[] = extractEvents(
|
let filteredEvents: CalendarEvent[] = extractEvents(
|
||||||
selectedCalendars,
|
selectedCalendars,
|
||||||
calendars || {}
|
calendars || {}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tempCalendarIds = useMemo(
|
||||||
|
() => Object.keys(tempcalendars || {}).sort(),
|
||||||
|
[tempcalendars]
|
||||||
|
);
|
||||||
|
|
||||||
let filteredTempEvents: CalendarEvent[] = extractEvents(
|
let filteredTempEvents: CalendarEvent[] = extractEvents(
|
||||||
Object.keys(tempcalendars || {}),
|
tempCalendarIds,
|
||||||
tempcalendars || {}
|
tempcalendars || {}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -169,55 +221,73 @@ export default function CalendarApp({
|
|||||||
|
|
||||||
const prefetchedCalendarsRef = useRef<Record<string, string>>({});
|
const prefetchedCalendarsRef = useRef<Record<string, string>>({});
|
||||||
const tempFetchedRangesRef = useRef<Record<string, string>>({});
|
const tempFetchedRangesRef = useRef<Record<string, string>>({});
|
||||||
|
const activeLoadCompletedRef = useRef<boolean>(false);
|
||||||
|
const [activeLoadCompleted, setActiveLoadCompleted] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rangeKey || sortedSelectedCalendars.length === 0) return;
|
activeLoadCompletedRef.current = false;
|
||||||
|
setActiveLoadCompleted(false);
|
||||||
|
}, [rangeKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rangeKey || sortedSelectedCalendars.length === 0) {
|
||||||
|
activeLoadCompletedRef.current = true;
|
||||||
|
setActiveLoadCompleted(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const ACTIVE_BATCH_SIZE = 5;
|
const ACTIVE_BATCH_SIZE = 5;
|
||||||
|
|
||||||
const loadCalendars = async () => {
|
const loadCalendars = async () => {
|
||||||
const pendingIds = sortedSelectedCalendars.filter(
|
try {
|
||||||
(id) => fetchedRangesRef.current[id] !== rangeKey
|
const pendingIds = sortedSelectedCalendars.filter(
|
||||||
);
|
(id) => fetchedRangesRef.current[id] !== rangeKey
|
||||||
|
);
|
||||||
|
|
||||||
if (pendingIds.length === 0) {
|
if (pendingIds.length === 0) {
|
||||||
return;
|
activeLoadCompletedRef.current = true;
|
||||||
}
|
setActiveLoadCompleted(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rangeStart = formatDateToYYYYMMDDTHHMMSS(calendarRange.start);
|
for (
|
||||||
const rangeEnd = formatDateToYYYYMMDDTHHMMSS(calendarRange.end);
|
let i = 0;
|
||||||
|
i < pendingIds.length && !cancelled;
|
||||||
|
i += ACTIVE_BATCH_SIZE
|
||||||
|
) {
|
||||||
|
const chunk = pendingIds.slice(i, i + ACTIVE_BATCH_SIZE);
|
||||||
|
|
||||||
for (
|
chunk.forEach((id) => {
|
||||||
let i = 0;
|
fetchedRangesRef.current[id] = rangeKey;
|
||||||
i < pendingIds.length && !cancelled;
|
prefetchedCalendarsRef.current[id] = "active";
|
||||||
i += ACTIVE_BATCH_SIZE
|
});
|
||||||
) {
|
|
||||||
const chunk = pendingIds.slice(i, i + ACTIVE_BATCH_SIZE);
|
|
||||||
|
|
||||||
chunk.forEach((id) => {
|
const requests = chunk.map(async (id) => {
|
||||||
fetchedRangesRef.current[id] = rangeKey;
|
try {
|
||||||
prefetchedCalendarsRef.current[id] = "active";
|
await dispatch(
|
||||||
});
|
getCalendarDetailAsync({
|
||||||
|
calId: id,
|
||||||
|
match: {
|
||||||
|
start: rangeStart,
|
||||||
|
end: rangeEnd,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).unwrap();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load calendar ${id}:`, error);
|
||||||
|
fetchedRangesRef.current[id] = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const requests = chunk.map(async (id) => {
|
await Promise.all(requests);
|
||||||
try {
|
}
|
||||||
await dispatch(
|
} finally {
|
||||||
getCalendarDetailAsync({
|
if (!cancelled) {
|
||||||
calId: id,
|
activeLoadCompletedRef.current = true;
|
||||||
match: {
|
setActiveLoadCompleted(true);
|
||||||
start: rangeStart,
|
}
|
||||||
end: rangeEnd,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
).unwrap();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load calendar ${id}:`, error);
|
|
||||||
fetchedRangesRef.current[id] = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(requests);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,22 +296,19 @@ export default function CalendarApp({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [
|
}, [dispatch, rangeKey, sortedSelectedCalendars, rangeStart, rangeEnd]);
|
||||||
dispatch,
|
|
||||||
rangeKey,
|
|
||||||
sortedSelectedCalendars,
|
|
||||||
calendarRange,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rangeKey) return;
|
if (!rangeKey || !activeLoadCompleted) return;
|
||||||
|
|
||||||
const rangeStart = formatDateToYYYYMMDDTHHMMSS(calendarRange.start);
|
const hiddenCalendars = calendarIds
|
||||||
const rangeEnd = formatDateToYYYYMMDDTHHMMSS(calendarRange.end);
|
|
||||||
|
|
||||||
const hiddenCalendars = Object.keys(calendars || {})
|
|
||||||
.filter((id) => !selectedCalendars.includes(id))
|
.filter((id) => !selectedCalendars.includes(id))
|
||||||
.filter((id) => prefetchedCalendarsRef.current[id] !== rangeKey);
|
.filter((id) => {
|
||||||
|
const prefetched = prefetchedCalendarsRef.current[id];
|
||||||
|
return prefetched !== rangeKey && prefetched !== "active";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hiddenCalendars.length === 0) return;
|
||||||
|
|
||||||
hiddenCalendars.forEach((id) => {
|
hiddenCalendars.forEach((id) => {
|
||||||
prefetchedCalendarsRef.current[id] = rangeKey;
|
prefetchedCalendarsRef.current[id] = rangeKey;
|
||||||
@@ -261,43 +328,56 @@ export default function CalendarApp({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
calendars,
|
calendarIdsString,
|
||||||
selectedCalendars,
|
selectedCalendars,
|
||||||
rangeKey,
|
rangeKey,
|
||||||
calendarRange,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd,
|
||||||
|
activeLoadCompleted,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const calendarsWithClearedCache = useMemo(() => {
|
||||||
|
return selectedCalendars
|
||||||
|
.map((id) => {
|
||||||
|
const cleared = calendars[id]?.lastCacheCleared;
|
||||||
|
return cleared ? { id, cleared } : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as { id: string; cleared: number }[];
|
||||||
|
}, [selectedCalendars, calendars]);
|
||||||
|
|
||||||
|
const processedCacheClearRef = useRef<Record<string, number>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedCalendars.forEach((calId) => {
|
calendarsWithClearedCache.forEach(({ id, cleared }) => {
|
||||||
const calendar = calendars[calId];
|
if (processedCacheClearRef.current[id] === cleared) {
|
||||||
if (calendar?.lastCacheCleared) {
|
return;
|
||||||
delete fetchedRangesRef.current[calId];
|
|
||||||
prefetchedCalendarsRef.current[calId] = "";
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
getCalendarDetailAsync({
|
|
||||||
calId,
|
|
||||||
match: {
|
|
||||||
start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start),
|
|
||||||
end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
fetchedRangesRef.current[calId] = rangeKey;
|
|
||||||
prefetchedCalendarsRef.current[calId] = rangeKey;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
selectedCalendars.map((id) => calendars[id]?.lastCacheCleared).join(","),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tempCalendarIds = useMemo(
|
processedCacheClearRef.current[id] = cleared;
|
||||||
() => Object.keys(tempcalendars || {}).sort(),
|
delete fetchedRangesRef.current[id];
|
||||||
[tempcalendars]
|
prefetchedCalendarsRef.current[id] = "";
|
||||||
);
|
|
||||||
|
dispatch(
|
||||||
|
getCalendarDetailAsync({
|
||||||
|
calId: id,
|
||||||
|
match: {
|
||||||
|
start: rangeStart,
|
||||||
|
end: rangeEnd,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.then(() => {
|
||||||
|
fetchedRangesRef.current[id] = rangeKey;
|
||||||
|
prefetchedCalendarsRef.current[id] = rangeKey;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
fetchedRangesRef.current[id] = "";
|
||||||
|
prefetchedCalendarsRef.current[id] = "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [calendarsWithClearedCache, dispatch, rangeKey, rangeStart, rangeEnd]);
|
||||||
|
|
||||||
const tempCalendarControllersRef = useRef<Map<string, AbortController>>(
|
const tempCalendarControllersRef = useRef<Map<string, AbortController>>(
|
||||||
new Map()
|
new Map()
|
||||||
@@ -317,9 +397,6 @@ export default function CalendarApp({
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const TEMP_BATCH_SIZE = 5;
|
const TEMP_BATCH_SIZE = 5;
|
||||||
const rangeStart = formatDateToYYYYMMDDTHHMMSS(calendarRange.start);
|
|
||||||
const rangeEnd = formatDateToYYYYMMDDTHHMMSS(calendarRange.end);
|
|
||||||
|
|
||||||
const loadTempCalendars = async () => {
|
const loadTempCalendars = async () => {
|
||||||
const pendingIds = tempCalendarIds.filter(
|
const pendingIds = tempCalendarIds.filter(
|
||||||
(id) => tempFetchedRangesRef.current[id] !== rangeKey
|
(id) => tempFetchedRangesRef.current[id] !== rangeKey
|
||||||
@@ -366,7 +443,7 @@ export default function CalendarApp({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [dispatch, rangeKey, tempCalendarIds, calendarRange]);
|
}, [dispatch, rangeKey, tempCalendarIds, rangeStart, rangeEnd]);
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -106,14 +106,14 @@ export default function CalendarSelection({
|
|||||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||||
const calendars = useAppSelector((state) => state.calendars.list);
|
const calendars = useAppSelector((state) => state.calendars.list);
|
||||||
|
|
||||||
const personalCalendars = Object.keys(calendars).filter(
|
const personalCalendars = Object.keys(calendars || {}).filter(
|
||||||
(id) => id.split("/")[0] === userId
|
(id) => id.split("/")[0] === userId
|
||||||
);
|
);
|
||||||
const delegatedCalendars = Object.keys(calendars).filter(
|
const delegatedCalendars = Object.keys(calendars || {}).filter(
|
||||||
(id) => id.split("/")[0] !== userId && calendars[id].delegated
|
(id) => id.split("/")[0] !== userId && calendars[id]?.delegated
|
||||||
);
|
);
|
||||||
const sharedCalendars = Object.keys(calendars).filter(
|
const sharedCalendars = Object.keys(calendars || {}).filter(
|
||||||
(id) => id.split("/")[0] !== userId && !calendars[id].delegated
|
(id) => id.split("/")[0] !== userId && !calendars?.[id]?.delegated
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCalendarToggle = (name: string) => {
|
const handleCalendarToggle = (name: string) => {
|
||||||
@@ -178,7 +178,7 @@ export default function CalendarSelection({
|
|||||||
</div>
|
</div>
|
||||||
<CalendarPopover
|
<CalendarPopover
|
||||||
open={Boolean(anchorElCal)}
|
open={Boolean(anchorElCal)}
|
||||||
calendar={calendars[selectedCalId] ?? undefined}
|
calendar={calendars?.[selectedCalId] ?? undefined}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setSelectedCalId("");
|
setSelectedCalId("");
|
||||||
setAnchorElCal(null);
|
setAnchorElCal(null);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function MiniCalendar({
|
|||||||
onMonthChange={(month) => {
|
onMonthChange={(month) => {
|
||||||
const calendarRange = getCalendarRange(month.toDate());
|
const calendarRange = getCalendarRange(month.toDate());
|
||||||
setVisibleDate(month.toDate());
|
setVisibleDate(month.toDate());
|
||||||
Object.values(calendars.list).forEach((cal) =>
|
Object.values(calendars.list || {}).forEach((cal) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
getCalendarDetailAsync({
|
getCalendarDetailAsync({
|
||||||
calId: cal.id,
|
calId: cal.id,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
|||||||
props.setEventDisplayedCalId,
|
props.setEventDisplayedCalId,
|
||||||
props.setEventDisplayedTemp,
|
props.setEventDisplayedTemp,
|
||||||
props.calendars,
|
props.calendars,
|
||||||
|
props.dispatch,
|
||||||
]),
|
]),
|
||||||
handleEventAllow: useCallback(eventHandlers.handleEventAllow, []),
|
handleEventAllow: useCallback(eventHandlers.handleEventAllow, []),
|
||||||
handleEventDrop: useCallback(eventHandlers.handleEventDrop, [
|
handleEventDrop: useCallback(eventHandlers.handleEventDrop, [
|
||||||
@@ -42,10 +43,14 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => {
|
|||||||
props.setSelectedEvent,
|
props.setSelectedEvent,
|
||||||
props.setOpenEditModePopup,
|
props.setOpenEditModePopup,
|
||||||
props.setAfterChoiceFunc,
|
props.setAfterChoiceFunc,
|
||||||
|
props.tempcalendars,
|
||||||
|
props.calendarRange,
|
||||||
]),
|
]),
|
||||||
handleEventResize: useCallback(eventHandlers.handleEventResize, [
|
handleEventResize: useCallback(eventHandlers.handleEventResize, [
|
||||||
props.calendars,
|
props.calendars,
|
||||||
props.dispatch,
|
props.dispatch,
|
||||||
|
props.tempcalendars,
|
||||||
|
props.calendarRange,
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export function updateDarkColor(
|
|||||||
? isDefault.dark
|
? isDefault.dark
|
||||||
: getAccessiblePair(baseColor, theme);
|
: getAccessiblePair(baseColor, theme);
|
||||||
|
|
||||||
|
if (cal.color?.dark === darkColor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateCalColor({
|
updateCalColor({
|
||||||
id: cal.id,
|
id: cal.id,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import logo from "../../static/header-logo.svg";
|
import logo from "../../static/header-logo.svg";
|
||||||
import AppsIcon from "@mui/icons-material/Apps";
|
import AppsIcon from "@mui/icons-material/Apps";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
@@ -53,8 +53,14 @@ export function Menubar({
|
|||||||
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null);
|
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
dispatch(push("/"));
|
||||||
|
}
|
||||||
|
}, [dispatch, user]);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
dispatch(push("/"));
|
return null;
|
||||||
}
|
}
|
||||||
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
|
|||||||
@@ -40,8 +40,16 @@ interface RejectedError {
|
|||||||
export const getCalendarsListAsync = createAsyncThunk<
|
export const getCalendarsListAsync = createAsyncThunk<
|
||||||
{ importedCalendars: Record<string, Calendars>; errors: string }, // Return type
|
{ importedCalendars: Record<string, Calendars>; errors: string }, // Return type
|
||||||
void, // Arg type
|
void, // Arg type
|
||||||
{ rejectValue: RejectedError } // ThunkAPI config
|
{ rejectValue: RejectedError; state: any } // ThunkAPI config
|
||||||
>("calendars/getCalendars", async (_, { rejectWithValue }) => {
|
>("calendars/getCalendars", async (_, { rejectWithValue, getState }) => {
|
||||||
|
const state = getState() as any;
|
||||||
|
if (Object.keys(state.calendars.list).length > 0) {
|
||||||
|
return {
|
||||||
|
importedCalendars: state.calendars.list,
|
||||||
|
errors: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const importedCalendars: Record<string, Calendars> = {};
|
const importedCalendars: Record<string, Calendars> = {};
|
||||||
const user = (await getOpenPaasUser()) as Record<string, string>;
|
const user = (await getOpenPaasUser()) as Record<string, string>;
|
||||||
@@ -106,15 +114,7 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
}
|
}
|
||||||
|
|
||||||
normalizedCalendars.forEach(
|
normalizedCalendars.forEach(
|
||||||
({
|
({ cal, description, delegated, link, id, ownerId, visibility }) => {
|
||||||
cal,
|
|
||||||
description,
|
|
||||||
delegated,
|
|
||||||
link,
|
|
||||||
id,
|
|
||||||
ownerId,
|
|
||||||
visibility,
|
|
||||||
}) => {
|
|
||||||
const ownerData = ownerDataMap.get(ownerId) || {
|
const ownerData = ownerDataMap.get(ownerId) || {
|
||||||
firstname: "",
|
firstname: "",
|
||||||
lastname: "Unknown User",
|
lastname: "Unknown User",
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ function EventPopover({
|
|||||||
const selectPersonalCalendars = createSelector(
|
const selectPersonalCalendars = createSelector(
|
||||||
(state: any) => state.calendars,
|
(state: any) => state.calendars,
|
||||||
(calendars: any) =>
|
(calendars: any) =>
|
||||||
Object.keys(calendars.list)
|
Object.keys(calendars.list || {})
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
if (id.split("/")[0] === userId) {
|
if (id.split("/")[0] === userId) {
|
||||||
return calendars.list[id];
|
return calendars.list?.[id];
|
||||||
}
|
}
|
||||||
return {} as Calendars;
|
return {} as Calendars;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export default function ImportAlert() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Object.keys(calendars).map((calendarId) =>
|
{Object.keys(calendars || {}).map((calendarId) =>
|
||||||
calendars[calendarId]?.events
|
calendars?.[calendarId]?.events
|
||||||
? Object.keys(calendars[calendarId]?.events)
|
? Object.keys(calendars[calendarId]?.events)
|
||||||
.filter((id) => calendars[calendarId]?.events[id].error)
|
.filter((id) => calendars[calendarId]?.events[id].error)
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { Auth } from "./oidcAuth";
|
import { Auth } from "./oidcAuth";
|
||||||
import { Loading } from "../../components/Loading/Loading";
|
import { Loading } from "../../components/Loading/Loading";
|
||||||
@@ -11,41 +11,50 @@ export function HandleLogin() {
|
|||||||
const userData = useAppSelector((state) => state.user);
|
const userData = useAppSelector((state) => state.user);
|
||||||
const calendars = useAppSelector((state) => state.calendars);
|
const calendars = useAppSelector((state) => state.calendars);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const hasInitiatedRef = useRef(false);
|
||||||
|
const calendarsLoadingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (hasInitiatedRef.current) return;
|
||||||
|
if (userData.userData) return;
|
||||||
|
if (Object.keys(calendars.list).length > 0) return;
|
||||||
|
if (calendarsLoadingRef.current) return;
|
||||||
|
|
||||||
|
hasInitiatedRef.current = true;
|
||||||
const initiateLogin = async () => {
|
const initiateLogin = async () => {
|
||||||
if (!userData.userData) {
|
const savedToken = sessionStorage.getItem("tokenSet")
|
||||||
const savedToken = sessionStorage.getItem("tokenSet")
|
? JSON.parse(sessionStorage.getItem("tokenSet")!)
|
||||||
? JSON.parse(sessionStorage.getItem("tokenSet")!)
|
: null;
|
||||||
: null;
|
const savedUser = sessionStorage.getItem("userData")
|
||||||
const savedUser = sessionStorage.getItem("userData")
|
? JSON.parse(sessionStorage.getItem("userData")!)
|
||||||
? JSON.parse(sessionStorage.getItem("userData")!)
|
: null;
|
||||||
: null;
|
|
||||||
|
|
||||||
if (savedToken && savedUser) {
|
if (savedToken && savedUser) {
|
||||||
dispatch(setTokens(savedToken));
|
dispatch(setTokens(savedToken));
|
||||||
dispatch(setUserData(savedUser));
|
dispatch(setUserData(savedUser));
|
||||||
await dispatch(getOpenPaasUserDataAsync());
|
await dispatch(getOpenPaasUserDataAsync());
|
||||||
await dispatch(getCalendarsListAsync());
|
calendarsLoadingRef.current = true;
|
||||||
dispatch(push("/calendar"));
|
await dispatch(getCalendarsListAsync());
|
||||||
return;
|
calendarsLoadingRef.current = false;
|
||||||
}
|
dispatch(push("/calendar"));
|
||||||
|
return;
|
||||||
const loginurl = await Auth();
|
|
||||||
|
|
||||||
sessionStorage.setItem(
|
|
||||||
"redirectState",
|
|
||||||
JSON.stringify({
|
|
||||||
code_verifier: loginurl.code_verifier,
|
|
||||||
state: loginurl.state,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
redirectTo(loginurl.redirectTo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginurl = await Auth();
|
||||||
|
|
||||||
|
sessionStorage.setItem(
|
||||||
|
"redirectState",
|
||||||
|
JSON.stringify({
|
||||||
|
code_verifier: loginurl.code_verifier,
|
||||||
|
state: loginurl.state,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
redirectTo(loginurl.redirectTo);
|
||||||
};
|
};
|
||||||
|
|
||||||
initiateLogin();
|
initiateLogin();
|
||||||
}, [userData, dispatch]);
|
}, [userData.userData, calendars.list, dispatch]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userData.error) {
|
if (userData.error) {
|
||||||
dispatch(push("/error"));
|
dispatch(push("/error"));
|
||||||
@@ -53,7 +62,7 @@ export function HandleLogin() {
|
|||||||
if (!calendars.pending && !userData.loading && !userData.error) {
|
if (!calendars.pending && !userData.loading && !userData.error) {
|
||||||
dispatch(push("/calendar"));
|
dispatch(push("/calendar"));
|
||||||
}
|
}
|
||||||
}, [calendars.pending, userData.loading]);
|
}, [calendars.pending, userData.loading, userData.error, dispatch]);
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { Callback } from "./oidcAuth";
|
import { Callback } from "./oidcAuth";
|
||||||
import { useAppDispatch } from "../../app/hooks";
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import { getOpenPaasUserDataAsync, setTokens, setUserData } from "./userSlice";
|
import { getOpenPaasUserDataAsync, setTokens, setUserData } from "./userSlice";
|
||||||
import { Loading } from "../../components/Loading/Loading";
|
import { Loading } from "../../components/Loading/Loading";
|
||||||
@@ -9,6 +9,7 @@ import { getCalendarsListAsync } from "../Calendars/CalendarSlice";
|
|||||||
export function CallbackResume() {
|
export function CallbackResume() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const hasRun = useRef(false);
|
const hasRun = useRef(false);
|
||||||
|
const calendarsLoadingRef = useRef(false);
|
||||||
const saved = sessionStorage.getItem("redirectState")
|
const saved = sessionStorage.getItem("redirectState")
|
||||||
? JSON.parse(sessionStorage.getItem("redirectState")!)
|
? JSON.parse(sessionStorage.getItem("redirectState")!)
|
||||||
: null;
|
: null;
|
||||||
@@ -23,7 +24,11 @@ export function CallbackResume() {
|
|||||||
dispatch(setUserData(data?.userinfo));
|
dispatch(setUserData(data?.userinfo));
|
||||||
dispatch(setTokens(data?.tokenSet));
|
dispatch(setTokens(data?.tokenSet));
|
||||||
await dispatch(getOpenPaasUserDataAsync());
|
await dispatch(getOpenPaasUserDataAsync());
|
||||||
await dispatch(getCalendarsListAsync());
|
if (!calendarsLoadingRef.current) {
|
||||||
|
calendarsLoadingRef.current = true;
|
||||||
|
await dispatch(getCalendarsListAsync());
|
||||||
|
calendarsLoadingRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
sessionStorage.removeItem("redirectState");
|
sessionStorage.removeItem("redirectState");
|
||||||
sessionStorage.setItem("tokenSet", JSON.stringify(data?.tokenSet));
|
sessionStorage.setItem("tokenSet", JSON.stringify(data?.tokenSet));
|
||||||
|
|||||||
Reference in New Issue
Block a user