fix: show themes the admin installed in the theme picker without opening mail first

This commit is contained in:
Linus Rath
2026-10-29 00:55:41 +01:00
parent 367e29913a
commit 6b41692d81
5 changed files with 39 additions and 6 deletions
@@ -3,6 +3,7 @@ import { act, render } from '@testing-library/react';
import { appLifecycleHooks, clearAllHooks, routerHooks } from '@/lib/plugin-hooks';
import { usePluginStore } from '@/stores/plugin-store';
import { useAuthStore } from '@/stores/auth-store';
import { useThemeStore } from '@/stores/theme-store';
let pathname = '/';
vi.mock('next/navigation', () => ({ usePathname: () => pathname }));
@@ -19,6 +20,7 @@ beforeEach(() => {
clearAllHooks();
pathname = '/';
usePluginStore.setState({ initialized: false, initializePlugins: vi.fn().mockResolvedValue(undefined) });
useThemeStore.setState({ syncServerThemes: vi.fn().mockResolvedValue(undefined) });
useAuthStore.setState({ isAuthenticated: false });
});
afterEach(() => {
@@ -83,6 +85,18 @@ describe('app events for plugins', () => {
expect(usePluginStore.getState().initializePlugins).toHaveBeenCalledTimes(1);
});
// A theme the admin installed used to arrive only once the mail view had
// been opened, so Settings opened directly never listed it.
it('syncs the server themes once the user is signed in, on any page', () => {
pathname = '/settings';
render(<PluginAppEvents />);
expect(useThemeStore.getState().syncServerThemes).not.toHaveBeenCalled();
act(() => { useAuthStore.setState({ isAuthenticated: true }); });
expect(useThemeStore.getState().syncServerThemes).toHaveBeenCalledTimes(1);
});
it('reports navigation between pages', () => {
const navigate = listen(routerHooks.onNavigate);
const { rerender } = render(<PluginAppEvents />);
+2 -3
View File
@@ -130,7 +130,6 @@ import { useDocumentTitle } from "@/hooks/use-document-title";
import { mailTitleContext } from "@/lib/tab-title";
import { usePluginStore } from "@/stores/plugin-store";
import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot";
import { useThemeStore } from "@/stores/theme-store";
import { consumePendingMailto, subscribeToPendingMailto } from "@/lib/protocol-handlers/session";
import { INTERNAL_MAILTO_EVENT, type ParsedMailto } from "@/lib/protocol-handlers/mailto";
import { PLUGIN_REPLY_EVENT, type PluginReplyRequest } from "@/lib/plugin-sandbox/host-api-compose";
@@ -1339,10 +1338,10 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
}, [checkAuth]);
// Initialize plugins on mount (re-activates enabled plugins after refresh)
// Also syncs server-managed plugins and themes to the client
// Also syncs server-managed plugins to the client; themes sync from
// PluginAppEvents on every page.
useEffect(() => {
usePluginStore.getState().initializePlugins();
useThemeStore.getState().syncServerThemes();
}, []);
// Hydrate persisted column widths from localStorage
+6 -1
View File
@@ -9,6 +9,7 @@ import { useEffect, useRef } from 'react';
import { usePathname } from 'next/navigation';
import { appLifecycleHooks, routerHooks, toastHooks, uiHooks } from '@/lib/plugin-hooks';
import { usePluginStore } from '@/stores/plugin-store';
import { useThemeStore } from '@/stores/theme-store';
import { useAuthStore } from '@/stores/auth-store';
import { useAccountStore } from '@/stores/account-store';
@@ -126,8 +127,12 @@ export function PluginAppEvents() {
// Start the plugins on whichever page the user lands on. They used to start
// only from the mail view, so a plugin's sidebar entry, page or reminders
// were missing on Calendar, Contacts, Files or Settings opened directly.
// Server themes had the same problem: one installed by the admin never
// reached the theme picker until the mail view had been opened.
useEffect(() => {
if (signedIn) void usePluginStore.getState().initializePlugins();
if (!signedIn) return;
void usePluginStore.getState().initializePlugins();
void useThemeStore.getState().syncServerThemes();
}, [signedIn]);
const readyFired = useRef(false);
@@ -1,5 +1,5 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ThemesSettings } from '../themes-settings';
import { useThemeStore } from '@/stores/theme-store';
@@ -16,7 +16,16 @@ function cardNames(): string[] {
describe('ThemesSettings collapse', () => {
beforeEach(() => {
act(() => { useThemeStore.setState({ activeThemeId: null }); });
act(() => {
useThemeStore.setState({ activeThemeId: null, syncServerThemes: vi.fn().mockResolvedValue(undefined) });
});
});
// Without this, a theme the admin installed in the same tab stayed out of
// the gallery until a reload.
it('fetches the server themes when it opens', () => {
render(<ThemesSettings embedded />);
expect(useThemeStore.getState().syncServerThemes).toHaveBeenCalledTimes(1);
});
it('shows three cards and a Show all button', () => {
+6
View File
@@ -25,6 +25,12 @@ export function ThemesSettings({ embedded }: { embedded?: boolean } = {}) {
const themePolicy = getThemePolicy();
const forcedThemeId = getForcedThemeId(installedThemes.map((theme) => theme.id));
// Fetch again on open, so a theme the admin just installed in this tab
// shows up without a reload.
useEffect(() => {
void useThemeStore.getState().syncServerThemes();
}, []);
// Render each preview in the variant matching the app's current mode, and
// keep it in sync when the user toggles light/dark elsewhere.
const [isDark, setIsDark] = useState(false);