fix: keep every calendar hidden after reload when the user hid them all

This commit is contained in:
Linus Rath
2026-10-26 16:13:52 +01:00
parent 92fe2fd1b1
commit 89be5f1e5b
3 changed files with 57 additions and 1 deletions
+1
View File
@@ -70,6 +70,7 @@ export function snapshotAccount(accountId: string): void {
calendars: [...calendarState.calendars],
events: [...calendarState.events],
selectedCalendarIds: [...calendarState.selectedCalendarIds],
calendarSelectionChosen: calendarState.calendarSelectionChosen,
viewMode: calendarState.viewMode,
supportsCalendar: calendarState.supportsCalendar,
},
@@ -35,7 +35,7 @@ const own = [cal({ id: 'a', accountId: 'me' }), cal({ id: 'b', accountId: 'me' }
describe('calendar selection after a fetch', () => {
beforeEach(() => {
useCalendarStore.setState({ calendars: [], selectedCalendarIds: [], error: null });
useCalendarStore.setState({ calendars: [], selectedCalendarIds: [], calendarSelectionChosen: false, error: null });
});
it('leaves the list and the selection alone when the fetch fails', async () => {
@@ -106,6 +106,48 @@ describe('calendar selection after a fetch', () => {
});
});
describe('a selection the user made', () => {
beforeEach(() => {
useCalendarStore.setState({ calendars: own, selectedCalendarIds: ['a', 'b'], calendarSelectionChosen: false, error: null });
});
it('stays empty after the user hid every calendar', async () => {
useCalendarStore.getState().toggleCalendarVisibility('a');
useCalendarStore.getState().toggleCalendarVisibility('b');
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: own }));
expect(useCalendarStore.getState().selectedCalendarIds).toEqual([]);
});
it('keeps only the birthday calendar when the user hid every other one', async () => {
useCalendarStore.setState({ selectedCalendarIds: ['a', 'b', BIRTHDAY_CALENDAR_ID] });
useCalendarStore.getState().toggleCalendarVisibility('a');
useCalendarStore.getState().toggleCalendarVisibility('b');
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: own }));
expect(useCalendarStore.getState().selectedCalendarIds).toEqual([BIRTHDAY_CALENDAR_ID]);
});
it('falls back to all when the calendars it named were removed', async () => {
useCalendarStore.getState().toggleCalendarVisibility('b');
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: [cal({ id: 'c', accountId: 'me' })] }));
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['c']);
});
it('does not count showing the birthday calendar as a choice', async () => {
useCalendarStore.setState({ selectedCalendarIds: [] });
useCalendarStore.getState().toggleCalendarVisibility(BIRTHDAY_CALENDAR_ID);
await useCalendarStore.getState().fetchCalendars(clientReturning({ calendars: own }));
expect(useCalendarStore.getState().selectedCalendarIds).toEqual(['a', 'b', BIRTHDAY_CALENDAR_ID]);
});
});
describe('reconcileSelectedIds with unloaded accounts', () => {
it('keeps ids under an unloaded prefix verbatim instead of remapping them by raw id', () => {
const calendars = [cal({ id: 'A@h::b', originalId: 'b', localAccountId: 'A@h' })];
+13
View File
@@ -527,6 +527,8 @@ export function reconcileSelectedIds(
* default when no real calendar is left in it. The virtual birthday calendar
* always survives reconciliation, so it must not count - counting it pinned
* the selection to "birthdays only" once the real calendars had dropped out.
* A selection the user emptied by hiding every calendar (`chosen`) stays
* empty; one that named calendars which are gone now falls back.
*/
function selectionAfterFetch(
selectedCalendarIds: string[],
@@ -534,9 +536,11 @@ function selectionAfterFetch(
defaultSelected: string[],
unloadedPrefixes?: string[],
rawOwner?: string,
chosen = false,
): string[] {
const reconciled = reconcileSelectedIds(selectedCalendarIds, calendars, unloadedPrefixes, rawOwner);
if (reconciled.some((id) => id !== BIRTHDAY_CALENDAR_ID)) return reconciled;
if (chosen && !selectedCalendarIds.some((id) => id !== BIRTHDAY_CALENDAR_ID)) return reconciled;
return reconciled.includes(BIRTHDAY_CALENDAR_ID) ? [...defaultSelected, BIRTHDAY_CALENDAR_ID] : defaultSelected;
}
@@ -770,6 +774,8 @@ interface CalendarStore {
selectedDate: Date;
viewMode: CalendarViewMode;
selectedCalendarIds: string[];
/** The user picked the visible calendars by hand: an empty pick hides them all. */
calendarSelectionChosen: boolean;
selectedEventId: string | null;
isLoading: boolean;
isLoadingEvents: boolean;
@@ -857,6 +863,7 @@ const initialState = {
events: [],
selectedDate: displayNow(),
selectedCalendarIds: [] as string[],
calendarSelectionChosen: false,
selectedEventId: null as string | null,
isLoading: false,
isLoadingEvents: false,
@@ -928,6 +935,8 @@ export const useCalendarStore = create<CalendarStore>()(
defaultSelected,
// Shared calendars' ids are `<accountId>:<id>` (getAllCalendars).
failedAccountIds.map((accountId) => `${accountId}:`),
undefined,
get().calendarSelectionChosen,
),
});
} catch (error) {
@@ -1014,6 +1023,7 @@ export const useCalendarStore = create<CalendarStore>()(
calendars.map(c => c.id),
unloadedPrefixes,
activeLocalAccountId ?? undefined,
get().calendarSelectionChosen,
),
});
} catch (error) {
@@ -1653,6 +1663,8 @@ export const useCalendarStore = create<CalendarStore>()(
selectedCalendarIds: visible
? [...state.selectedCalendarIds, calendarId]
: state.selectedCalendarIds.filter(id => id !== calendarId),
// Showing birthdays is a setting, not a pick of calendars.
calendarSelectionChosen: state.calendarSelectionChosen || calendarId !== BIRTHDAY_CALENDAR_ID,
}));
void calendarHooks.onCalendarVisibilityToggle.emit({ calendarId, visible });
},
@@ -2060,6 +2072,7 @@ export const useCalendarStore = create<CalendarStore>()(
},
partialize: (state) => ({
selectedCalendarIds: state.selectedCalendarIds,
calendarSelectionChosen: state.calendarSelectionChosen,
viewMode: state.viewMode,
icalSubscriptions: state.icalSubscriptions,
deletedSubscriptionIds: state.deletedSubscriptionIds,