2026-04-09 23:03:55 +00:00
|
|
|
/**
|
|
|
|
|
* URL router for the SipRouter dashboard.
|
2026-05-22 13:45:21 +00:00
|
|
|
* Maps grouped URL paths to views in dees-simple-appdash.
|
2026-04-09 23:03:55 +00:00
|
|
|
*/
|
|
|
|
|
|
2026-05-22 13:45:21 +00:00
|
|
|
const SUBVIEW_MAP = {
|
|
|
|
|
overview: ['stats'],
|
|
|
|
|
telephony: ['calls', 'phone', 'routes', 'voicemail', 'ivr'],
|
|
|
|
|
configuration: ['contacts', 'providers'],
|
|
|
|
|
system: ['log'],
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SUBVIEW = {
|
|
|
|
|
overview: 'stats',
|
|
|
|
|
telephony: 'calls',
|
|
|
|
|
configuration: 'contacts',
|
|
|
|
|
system: 'log',
|
|
|
|
|
} as const satisfies { [key in TViewSlug]: TSubviewSlug };
|
|
|
|
|
|
|
|
|
|
const DEFAULT_ROUTE: IRouteState = {
|
|
|
|
|
view: 'overview',
|
|
|
|
|
subview: 'stats',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type TViewSlug = keyof typeof SUBVIEW_MAP;
|
|
|
|
|
type TSubviewSlug = (typeof SUBVIEW_MAP)[TViewSlug][number];
|
|
|
|
|
|
|
|
|
|
interface IRouteState {
|
|
|
|
|
view: TViewSlug;
|
|
|
|
|
subview: TSubviewSlug;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidView(view: string): view is TViewSlug {
|
|
|
|
|
return Object.prototype.hasOwnProperty.call(SUBVIEW_MAP, view);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidSubview(view: TViewSlug, subview: string): subview is TSubviewSlug {
|
|
|
|
|
return (SUBVIEW_MAP[view] as readonly string[]).includes(subview);
|
|
|
|
|
}
|
2026-04-09 23:03:55 +00:00
|
|
|
|
|
|
|
|
class AppRouter {
|
2026-05-22 13:45:21 +00:00
|
|
|
private currentRoute: IRouteState = DEFAULT_ROUTE;
|
|
|
|
|
private onNavigate: ((view: TViewSlug, subview: TSubviewSlug) => void) | null = null;
|
|
|
|
|
private initialized = false;
|
2026-04-09 23:03:55 +00:00
|
|
|
|
|
|
|
|
init(): void {
|
2026-05-22 13:45:21 +00:00
|
|
|
if (this.initialized) return;
|
|
|
|
|
|
|
|
|
|
this.applyRoute(this.parsePath(location.pathname) ?? DEFAULT_ROUTE, false, true);
|
2026-04-09 23:03:55 +00:00
|
|
|
|
|
|
|
|
// Handle browser back/forward.
|
|
|
|
|
window.addEventListener('popstate', () => {
|
2026-05-22 13:45:21 +00:00
|
|
|
this.applyRoute(this.parsePath(location.pathname) ?? DEFAULT_ROUTE, false, true);
|
2026-04-09 23:03:55 +00:00
|
|
|
});
|
2026-05-22 13:45:21 +00:00
|
|
|
|
|
|
|
|
this.initialized = true;
|
2026-04-09 23:03:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 13:45:21 +00:00
|
|
|
setNavigateHandler(handler: (view: TViewSlug, subview: TSubviewSlug) => void): void {
|
2026-04-09 23:03:55 +00:00
|
|
|
this.onNavigate = handler;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 13:45:21 +00:00
|
|
|
navigateToView(view: TViewSlug, subview?: TSubviewSlug, skipCallback = false): void {
|
|
|
|
|
const targetSubview = subview && isValidSubview(view, subview) ? subview : DEFAULT_SUBVIEW[view];
|
|
|
|
|
this.applyRoute({ view, subview: targetSubview }, skipCallback, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCurrentRoute(): IRouteState {
|
|
|
|
|
return this.currentRoute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isCurrentRoute(view: TViewSlug, subview: TSubviewSlug): boolean {
|
|
|
|
|
return this.currentRoute.view === view && this.currentRoute.subview === subview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private parsePath(pathname: string): IRouteState | null {
|
|
|
|
|
const segments = pathname.split('/').filter(Boolean);
|
|
|
|
|
if (segments.length === 0) return DEFAULT_ROUTE;
|
|
|
|
|
if (segments.length > 2) return null;
|
|
|
|
|
|
|
|
|
|
const [viewSegment, subviewSegment] = segments;
|
|
|
|
|
if (!isValidView(viewSegment)) return null;
|
|
|
|
|
|
|
|
|
|
const targetSubview = subviewSegment ?? DEFAULT_SUBVIEW[viewSegment];
|
|
|
|
|
if (!isValidSubview(viewSegment, targetSubview)) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
view: viewSegment,
|
|
|
|
|
subview: targetSubview,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private applyRoute(route: IRouteState, skipCallback: boolean, replace: boolean): void {
|
|
|
|
|
this.currentRoute = route;
|
|
|
|
|
|
|
|
|
|
const url = `/${route.view}/${route.subview}`;
|
|
|
|
|
if (location.pathname !== url) {
|
|
|
|
|
if (replace) {
|
|
|
|
|
history.replaceState(null, '', url);
|
|
|
|
|
} else {
|
2026-04-09 23:03:55 +00:00
|
|
|
history.pushState(null, '', url);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 13:45:21 +00:00
|
|
|
if (!skipCallback) {
|
|
|
|
|
this.onNavigate?.(route.view, route.subview);
|
2026-04-09 23:03:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const appRouter = new AppRouter();
|
2026-05-22 13:45:21 +00:00
|
|
|
export type { IRouteState, TSubviewSlug, TViewSlug };
|