feat(web): group onebox sidebar navigation

This commit is contained in:
2026-05-21 18:38:44 +00:00
parent ba370cbce8
commit 3f6b058ce5
4 changed files with 279 additions and 83 deletions
+94 -24
View File
@@ -3,12 +3,40 @@ import * as appstate from './appstate.js';
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
export const validViews = [
'dashboard', 'app-store', 'services', 'domains', 'dns-records', 'network',
'registries', 'tokens', 'settings',
] as const;
const flatViews = ['dashboard', 'settings'] as const;
export type TValidView = typeof validViews[number];
const subviewMap: Record<string, readonly string[]> = {
apps: ['app-store', 'services'] as const,
network: ['proxy', 'domains', 'dns-records'] as const,
registry: ['registries', 'tokens'] as const,
};
const defaultSubview: Record<string, string> = {
apps: 'app-store',
network: 'proxy',
registry: 'registries',
};
const legacySubviewTargetMap: Record<string, { view: string; subview: string }> = {
'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<typeof SmartRouter>;
@@ -28,24 +56,37 @@ class AppRouter {
}
private setupRoutes(): void {
for (const view of validViews) {
for (const view of flatViews) {
this.router.on(`/${view}`, async () => {
this.updateViewState(view);
this.updateViewState(view, null);
});
}
// Root redirect
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((s) => s.activeView).subscribe((activeView) => {
appstate.uiStatePart.select().subscribe((uiState: appstate.IUiState) => {
if (this.suppressStateUpdate) return;
const currentPath = window.location.pathname;
const expectedPath = `/${activeView}`;
const expectedPath = uiState.activeSubview
? `/${uiState.activeView}/${uiState.activeSubview}`
: `/${uiState.activeView}`;
if (currentPath !== expectedPath) {
this.suppressStateUpdate = true;
@@ -60,25 +101,37 @@ class AppRouter {
if (!path || path === '/') {
this.router.pushUrl('/dashboard');
} else {
const segments = path.split('/').filter(Boolean);
const view = segments[0];
return;
}
if (validViews.includes(view as TValidView)) {
this.updateViewState(view as TValidView);
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('/dashboard');
this.router.pushUrl(`/${view}/${defaultSubview[view]}`);
}
} else {
this.updateViewState(view, null);
}
}
private updateViewState(view: string): void {
private updateViewState(view: string, subview: string | null): void {
this.suppressStateUpdate = true;
const currentState = appstate.uiStatePart.getState();
if (currentState.activeView !== view) {
if (currentState.activeView !== view || currentState.activeSubview !== subview) {
appstate.uiStatePart.setState({
...currentState,
activeView: view,
activeSubview: subview,
});
}
this.suppressStateUpdate = false;
@@ -88,17 +141,34 @@ class AppRouter {
this.router.pushUrl(path);
}
public navigateToView(view: string): void {
const normalized = view.toLowerCase().replace(/\s+/g, '-');
if (validViews.includes(normalized as TValidView)) {
this.navigateTo(`/${normalized}`);
} else {
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 {
return appstate.uiStatePart.getState().activeView;
const uiState = appstate.uiStatePart.getState();
return uiState.activeSubview
? `${uiState.activeView}/${uiState.activeSubview}`
: uiState.activeView;
}
public destroy(): void {