// Server-filtered navigation (D-10). The server removes items the admin may // not open; the rail also omits a plugin whose side menu ends up empty. import { computed, readonly, ref } from 'vue' import { api } from '../api/client' import type { NavigationEntry } from '../api/types' import { controllerPath, pluginKey } from '../app/controllerRoutes' const entries = ref([]) export const navigation = readonly(entries) export async function loadNavigation(): Promise { const { data, response } = await api.GET('/navigation') entries.value = response.ok && data ? data.data : [] return entries.value } export function setNavigation(next: NavigationEntry[]): void { entries.value = next } /** Rail entries: plugins with at least one permitted side-menu item, by order. */ export const railEntries = computed(() => entries.value .filter((entry) => entry.sideMenu.length > 0) .map((entry, index) => ({ entry, index })) .sort((a, b) => a.entry.order - b.entry.order || a.index - b.index) .map(({ entry }) => entry), ) /** Path of the plugin's first permitted side-menu controller. */ export function firstControllerPath(entry: NavigationEntry): string | null { for (const item of entry.sideMenu) { const path = controllerPath(item.controller) if (path) { return path } } return controllerPath(entry.controller) } /** The rail entry owning a route's vendor and plugin segments. */ export function activeEntry(vendor: string, plugin: string): NavigationEntry | null { const key = `${vendor}.${plugin}` return ( railEntries.value.find( (entry) => pluginKey(entry.controller) === key || entry.sideMenu.some((item) => pluginKey(item.controller) === key), ) ?? null ) } /** Where "/" lands: the first plugin's first controller. */ export function homePath(): string | null { for (const entry of railEntries.value) { const path = firstControllerPath(entry) if (path) { return path } } return null } export function useNavigation() { return { navigation, railEntries, loadNavigation, firstControllerPath, activeEntry, homePath } }