diff --git a/src/pages/AdminPanel.tsx b/src/pages/AdminPanel.tsx
index f7aa384..5692dea 100644
--- a/src/pages/AdminPanel.tsx
+++ b/src/pages/AdminPanel.tsx
@@ -83,6 +83,7 @@ export default function AdminPanel() {
const permissions = useAccountStore((s) => s.permissions);
const setSession = useAuthStore((s) => s.setSession);
const accessToken = useAuthStore((s) => s.accessToken);
+ const activeAccountId = useAuthStore((s) => s.activeAccountId);
const setActiveSection = useUIStore((s) => s.setActiveSection);
const activeSection = useUIStore((s) => s.activeSection);
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
@@ -322,7 +323,11 @@ export default function AdminPanel() {
>
-
+ {/* Keyed on the active account: forces MainContent (and every
+ view it renders) to fully remount on switch, so
+ account-scoped views can't keep showing stale data fetched
+ under the previous account. */}
+
{section === 'Appearance' ? (
) : (
diff --git a/src/stores/authStore.test.ts b/src/stores/authStore.test.ts
index 71be984..c91c5de 100644
--- a/src/stores/authStore.test.ts
+++ b/src/stores/authStore.test.ts
@@ -6,6 +6,7 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useAuthStore } from './authStore';
+import { useCacheStore } from './cacheStore';
const initialState = {
accessToken: null,
@@ -21,6 +22,7 @@ const initialState = {
describe('authStore', () => {
beforeEach(() => {
useAuthStore.setState(initialState);
+ useCacheStore.setState({ displayNames: {}, objectLists: {} });
vi.restoreAllMocks();
});
@@ -124,6 +126,26 @@ describe('authStore', () => {
expect(useAuthStore.getState().activeAccountId).toBe('acc-1');
});
+
+ it('preserves the previously active account across a reload if still valid', () => {
+ useAuthStore.setState({ activeAccountId: 'acc-2' });
+ const accounts = {
+ 'acc-1': { name: 'Personal', isPersonal: true },
+ 'acc-2': { name: 'Shared', isPersonal: false },
+ };
+
+ useAuthStore.getState().setSession(accounts, 'acc-1', 'https://api');
+
+ expect(useAuthStore.getState().activeAccountId).toBe('acc-2');
+ });
+
+ it('falls back to primaryAccountId if the previously active account is gone', () => {
+ useAuthStore.setState({ activeAccountId: 'acc-removed' });
+
+ useAuthStore.getState().setSession({ 'acc-1': { name: 'A', isPersonal: true } }, 'acc-1', 'https://api');
+
+ expect(useAuthStore.getState().activeAccountId).toBe('acc-1');
+ });
});
describe('switchAccount', () => {
@@ -149,6 +171,33 @@ describe('authStore', () => {
useAuthStore.getState().switchAccount('nonexistent');
expect(useAuthStore.getState().activeAccountId).toBe('a1');
});
+
+ it('clears the cache store when switching to a different account', () => {
+ useAuthStore.setState({
+ accounts: {
+ a1: { name: 'A1', isPersonal: true },
+ a2: { name: 'A2', isPersonal: false },
+ },
+ activeAccountId: 'a1',
+ });
+ useCacheStore.getState().setDisplayNames('x:Domain', { d1: 'example.org' });
+
+ useAuthStore.getState().switchAccount('a2');
+
+ expect(useCacheStore.getState().displayNames).toEqual({});
+ });
+
+ it('does not clear the cache store when "switching" to the already-active account', () => {
+ useAuthStore.setState({
+ accounts: { a1: { name: 'A1', isPersonal: true } },
+ activeAccountId: 'a1',
+ });
+ useCacheStore.getState().setDisplayNames('x:Domain', { d1: 'example.org' });
+
+ useAuthStore.getState().switchAccount('a1');
+
+ expect(useCacheStore.getState().displayNames).toEqual({ 'x:Domain': { d1: 'example.org' } });
+ });
});
describe('logout', () => {
diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts
index 8b9d58d..f62f1f4 100644
--- a/src/stores/authStore.ts
+++ b/src/stores/authStore.ts
@@ -6,6 +6,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
+import { useCacheStore } from '@/stores/cacheStore';
interface AccountInfo {
name: string;
@@ -71,10 +72,15 @@ export const useAuthStore = create()(
},
setSession: (accounts, primaryAccountId, apiUrl, maxObjectsInGet, maxObjectsInSet) => {
+ // Preserve the previously active account across a page reload (the
+ // session is refetched from scratch on every load) instead of
+ // always resetting to the primary account, as long as it's still
+ // in the refreshed accounts map.
+ const current = get().activeAccountId;
set({
accounts,
primaryAccountId,
- activeAccountId: primaryAccountId,
+ activeAccountId: current && accounts[current] ? current : primaryAccountId,
apiUrl,
...(maxObjectsInGet !== undefined ? { maxObjectsInGet } : {}),
...(maxObjectsInSet !== undefined ? { maxObjectsInSet } : {}),
@@ -82,9 +88,12 @@ export const useAuthStore = create()(
},
switchAccount: (accountId) => {
- const { accounts } = get();
- if (accounts[accountId]) {
+ const { accounts, activeAccountId } = get();
+ if (accounts[accountId] && accountId !== activeAccountId) {
set({ activeAccountId: accountId });
+ // Cached objectId display names/lists were resolved against the
+ // previous account and would otherwise show stale labels.
+ useCacheStore.getState().clearAll();
}
},
@@ -134,6 +143,7 @@ export const useAuthStore = create()(
tokenExpiresAt: state.tokenExpiresAt,
tokenEndpoint: state.tokenEndpoint,
endSessionEndpoint: state.endSessionEndpoint,
+ activeAccountId: state.activeAccountId,
}) as AuthState,
},
),
diff --git a/src/stores/cacheStore.ts b/src/stores/cacheStore.ts
index d99d2f3..526ee1a 100644
--- a/src/stores/cacheStore.ts
+++ b/src/stores/cacheStore.ts
@@ -24,6 +24,7 @@ interface CacheState {
getObjectList: (key: string) => ObjectListEntry[] | undefined;
invalidateObjectList: (key: string) => void;
invalidateAllObjectLists: () => void;
+ clearAll: () => void;
}
export const useCacheStore = create()((set, get) => ({
@@ -75,4 +76,10 @@ export const useCacheStore = create()((set, get) => ({
invalidateAllObjectLists: () => {
set({ objectLists: {} });
},
+
+ // Switching the active JMAP account can make cached objectId display
+ // names and lists resolve against the wrong account's data.
+ clearAll: () => {
+ set({ displayNames: {}, objectLists: {} });
+ },
}));