fix: take the browser's templates into the account as soon as its synced settings have loaded

This commit is contained in:
Linus Rath
2026-10-28 08:29:03 +01:00
parent f1bcba6222
commit dfa37ee311
3 changed files with 68 additions and 1 deletions
@@ -0,0 +1,48 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
// The driver syncs the active account's templates when its synced settings
// have loaded, also when the load changed nothing in the browser's list:
// only then is it known whose that list is.
const sync = vi.hoisted(() => ({ schedule: vi.fn(), refresh: vi.fn() }));
vi.mock('@/lib/templates/personal-templates', () => ({
scheduleAccountSync: sync.schedule,
refreshAccountTemplates: sync.refresh,
}));
vi.mock('@/lib/templates/team-templates', () => ({ refreshTeamTemplates: vi.fn() }));
vi.mock('@/lib/browser-navigation', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
apiFetch: vi.fn(async () => new Response(JSON.stringify({ settings: { theme: 'dark' } }), { status: 200 })),
}));
import { AccountTemplateSync } from '../account-template-sync';
import { useAuthStore } from '@/stores/auth-store';
import { useSettingsStore } from '@/stores/settings-store';
const ACCOUNT = 'carol@example.org@mail.example.com';
const original = useAuthStore.getState();
beforeEach(() => {
sync.schedule.mockClear();
localStorage.removeItem('settings-sync-owner');
useAuthStore.setState({ isAuthenticated: true, activeAccountId: ACCOUNT, client: {} as never });
});
afterEach(() => {
useAuthStore.setState({ isAuthenticated: original.isAuthenticated, activeAccountId: original.activeAccountId, client: original.client });
});
describe('AccountTemplateSync', () => {
it('syncs the active account once its settings have loaded', async () => {
render(<AccountTemplateSync />);
expect(sync.schedule).not.toHaveBeenCalled();
await useSettingsStore.getState().loadFromServer('carol@example.org', 'https://mail.example.com');
expect(sync.schedule).toHaveBeenCalledWith(ACCOUNT);
});
it('leaves another account\'s settings load alone', async () => {
render(<AccountTemplateSync />);
await useSettingsStore.getState().loadFromServer('dave@example.org', 'https://mail.example.com');
expect(sync.schedule).not.toHaveBeenCalled();
});
});
+11 -1
View File
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth-store';
import { useAccountTemplatesStore } from '@/stores/account-templates-store';
import { refreshAccountTemplates, scheduleAccountSync } from '@/lib/templates/personal-templates';
import { useTemplateStore } from '@/stores/template-store';
import { onSettingsLoadSettled } from '@/stores/settings-store';
import { refreshTeamTemplates } from '@/lib/templates/team-templates';
/**
@@ -26,7 +27,16 @@ export function AccountTemplateSync() {
// loaded, or a template changed there) is merged in at once.
useEffect(() => {
if (!isAuthenticated || !activeAccountId) return;
return useTemplateStore.subscribe(() => scheduleAccountSync(activeAccountId));
const unsubscribe = useTemplateStore.subscribe(() => scheduleAccountSync(activeAccountId));
// Once the account's synced settings have loaded, its browser list is
// known, even when the load changed nothing in it.
const unlisten = onSettingsLoadSettled((accountId) => {
if (accountId === activeAccountId) scheduleAccountSync(activeAccountId);
});
return () => {
unsubscribe();
unlisten();
};
}, [activeAccountId, isAuthenticated]);
useEffect(() => {
+9
View File
@@ -65,6 +65,14 @@ export function isSettingsLoadSettled(accountId: string): boolean {
return settledLoads.has(accountId);
}
const settledListeners = new Set<(accountId: string) => void>();
/** Called whenever an account's settings load finishes (with or without a copy). */
export function onSettingsLoadSettled(listener: (accountId: string) => void): () => void {
settledListeners.add(listener);
return () => settledListeners.delete(listener);
}
/** The account whose server copy the local settings (and template list) mirror; null if none. */
export function readSettingsOwner(): string | null {
try {
@@ -1819,6 +1827,7 @@ export const useSettingsStore = create<SettingsState>()(
return failWithoutForeignState();
} finally {
settledLoads.add(accountId);
for (const listener of settledListeners) listener(accountId);
}
},