[#475] fixed calendars selection persistence (#479)

This commit is contained in:
Camille Moussu
2026-01-26 11:14:42 +01:00
committed by GitHub
parent 7058f1c2d6
commit 1fc85e141e
5 changed files with 60 additions and 23 deletions
+34 -17
View File
@@ -135,7 +135,7 @@ describe("CalendarSelection", () => {
}, },
}, },
}, },
pending: false, pending: true,
}, },
}; };
it("renders calendars", async () => { it("renders calendars", async () => {
@@ -340,7 +340,7 @@ describe("calendar Availability search", () => {
events: {}, events: {},
}, },
}, },
pending: false, pending: true,
templist: {}, templist: {},
}, },
}; };
@@ -495,12 +495,20 @@ describe("calendar Availability search", () => {
}, 15000); }, 15000);
it("BUGFIX: can untoggle all calendar.personal", async () => { it("BUGFIX: can untoggle all calendar.personal", async () => {
jest
.spyOn(calendarDetailThunks, "getCalendarDetailAsync")
.mockImplementation(
() =>
({
type: "getCalendarDetailAsync",
unwrap: () => Promise.resolve({}),
}) as any
);
await act(async () => await act(async () =>
renderWithProviders(<CalendarTestWrapper />, { renderWithProviders(<CalendarTestWrapper />, {
user: preloadedState.user, user: preloadedState.user,
calendars: { calendars: {
list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] }, list: { "user1/cal1": preloadedState.calendars.list["user1/cal1"] },
pending: false,
}, },
}) })
); );
@@ -572,8 +580,8 @@ describe("calendar Availability search", () => {
calendarApi.changeView("dayGridMonth"); calendarApi.changeView("dayGridMonth");
fireEvent.click(screen.getByTestId("ChevronRightIcon")); fireEvent.click(screen.getByTestId("ChevronRightIcon"));
}); });
expect(spy).toHaveBeenCalledTimes(4); expect(spy).toHaveBeenCalledTimes(2);
const callArgs = spy.mock.calls[3][0]; const callArgs = spy.mock.calls[1][0];
expect(callArgs.calId).toBe("user1/cal1"); expect(callArgs.calId).toBe("user1/cal1");
const startDate = new Date( const startDate = new Date(
@@ -621,10 +629,13 @@ describe("calendar Availability search", () => {
const spy = jest const spy = jest
.spyOn(calendarDetailThunks, "getCalendarDetailAsync") .spyOn(calendarDetailThunks, "getCalendarDetailAsync")
.mockImplementation(() => ({ .mockImplementation(
type: "getCalendarDetailAsync", () =>
unwrap: () => Promise.resolve({}), ({
})) as any; type: "getCalendarDetailAsync",
unwrap: () => Promise.resolve({}),
}) as any
);
await act(async () => { await act(async () => {
renderWithProviders( renderWithProviders(
@@ -675,10 +686,13 @@ describe("calendar Availability search", () => {
const spy = jest const spy = jest
.spyOn(calendarDetailThunks, "getCalendarDetailAsync") .spyOn(calendarDetailThunks, "getCalendarDetailAsync")
.mockImplementation(() => ({ .mockImplementation(
type: "getCalendarDetailAsync", () =>
unwrap: () => Promise.resolve({}), ({
})) as any; type: "getCalendarDetailAsync",
unwrap: () => Promise.resolve({}),
}) as any
);
await act(async () => { await act(async () => {
renderWithProviders( renderWithProviders(
@@ -708,10 +722,13 @@ describe("calendar Availability search", () => {
it("does not make duplicate API calls for same calendar and range", async () => { it("does not make duplicate API calls for same calendar and range", async () => {
const spy = jest const spy = jest
.spyOn(calendarDetailThunks, "getCalendarDetailAsync") .spyOn(calendarDetailThunks, "getCalendarDetailAsync")
.mockImplementation(() => ({ .mockImplementation(
type: "getCalendarDetailAsync", () =>
unwrap: () => Promise.resolve({}), ({
})) as any; type: "getCalendarDetailAsync",
unwrap: () => Promise.resolve({}),
}) as any
);
await act(async () => { await act(async () => {
renderWithProviders( renderWithProviders(
+18 -4
View File
@@ -13,6 +13,7 @@ import {
} from "@/utils/dateUtils"; } from "@/utils/dateUtils";
import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid"; import { extractEventBaseUuid } from "@/utils/extractEventBaseUuid";
import { setSelectedCalendars as setSelectedCalendarsToStorage } from "@/utils/storage/setSelectedCalendars"; import { setSelectedCalendars as setSelectedCalendarsToStorage } from "@/utils/storage/setSelectedCalendars";
import { useSelectedCalendars } from "@/utils/storage/useSelectedCalendars";
import { browserDefaultTimeZone } from "@/utils/timezone"; import { browserDefaultTimeZone } from "@/utils/timezone";
import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
import frLocale from "@fullcalendar/core/locales/fr"; import frLocale from "@fullcalendar/core/locales/fr";
@@ -76,11 +77,14 @@ export default function CalendarApp({
(state) => state.settings.hideDeclinedEvents (state) => state.settings.hideDeclinedEvents
); );
const calendars = useAppSelector((state) => state.calendars.list); const calendars = useAppSelector((state) => state.calendars.list);
const isPending = useAppSelector((state) => state.calendars.pending);
const displayWeekNumbers = useAppSelector( const displayWeekNumbers = useAppSelector(
(state) => state.settings.displayWeekNumbers (state) => state.settings.displayWeekNumbers
); );
const tempcalendars = useAppSelector((state) => state.calendars.templist); const tempcalendars = useAppSelector((state) => state.calendars.templist);
const [selectedCalendars, setSelectedCalendars] = useState<string[]>([]); const storedCalendars = useSelectedCalendars();
const [selectedCalendars, setSelectedCalendars] =
useState<string[]>(storedCalendars);
const calendarLightSignature = useMemo(() => { const calendarLightSignature = useMemo(() => {
return Object.values(calendars || {}) return Object.values(calendars || {})
@@ -253,6 +257,8 @@ export default function CalendarApp({
}, [rangeKey]); }, [rangeKey]);
useEffect(() => { useEffect(() => {
if (isPending) return;
if (!rangeKey || sortedSelectedCalendars.length === 0) { if (!rangeKey || sortedSelectedCalendars.length === 0) {
activeLoadCompletedRef.current = true; activeLoadCompletedRef.current = true;
setActiveLoadCompleted(true); setActiveLoadCompleted(true);
@@ -320,8 +326,7 @@ export default function CalendarApp({
}, [dispatch, rangeKey, sortedSelectedCalendars, rangeStart, rangeEnd]); }, [dispatch, rangeKey, sortedSelectedCalendars, rangeStart, rangeEnd]);
useEffect(() => { useEffect(() => {
if (!rangeKey || !activeLoadCompleted) return; if (!rangeKey || !activeLoadCompleted || isPending) return;
const hiddenCalendars = calendarIds const hiddenCalendars = calendarIds
.filter((id) => !selectedCalendars.includes(id)) .filter((id) => !selectedCalendars.includes(id))
.filter((id) => { .filter((id) => {
@@ -355,6 +360,7 @@ export default function CalendarApp({
rangeStart, rangeStart,
rangeEnd, rangeEnd,
activeLoadCompleted, activeLoadCompleted,
isPending,
]); ]);
const calendarsWithClearedCache = useMemo(() => { const calendarsWithClearedCache = useMemo(() => {
@@ -369,6 +375,7 @@ export default function CalendarApp({
const processedCacheClearRef = useRef<Record<string, number>>({}); const processedCacheClearRef = useRef<Record<string, number>>({});
useEffect(() => { useEffect(() => {
if (isPending) return;
calendarsWithClearedCache.forEach(({ id, cleared }) => { calendarsWithClearedCache.forEach(({ id, cleared }) => {
if (processedCacheClearRef.current[id] === cleared) { if (processedCacheClearRef.current[id] === cleared) {
return; return;
@@ -397,7 +404,14 @@ export default function CalendarApp({
prefetchedCalendarsRef.current[id] = ""; prefetchedCalendarsRef.current[id] = "";
}); });
}); });
}, [calendarsWithClearedCache, dispatch, rangeKey, rangeStart, rangeEnd]); }, [
calendarsWithClearedCache,
dispatch,
rangeKey,
rangeStart,
rangeEnd,
isPending,
]);
useEffect(() => { useEffect(() => {
const currentIds = new Set(tempCalendarIds); const currentIds = new Set(tempCalendarIds);
+1 -1
View File
@@ -28,7 +28,7 @@ const CalendarSlice = createSlice({
initialState: { initialState: {
list: {} as Record<string, Calendar>, list: {} as Record<string, Calendar>,
templist: {} as Record<string, Calendar>, templist: {} as Record<string, Calendar>,
pending: false, pending: true,
error: null as string | null, error: null as string | null,
} as { } as {
list: Record<string, Calendar>; list: Record<string, Calendar>;
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
export function useSelectedCalendars(): string[] { export function useSelectedCalendars(): string[] {
const [calendars, setCalendars] = useState<string[]>(() => { const [calendars, setCalendars] = useState<string[]>(() => {
if (typeof window === "undefined") return [];
try { try {
return JSON.parse(localStorage.getItem("selectedCalendars") ?? "[]"); return JSON.parse(localStorage.getItem("selectedCalendars") ?? "[]");
} catch { } catch {
@@ -10,6 +11,8 @@ export function useSelectedCalendars(): string[] {
}); });
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return;
const onStorage = (e: StorageEvent) => { const onStorage = (e: StorageEvent) => {
if (e.key === "selectedCalendars") { if (e.key === "selectedCalendars") {
try { try {
+4 -1
View File
@@ -17,6 +17,7 @@ export function WebSocketGate() {
); );
const [isSocketOpen, setIsSocketOpen] = useState(false); const [isSocketOpen, setIsSocketOpen] = useState(false);
const isPending = useAppSelector((state) => state.calendars.pending);
const calendarList = useSelectedCalendars(); const calendarList = useSelectedCalendars();
const tempCalendarList = Object.keys( const tempCalendarList = Object.keys(
@@ -73,13 +74,15 @@ export function WebSocketGate() {
// Register using a diff with previous calendars // Register using a diff with previous calendars
useEffect(() => { useEffect(() => {
if (isPending) return;
syncCalendarRegistrations( syncCalendarRegistrations(
isSocketOpen, isSocketOpen,
socketRef, socketRef,
calendarList, calendarList,
previousCalendarListRef previousCalendarListRef
); );
}, [isSocketOpen, calendarList]); }, [isSocketOpen, calendarList, isPending]);
useEffect(() => { useEffect(() => {
syncCalendarRegistrations( syncCalendarRegistrations(