From 65d60008da382b5ba217f07eff1218eed8676096 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Thu, 9 Jul 2026 20:47:30 -0400 Subject: [PATCH] fix: layout loading settings Signed-off-by: Sebastian Krupinski --- core/src/private.ts | 3 ++ core/src/stores/layoutStore.ts | 53 +++++++++++++++++----- tests/js/unit/layoutStore.test.ts | 74 +++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 tests/js/unit/layoutStore.test.ts diff --git a/core/src/private.ts b/core/src/private.ts index 096a160..08eb1d7 100644 --- a/core/src/private.ts +++ b/core/src/private.ts @@ -9,6 +9,7 @@ import { useTenantStore } from '@KTXC/stores/tenantStore' import { useUserStore } from '@KTXC/stores/userStore' import { useL10nStore } from '@KTXC/stores/l10nStore' import { useThemeStore } from '@KTXC/stores/themeStore' +import { useLayoutStore } from '@KTXC/stores/layoutStore' import { i18n } from '@KTXC/l10n/runtime' import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper' import { FetchError } from '@KTXC/utils/helpers/fetch-wrapper-core' @@ -43,12 +44,14 @@ globalWindow.Pinia = PiniaLib as unknown const userStore = useUserStore(); const l10nStore = useL10nStore(); const themeStore = useThemeStore(); + const layoutStore = useLayoutStore(); try { const payload = await fetchWrapper.get('/init'); moduleStore.init(payload?.modules ?? {}); tenantStore.init(payload?.tenant ?? null); userStore.init(payload?.user ?? {}); + layoutStore.hydrateFromSettings(); themeStore.boot(); // Resolve locale and load catalogs before modules boot diff --git a/core/src/stores/layoutStore.ts b/core/src/stores/layoutStore.ts index cdf9fcd..5076fda 100644 --- a/core/src/stores/layoutStore.ts +++ b/core/src/stores/layoutStore.ts @@ -13,29 +13,59 @@ export const useLayoutStore = defineStore('layout', () => { // Sidebar state - initialize from settings or defaults const userStore = useUserStore(); const tenantStore = useTenantStore(); - const sidebarDrawer = ref(userStore.getSetting('sidebar_drawer') ?? true); - const miniSidebar = ref(userStore.getSetting('mini_sidebar') ?? false); + + function booleanSetting(key: string, fallback: boolean): boolean { + const value = userStore.getSetting(key); + return typeof value === 'boolean' ? value : fallback; + } + + function themeSetting(value: unknown): ThemeMode | null { + return value === 'light' || value === 'dark' || value === 'system' ? value : null; + } + + function initialTheme(): ThemeMode { + return ( + themeSetting(userStore.getSetting('theme')) ?? + themeSetting(tenantStore.getSetting('theme_default_mode')) ?? + 'light' + ); + } + + const sidebarDrawer = ref(booleanSetting('sidebar_drawer', true)); + const miniSidebar = ref(booleanSetting('mini_sidebar', false)); const menuMode = ref('apps'); // Theme mode - user choice, falling back to the tenant default - const theme = ref( - (userStore.getSetting('theme') as ThemeMode | null) ?? - (tenantStore.getSetting('theme_default_mode') as ThemeMode | null) ?? - 'light' - ); + const theme = ref(initialTheme()); + + let hydratingFromSettings = false; // Watch and sync sidebar state to settings watch(sidebarDrawer, (value) => { + if (hydratingFromSettings) return; userStore.setSetting('sidebar_drawer', value); - }); + }, { flush: 'sync' }); watch(miniSidebar, (value) => { + if (hydratingFromSettings) return; userStore.setSetting('mini_sidebar', value); - }); + }, { flush: 'sync' }); watch(theme, (value) => { + if (hydratingFromSettings) return; userStore.setSetting('theme', value); - }); + }, { flush: 'sync' }); + + function hydrateFromSettings() { + hydratingFromSettings = true; + try { + sidebarDrawer.value = booleanSetting('sidebar_drawer', true); + miniSidebar.value = booleanSetting('mini_sidebar', false); + theme.value = initialTheme(); + } finally { + hydratingFromSettings = false; + } + } // Actions function toggleSidebarDrawer() { @@ -75,6 +105,7 @@ export const useLayoutStore = defineStore('layout', () => { setMiniSidebar, setMenuMode, toggleMenuMode, - setTheme + setTheme, + hydrateFromSettings, }; }); diff --git a/tests/js/unit/layoutStore.test.ts b/tests/js/unit/layoutStore.test.ts new file mode 100644 index 0000000..a1790a7 --- /dev/null +++ b/tests/js/unit/layoutStore.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { useLayoutStore } from '@KTXC/stores/layoutStore' +import { useTenantStore } from '@KTXC/stores/tenantStore' +import { useUserStore } from '@KTXC/stores/userStore' + +const userServiceMock = vi.hoisted(() => ({ + updateProfile: vi.fn(() => Promise.resolve()), + updateSettings: vi.fn(() => Promise.resolve()), + flushAll: vi.fn(() => Promise.resolve()), +})) + +vi.mock('@KTXC/services/user/userService', () => ({ + userService: userServiceMock, +})) + +describe('layoutStore', () => { + beforeEach(() => { + const storage = new Map() + vi.stubGlobal('localStorage', { + getItem: vi.fn((key: string) => storage.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => storage.set(key, value)), + removeItem: vi.fn((key: string) => storage.delete(key)), + clear: vi.fn(() => storage.clear()), + }) + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('hydrates initial layout preferences after settings are loaded', () => { + const userStore = useUserStore() + const tenantStore = useTenantStore() + + const layoutStore = useLayoutStore() + expect(layoutStore.sidebarDrawer).toBe(true) + expect(layoutStore.miniSidebar).toBe(false) + expect(layoutStore.theme).toBe('light') + + tenantStore.init({ + id: 'tenant', + domain: 'example.test', + label: 'Example', + settings: { + theme_default_mode: 'dark', + }, + }) + userStore.init({ + settings: { + sidebar_drawer: false, + mini_sidebar: true, + theme: 'system', + }, + }) + + layoutStore.hydrateFromSettings() + + expect(layoutStore.sidebarDrawer).toBe(false) + expect(layoutStore.miniSidebar).toBe(true) + expect(layoutStore.theme).toBe('system') + expect(userServiceMock.updateSettings).not.toHaveBeenCalled() + }) + + it('continues to persist user-driven layout changes', () => { + const layoutStore = useLayoutStore() + + layoutStore.toggleSidebarDrawer() + layoutStore.setMiniSidebar(true) + layoutStore.setTheme('dark') + + expect(userServiceMock.updateSettings).toHaveBeenCalledWith({ sidebar_drawer: false }) + expect(userServiceMock.updateSettings).toHaveBeenCalledWith({ mini_sidebar: true }) + expect(userServiceMock.updateSettings).toHaveBeenCalledWith({ theme: 'dark' }) + }) +})