import * as plugins from './plugins.js'; import * as appstate from './appstate.js'; const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter; const flatViews = ['dashboard', 'settings'] as const; const subviewMap: Record = { apps: ['app-store', 'services'] as const, network: ['proxy', 'domains', 'dns-records'] as const, registry: ['registries', 'tokens'] as const, }; const defaultSubview: Record = { apps: 'app-store', network: 'proxy', registry: 'registries', }; const legacySubviewTargetMap: Record = { 'app-store': { view: 'apps', subview: 'app-store' }, services: { view: 'apps', subview: 'services' }, proxy: { view: 'network', subview: 'proxy' }, domains: { view: 'network', subview: 'domains' }, 'dns-records': { view: 'network', subview: 'dns-records' }, registries: { view: 'registry', subview: 'registries' }, tokens: { view: 'registry', subview: 'tokens' }, }; export const validTopLevelViews = [...flatViews, ...Object.keys(subviewMap)] as const; export type TValidView = typeof validTopLevelViews[number]; export function isValidView(view: string): boolean { return (validTopLevelViews as readonly string[]).includes(view); } export function isValidSubview(view: string, subview: string): boolean { return subviewMap[view]?.includes(subview) ?? false; } class AppRouter { private router: InstanceType; private initialized = false; private suppressStateUpdate = false; constructor() { this.router = new SmartRouter({ debug: false }); } public init(): void { if (this.initialized) return; this.setupRoutes(); this.setupStateSync(); this.handleInitialRoute(); this.initialized = true; } private setupRoutes(): void { for (const view of flatViews) { this.router.on(`/${view}`, async () => { this.updateViewState(view, null); }); } for (const view of Object.keys(subviewMap)) { this.router.on(`/${view}`, async () => { this.navigateTo(`/${view}/${defaultSubview[view]}`); }); for (const subview of subviewMap[view]) { this.router.on(`/${view}/${subview}`, async () => { this.updateViewState(view, subview); }); } } this.router.on('/', async () => { this.navigateTo('/dashboard'); }); } private setupStateSync(): void { appstate.uiStatePart.select().subscribe((uiState: appstate.IUiState) => { if (this.suppressStateUpdate) return; const currentPath = window.location.pathname; const expectedPath = uiState.activeSubview ? `/${uiState.activeView}/${uiState.activeSubview}` : `/${uiState.activeView}`; if (currentPath !== expectedPath) { this.suppressStateUpdate = true; this.router.pushUrl(expectedPath); this.suppressStateUpdate = false; } }); } private handleInitialRoute(): void { const path = window.location.pathname; if (!path || path === '/') { this.router.pushUrl('/dashboard'); return; } const segments = path.split('/').filter(Boolean); const view = segments[0]; const subview = segments[1]; if (!isValidView(view)) { this.router.pushUrl('/dashboard'); return; } if (subviewMap[view]) { if (subview && isValidSubview(view, subview)) { this.updateViewState(view, subview); } else { this.router.pushUrl(`/${view}/${defaultSubview[view]}`); } } else { this.updateViewState(view, null); } } private updateViewState(view: string, subview: string | null): void { this.suppressStateUpdate = true; const currentState = appstate.uiStatePart.getState(); if (currentState.activeView !== view || currentState.activeSubview !== subview) { appstate.uiStatePart.setState({ ...currentState, activeView: view, activeSubview: subview, }); } this.suppressStateUpdate = false; } public navigateTo(path: string): void { this.router.pushUrl(path); } public navigateToView(view: string, subview?: string): void { const normalizedView = view.toLowerCase().replace(/\s+/g, '-'); const normalizedSubview = subview?.toLowerCase().replace(/\s+/g, '-'); if (!isValidView(normalizedView)) { const legacyTarget = legacySubviewTargetMap[normalizedView]; if (legacyTarget) { this.navigateToView(legacyTarget.view, legacyTarget.subview); return; } this.navigateTo('/dashboard'); return; } if (normalizedSubview && isValidSubview(normalizedView, normalizedSubview)) { this.navigateTo(`/${normalizedView}/${normalizedSubview}`); } else if (subviewMap[normalizedView]) { this.navigateTo(`/${normalizedView}/${defaultSubview[normalizedView]}`); } else { this.navigateTo(`/${normalizedView}`); } } public getCurrentView(): string { const uiState = appstate.uiStatePart.getState(); return uiState.activeSubview ? `${uiState.activeView}/${uiState.activeSubview}` : uiState.activeView; } public destroy(): void { this.router.destroy(); this.initialized = false; } } export const appRouter = new AppRouter();