fix: layout loading settings

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-09 20:47:30 -04:00
parent 4ea9dc8908
commit 65d60008da
3 changed files with 119 additions and 11 deletions
+3
View File
@@ -9,6 +9,7 @@ import { useTenantStore } from '@KTXC/stores/tenantStore'
import { useUserStore } from '@KTXC/stores/userStore' import { useUserStore } from '@KTXC/stores/userStore'
import { useL10nStore } from '@KTXC/stores/l10nStore' import { useL10nStore } from '@KTXC/stores/l10nStore'
import { useThemeStore } from '@KTXC/stores/themeStore' import { useThemeStore } from '@KTXC/stores/themeStore'
import { useLayoutStore } from '@KTXC/stores/layoutStore'
import { i18n } from '@KTXC/l10n/runtime' import { i18n } from '@KTXC/l10n/runtime'
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper' import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper'
import { FetchError } from '@KTXC/utils/helpers/fetch-wrapper-core' import { FetchError } from '@KTXC/utils/helpers/fetch-wrapper-core'
@@ -43,12 +44,14 @@ globalWindow.Pinia = PiniaLib as unknown
const userStore = useUserStore(); const userStore = useUserStore();
const l10nStore = useL10nStore(); const l10nStore = useL10nStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const layoutStore = useLayoutStore();
try { try {
const payload = await fetchWrapper.get('/init'); const payload = await fetchWrapper.get('/init');
moduleStore.init(payload?.modules ?? {}); moduleStore.init(payload?.modules ?? {});
tenantStore.init(payload?.tenant ?? null); tenantStore.init(payload?.tenant ?? null);
userStore.init(payload?.user ?? {}); userStore.init(payload?.user ?? {});
layoutStore.hydrateFromSettings();
themeStore.boot(); themeStore.boot();
// Resolve locale and load catalogs before modules boot // Resolve locale and load catalogs before modules boot
+42 -11
View File
@@ -13,29 +13,59 @@ export const useLayoutStore = defineStore('layout', () => {
// Sidebar state - initialize from settings or defaults // Sidebar state - initialize from settings or defaults
const userStore = useUserStore(); const userStore = useUserStore();
const tenantStore = useTenantStore(); 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<boolean>(booleanSetting('sidebar_drawer', true));
const miniSidebar = ref<boolean>(booleanSetting('mini_sidebar', false));
const menuMode = ref<MenuMode>('apps'); const menuMode = ref<MenuMode>('apps');
// Theme mode - user choice, falling back to the tenant default // Theme mode - user choice, falling back to the tenant default
const theme = ref<ThemeMode>( const theme = ref<ThemeMode>(initialTheme());
(userStore.getSetting('theme') as ThemeMode | null) ??
(tenantStore.getSetting('theme_default_mode') as ThemeMode | null) ?? let hydratingFromSettings = false;
'light'
);
// Watch and sync sidebar state to settings // Watch and sync sidebar state to settings
watch(sidebarDrawer, (value) => { watch(sidebarDrawer, (value) => {
if (hydratingFromSettings) return;
userStore.setSetting('sidebar_drawer', value); userStore.setSetting('sidebar_drawer', value);
}); }, { flush: 'sync' });
watch(miniSidebar, (value) => { watch(miniSidebar, (value) => {
if (hydratingFromSettings) return;
userStore.setSetting('mini_sidebar', value); userStore.setSetting('mini_sidebar', value);
}); }, { flush: 'sync' });
watch(theme, (value) => { watch(theme, (value) => {
if (hydratingFromSettings) return;
userStore.setSetting('theme', value); 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 // Actions
function toggleSidebarDrawer() { function toggleSidebarDrawer() {
@@ -75,6 +105,7 @@ export const useLayoutStore = defineStore('layout', () => {
setMiniSidebar, setMiniSidebar,
setMenuMode, setMenuMode,
toggleMenuMode, toggleMenuMode,
setTheme setTheme,
hydrateFromSettings,
}; };
}); });
+74
View File
@@ -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<string, string>()
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' })
})
})