feat(navigation): apply plugin layout

This commit is contained in:
digua committed 2026-08-18 14:29:38 +08:00
1 parent b5816be99f
commit 7907435cff
14 files changed
+427 -27

No files matched your search

+9 -1
View File
@@ -18,8 +18,15 @@ import { WebWasmSessionSync } from './session-sync'
import { useSessionStore } from '@/stores/session'
import { initializeWebWasmLocale } from './locale-bootstrap'
import { installInsightPluginRuntime } from '@/plugins/insight-vue'
import { webWasmInsightBuiltins, webWasmInsightRuntime, webWasmUiHost, webWasmUiServices } from '@/plugins/web-wasm'
import {
webWasmInsightBuiltins,
webWasmInsightRuntime,
webWasmNavigationLayout,
webWasmUiHost,
webWasmUiServices,
} from '@/plugins/web-wasm'
import { installStaticInsightPluginUiServices } from '@/plugins/static-insight'
import { installNavigationLayout } from '@/navigation/vue'
import '@/assets/styles/main.css'
initializeWebWasmLocale()
@@ -53,6 +60,7 @@ async function start(): Promise<void> {
app.use(router)
app.use(ui)
app.use(i18n)
installNavigationLayout(app, webWasmNavigationLayout)
await installStaticInsightPluginUiServices(webWasmInsightBuiltins, webWasmInsightRuntime, webWasmUiServices)
installInsightPluginRuntime(app, webWasmInsightRuntime)
@@ -87,8 +87,20 @@ test('navigation layout validation enforces two levels, stable unique entries, a
() =>
normalizeNavigationLayout({
...layout,
primary: [{ kind: 'group', id: 'group', title: 'Group', children: [{ kind: 'entry', entryId: 'nested' }] }],
primary: [{ kind: 'group', id: 'group', children: [{ kind: 'entry', entryId: 'nested' }] }],
}),
/must be a non-empty string/
)
assert.deepEqual(
normalizeNavigationLayout({
schemaVersion: 1,
primary: [{ kind: 'group', id: 'host.insight', children: [] }],
hiddenEntryIds: [],
}),
{
schemaVersion: 1,
primary: [{ kind: 'group', id: 'host.insight', children: [] }],
hiddenEntryIds: [],
}
)
})
@@ -101,15 +101,20 @@ function normalizePrimaryItem(
if (item.kind === 'group') {
const id = requireId(item.id, `primary[${index}].id`)
addUnique(groupIds, id, 'navigation group')
if (typeof item.title !== 'string' || item.title.trim().length === 0) {
throw new NavigationLayoutValidationError(`primary[${index}].title must be a non-empty string`)
if (item.title !== undefined && (typeof item.title !== 'string' || item.title.trim().length === 0)) {
throw new NavigationLayoutValidationError(`primary[${index}].title must be a non-empty string when provided`)
}
if (!Array.isArray(item.children)) {
throw new NavigationLayoutValidationError(`primary[${index}].children must be an array`)
}
const children = normalizeUniqueIds(item.children, `primary[${index}].children`)
for (const entryId of children) addUnique(visibleEntryIds, entryId, 'navigation entry')
return { kind: 'group', id, title: item.title, children }
return {
kind: 'group',
id,
...(typeof item.title === 'string' && { title: item.title }),
children,
}
}
throw new NavigationLayoutValidationError(`primary[${index}].kind must be "entry" or "group"`)
}
+2 -2
View File
@@ -21,8 +21,8 @@ export interface NavigationLayoutEntryItem {
export interface NavigationLayoutGroupItem {
kind: 'group'
id: string
/** User-authored content. Keep it as-is instead of translating it. */
title: string
/** User-authored override. Missing values use the host-localized group title. */
title?: string
/** Stable navigation entry IDs. Unknown IDs are intentionally preserved. */
children: string[]
}
+20
View File
@@ -23,6 +23,9 @@ import { configureHttpClient } from '@/services/utils/http'
import { IS_ELECTRON } from '@/utils/platform'
import { PLATFORM_CAPABILITIES } from '@/utils/platform-capabilities'
import { usePlatformService } from '@/services'
import { useNavigationLayoutService } from '@/services'
import { listResolvedNavigationEntries } from '@/navigation/layout'
import { useNavigationLayout } from '@/navigation/vue'
import type { PresentationPreferences } from '@/services/preferences/types'
import { resolvePageTransitionKey } from '@/routes/page-transition-key'
import { useLockScreenBootstrap } from '@/components/lock-screen/bootstrap'
@@ -55,6 +58,7 @@ const authStore = useAuthStore()
const apiServerStore = useApiServerStore()
const route = useRoute()
const router = useRouter()
const { controller: navigationLayoutController } = useNavigationLayout()
const { isBootstrapMaskVisible, isApplicationInteractive, markLockScreenReady, syncBootstrapMask, updateLockState } =
useLockScreenBootstrap(IS_ELECTRON)
@@ -130,6 +134,18 @@ function scheduleNonCriticalUiPrefetch() {
cancelNonCriticalUiPrefetch = () => window.clearTimeout(timer)
}
async function hydrateNavigationLayout(): Promise<void> {
const result = await useNavigationLayoutService().load()
navigationLayoutController.applyLoadResult(result)
const currentPageId = String(router.currentRoute.value.meta.insightPageId ?? '')
if (!currentPageId) return
const visibleEntries = listResolvedNavigationEntries(navigationLayoutController.getResolvedLayout())
if (visibleEntries.some(({ page }) => page.id === currentPageId)) return
const fallback = visibleEntries[0]
if (fallback) await router.replace({ name: fallback.page.routeName })
}
async function initializeApp() {
if (initInProgress || isRuntimeReady.value) return
initInProgress = true
@@ -161,6 +177,10 @@ async function initializeApp() {
deferAfterPresentationError: () =>
PLATFORM_CAPABILITIES.requiresAuth && authStore.requiresAuth && !authStore.isAuthenticated,
initializeBackground: [
{
name: 'navigation-layout',
run: hydrateNavigationLayout,
},
{
name: 'preferences',
run: async () => {
+3
View File
@@ -11,11 +11,13 @@ import { markStartupPhase } from '@/bootstrap/startup-performance'
import {
desktopCliWebInsightBuiltins,
desktopCliWebInsightRuntime,
desktopCliWebNavigationLayout,
desktopCliWebUiHost,
desktopCliWebUiServices,
} from '@/plugins/desktop-cli-web'
import { installInsightPluginRuntime } from '@/plugins/insight-vue'
import { installStaticInsightPluginUiServices } from '@/plugins/static-insight'
import { installNavigationLayout } from '@/navigation/vue'
import '@/assets/styles/main.css'
export interface MountChatLabAppOptions {
@@ -41,6 +43,7 @@ export async function mountChatLabApp(options: MountChatLabAppOptions = {}): Pro
app.use(router)
app.use(ui)
app.use(i18n)
installNavigationLayout(app, desktopCliWebNavigationLayout)
await installStaticInsightPluginUiServices(
desktopCliWebInsightBuiltins,
desktopCliWebInsightRuntime,
+33 -11
View File
@@ -22,6 +22,9 @@ import { CompactTabs } from '@/components/navigation'
import { useSessionStore } from '@/stores/session'
import { useLayoutStore } from '@/stores/layout'
import { usePlatformService } from '@/services'
import { useHostLocale } from '@/plugins/insight-vue'
import { DEFAULT_INSIGHT_NAVIGATION_GROUP_ID } from '@/navigation/layout'
import { useNavigationLayout } from '@/navigation/vue'
import { IS_ELECTRON } from '@/utils/platform'
import logoSvg from '@/assets/images/logo.svg'
@@ -42,6 +45,7 @@ const props = withDefaults(
)
const { t } = useI18n()
const { translate } = useHostLocale()
const LATEST_VERSION_URL = 'https://chatlab.fun/latest-version'
const UPDATE_CHECK_CACHE_KEY = 'chatlab:latest-version-check:v2'
const hasMacTitlebarInset =
@@ -54,6 +58,7 @@ const { isSidebarCollapsed: isCollapsed } = storeToRefs(layoutStore)
const { toggleSidebar } = layoutStore
const router = useRouter()
const route = useRoute()
const { controller: navigationLayoutController, snapshot: navigationLayoutSnapshot } = useNavigationLayout()
const SESSION_ROW_SIZE = 44
const SESSION_LIST_BOTTOM_PADDING = 32
@@ -68,10 +73,21 @@ const showContactsEntry = computed(() =>
groupSessionCount: groupSessionCount.value,
})
)
const showInsightEntry = computed(() => props.backendFeatures || props.insightEnabled)
const showInsightNavigation = computed(() => props.backendFeatures || props.insightEnabled)
const resolvedPrimaryNavigation = computed(() => {
void navigationLayoutSnapshot.value.revision
return navigationLayoutController.getResolvedLayout().primary
})
const activeInsightPageId = computed(() => String(route.meta.insightPageId ?? ''))
// 是否在足迹页
const isInsightPage = computed(() => String(route.name ?? '').startsWith('insight-'))
function navigationGroupTitle(groupId: string, customTitle?: string): string {
if (customTitle) return customTitle
return groupId === DEFAULT_INSIGHT_NAVIGATION_GROUP_ID ? t('layout.insight') : groupId
}
function openInsightRoute(routeName: string): void {
void router.push({ name: routeName })
}
// 重命名相关状态
const showRenameModal = ref(false)
@@ -510,14 +526,20 @@ function getAvatarColorClass(session: AnalysisSession, isActive: boolean) {
@click="handleImport"
/>
<!-- 足迹 -->
<SidebarButton
v-if="showInsightEntry"
icon="i-heroicons-presentation-chart-bar"
:title="t('layout.insight')"
:active="isInsightPage"
@click="router.push('/insight')"
/>
<template v-if="showInsightNavigation">
<SidebarButton
v-for="item in resolvedPrimaryNavigation"
:key="item.kind === 'entry' ? item.entryId : item.id"
:icon="item.kind === 'entry' ? item.page.icon : 'i-heroicons-presentation-chart-bar'"
:title="item.kind === 'entry' ? translate(item.page.title) : navigationGroupTitle(item.id, item.title)"
:active="
item.kind === 'entry'
? activeInsightPageId === item.page.id
: item.entries.some(({ page }) => page.id === activeInsightPageId)
"
@click="openInsightRoute(item.kind === 'entry' ? item.page.routeName : item.entries[0]!.page.routeName)"
/>
</template>
<SidebarButton
v-if="props.backendFeatures && showContactsEntry"
+118
View File
@@ -0,0 +1,118 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { NAVIGATION_LAYOUT_SCHEMA_VERSION, type NavigationLayout } from '@openchatlab/shared-types'
import { createInsightPluginRuntime, type InsightPlugin } from '@/plugins/insight'
import { InsightScopeController } from '@/plugins/insight-scope'
import { PluginLocaleHost } from '@/plugins/locale'
import { UiServiceRegistry, type UiHostContext } from '@/plugins/ui-host'
import { createDefaultNavigationLayout, NavigationLayoutController, resolveNavigationLayout } from './layout'
function createRuntime() {
const locale = new PluginLocaleHost({
getLocale: () => 'en-US',
subscribe: () => () => {},
translate: (key) => key,
})
const ui: UiHostContext = { locale, insightScope: new InsightScopeController(), services: new UiServiceRegistry() }
const plugin = (id: string, pageId: string, order: number): InsightPlugin => ({
id,
platforms: ['cli-web'],
activate(context) {
context.locale.register(`plugins.${id}`, {
'en-US': { title: pageId },
'zh-CN': { title: pageId },
'zh-TW': { title: pageId },
'ja-JP': { title: pageId },
})
context.pages.register({
id: pageId,
path: pageId,
routeName: `insight-${pageId}`,
title: { namespace: `plugins.${id}`, key: 'title' },
icon: `icon-${pageId}`,
view: { load: async () => ({}) },
})
context.navigation.register({ id: `entry.${pageId}`, pageId, order })
},
})
return createInsightPluginRuntime('cli-web', ui, locale, [
plugin('first', 'first', 10),
plugin('second', 'second', 20),
])
}
test('default layout keeps the current single Insight primary group and contribution order', () => {
const layout = createDefaultNavigationLayout(createRuntime())
assert.deepEqual(layout, {
schemaVersion: NAVIGATION_LAYOUT_SCHEMA_VERSION,
primary: [
{
kind: 'group',
id: 'host.insight',
children: ['entry.first', 'entry.second', 'insight.relationship-changes'],
},
],
hiddenEntryIds: [],
})
})
test('saved layout promotes entries, preserves custom groups, hides entries, and omits unavailable IDs', () => {
const runtime = createRuntime()
const layout: NavigationLayout = {
schemaVersion: NAVIGATION_LAYOUT_SCHEMA_VERSION,
primary: [
{ kind: 'entry', entryId: 'entry.second' },
{ kind: 'group', id: 'custom', title: 'My group', children: ['plugin.unavailable', 'entry.first'] },
],
hiddenEntryIds: ['insight.relationship-changes'],
}
assert.deepEqual(
resolveNavigationLayout(runtime, layout).primary.map((item) =>
item.kind === 'entry'
? { kind: item.kind, entryId: item.entryId }
: { kind: item.kind, id: item.id, title: item.title, entries: item.entries.map(({ entryId }) => entryId) }
),
[
{ kind: 'entry', entryId: 'entry.second' },
{ kind: 'group', id: 'custom', title: 'My group', entries: ['entry.first'] },
]
)
assert.deepEqual(layout.primary[1], {
kind: 'group',
id: 'custom',
title: 'My group',
children: ['plugin.unavailable', 'entry.first'],
})
})
test('new plugin entries are appended to the localized default group without mutating saved layout', () => {
const runtime = createRuntime()
const layout: NavigationLayout = {
schemaVersion: 1,
primary: [{ kind: 'entry', entryId: 'entry.first' }],
hiddenEntryIds: [],
}
const resolved = resolveNavigationLayout(runtime, layout)
assert.deepEqual(
resolved.primary.map((item) =>
item.kind === 'entry' ? item.entryId : { id: item.id, entries: item.entries.map(({ entryId }) => entryId) }
),
['entry.first', { id: 'host.insight', entries: ['entry.second', 'insight.relationship-changes'] }]
)
assert.deepEqual(layout.primary, [{ kind: 'entry', entryId: 'entry.first' }])
})
test('controller falls back to defaults for missing or invalid saved layouts', () => {
const controller = new NavigationLayoutController(createRuntime())
let notifications = 0
controller.subscribe(() => notifications++)
controller.applyLoadResult({ status: 'invalid', layout: null })
assert.equal(controller.getSnapshot().source, 'invalid')
assert.equal(controller.getResolvedLayout().primary.length, 1)
assert.equal(notifications, 1)
})
+145
View File
@@ -0,0 +1,145 @@
import {
NAVIGATION_LAYOUT_SCHEMA_VERSION,
type NavigationLayout,
type NavigationLayoutLoadResult,
} from '@openchatlab/shared-types'
import type { InsightPageDefinition, InsightPluginRuntime } from '@/plugins/insight'
import { listInsightShellNavigation } from '@/plugins/insight-catalog'
import type { Disposer } from '@/plugins/core'
export const DEFAULT_INSIGHT_NAVIGATION_GROUP_ID = 'host.insight'
export interface ResolvedNavigationEntry {
kind: 'entry'
entryId: string
page: InsightPageDefinition
}
export interface ResolvedNavigationGroup {
kind: 'group'
id: string
title?: string
entries: ResolvedNavigationEntry[]
}
export type ResolvedPrimaryNavigationItem = ResolvedNavigationEntry | ResolvedNavigationGroup
export interface ResolvedNavigationLayout {
primary: ResolvedPrimaryNavigationItem[]
hiddenEntryIds: string[]
}
export function listResolvedNavigationEntries(layout: ResolvedNavigationLayout): ResolvedNavigationEntry[] {
return layout.primary.flatMap((item) => (item.kind === 'entry' ? [item] : item.entries))
}
export interface NavigationLayoutSnapshot {
layout: NavigationLayout
source: NavigationLayoutLoadResult['status'] | 'default'
revision: number
}
export function createDefaultNavigationLayout(runtime: InsightPluginRuntime): NavigationLayout {
return {
schemaVersion: NAVIGATION_LAYOUT_SCHEMA_VERSION,
primary: [
{
kind: 'group',
id: DEFAULT_INSIGHT_NAVIGATION_GROUP_ID,
children: listInsightShellNavigation(runtime).map(({ entryId }) => entryId),
},
],
hiddenEntryIds: [],
}
}
export function resolveNavigationLayout(
runtime: InsightPluginRuntime,
layout: NavigationLayout
): ResolvedNavigationLayout {
const catalog = listInsightShellNavigation(runtime)
const availableEntries = new Map(
catalog.map(({ entryId, page }) => [entryId, { kind: 'entry' as const, entryId, page }])
)
const hiddenEntryIds = new Set(layout.hiddenEntryIds)
const positionedEntryIds = new Set<string>()
const primary: ResolvedPrimaryNavigationItem[] = []
for (const item of layout.primary) {
if (item.kind === 'entry') {
positionedEntryIds.add(item.entryId)
const entry = availableEntries.get(item.entryId)
if (entry && !hiddenEntryIds.has(item.entryId)) primary.push(entry)
continue
}
const entries: ResolvedNavigationEntry[] = []
for (const entryId of item.children) {
positionedEntryIds.add(entryId)
const entry = availableEntries.get(entryId)
if (entry && !hiddenEntryIds.has(entryId)) entries.push(entry)
}
if (entries.length > 0) primary.push({ kind: 'group', id: item.id, title: item.title, entries })
}
const newEntries = catalog
.filter(({ entryId }) => !positionedEntryIds.has(entryId) && !hiddenEntryIds.has(entryId))
.map(({ entryId }) => availableEntries.get(entryId)!)
if (newEntries.length > 0) {
const defaultGroup = primary.find(
(item): item is ResolvedNavigationGroup =>
item.kind === 'group' && item.id === DEFAULT_INSIGHT_NAVIGATION_GROUP_ID
)
if (defaultGroup) defaultGroup.entries.push(...newEntries)
else primary.push({ kind: 'group', id: DEFAULT_INSIGHT_NAVIGATION_GROUP_ID, entries: newEntries })
}
return { primary, hiddenEntryIds: [...layout.hiddenEntryIds] }
}
export class NavigationLayoutController {
private layout: NavigationLayout
private source: NavigationLayoutSnapshot['source'] = 'default'
private revision = 0
private readonly listeners = new Set<() => void>()
constructor(readonly runtime: InsightPluginRuntime) {
this.layout = createDefaultNavigationLayout(runtime)
}
getSnapshot(): NavigationLayoutSnapshot {
return { layout: this.layout, source: this.source, revision: this.revision }
}
getResolvedLayout(): ResolvedNavigationLayout {
return resolveNavigationLayout(this.runtime, this.layout)
}
applyLoadResult(result: NavigationLayoutLoadResult): void {
this.layout = result.layout ?? createDefaultNavigationLayout(this.runtime)
this.source = result.status
this.emit()
}
applySavedLayout(layout: NavigationLayout): void {
this.layout = layout
this.source = 'saved'
this.emit()
}
applyDefaultLayout(): void {
this.layout = createDefaultNavigationLayout(this.runtime)
this.source = 'default'
this.emit()
}
subscribe(listener: () => void): Disposer {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
private emit(): void {
this.revision++
for (const listener of this.listeners) listener()
}
}
+22
View File
@@ -0,0 +1,22 @@
import { inject, onBeforeUnmount, shallowRef, type App, type InjectionKey, type ShallowRef } from 'vue'
import type { NavigationLayoutController, NavigationLayoutSnapshot } from './layout'
const NAVIGATION_LAYOUT_KEY: InjectionKey<NavigationLayoutController> = Symbol('NavigationLayoutController')
export function installNavigationLayout(app: App, controller: NavigationLayoutController): void {
app.provide(NAVIGATION_LAYOUT_KEY, controller)
}
export function useNavigationLayout(): {
controller: NavigationLayoutController
snapshot: Readonly<ShallowRef<NavigationLayoutSnapshot>>
} {
const controller = inject(NAVIGATION_LAYOUT_KEY)
if (!controller) throw new Error('Navigation layout controller is unavailable')
const snapshot = shallowRef(controller.getSnapshot())
const unsubscribe = controller.subscribe(() => {
snapshot.value = controller.getSnapshot()
})
onBeforeUnmount(unsubscribe)
return { controller, snapshot }
}
+23 -1
View File
@@ -8,12 +8,14 @@ import { PageTabs } from '@/components/navigation'
import { listInsightShellPages } from '@/plugins/insight-catalog'
import { InsightScopeController } from '@/plugins/insight-scope'
import { useHostLocale, useInsightPluginRuntime } from '@/plugins/insight-vue'
import { useNavigationLayout } from '@/navigation/vue'
import { createInsightTimeRange, getInsightTimeFilterSignature } from './insight-time-range'
const { t } = useI18n()
const { translate } = useHostLocale()
const route = useRoute()
const pluginRuntime = useInsightPluginRuntime()
const { controller: navigationLayoutController, snapshot: navigationLayoutSnapshot } = useNavigationLayout()
const pages = listInsightShellPages(pluginRuntime)
const activeSubpage = computed(() => String(route.meta.insightPageId ?? pages[0]?.id ?? ''))
const activePage = computed(() => pages.find((page) => page.id === activeSubpage.value))
@@ -54,8 +56,17 @@ const stopSyncingScope = watch(
},
{ immediate: true }
)
const resolvedNavigation = computed(() => {
void navigationLayoutSnapshot.value.revision
return navigationLayoutController.getResolvedLayout()
})
const activeNavigationGroup = computed(() =>
resolvedNavigation.value.primary.find(
(item) => item.kind === 'group' && item.entries.some(({ page }) => page.id === activeSubpage.value)
)
)
const navigationItems = computed(() =>
pages.map((page) => ({
(activeNavigationGroup.value?.kind === 'group' ? activeNavigationGroup.value.entries : []).map(({ page }) => ({
id: page.id,
label: translate(page.title),
icon: page.icon,
@@ -89,6 +100,7 @@ onBeforeUnmount(() => {
size="compact"
>
<PageTabs
v-if="navigationItems.length > 0"
class="mt-3 pb-1.5"
:model-value="activeSubpage"
:items="navigationItems"
@@ -105,6 +117,16 @@ onBeforeUnmount(() => {
/>
</template>
</PageTabs>
<div v-else-if="timeFilter" class="mt-3 flex justify-end pb-1.5">
<TimeSelect
:key="`${componentKey}:${timeFilterSignature}`"
v-model="modelValue"
:range-source="rangeSource"
:allowed-modes="allowedTimeModes"
:allowed-recent-days="allowedRecentDays"
:initial-state="initialState"
/>
</div>
</PageHeader>
<RouterView v-slot="{ Component }">
+2
View File
@@ -5,6 +5,7 @@ import { getLegacyInsightPages } from './insight-catalog'
import { createStaticInsightPluginRuntime } from './static-insight'
import { UiServiceRegistry } from './ui-host'
import { createVueUiHostContext } from './vue-ui-host'
import { NavigationLayoutController } from '@/navigation/layout'
const platform = PLATFORM_CAPABILITIES.platform
export const desktopCliWebInsightBuiltins = [annualSummaryBuiltin, timeInvestmentBuiltin] as const
@@ -17,3 +18,4 @@ export const desktopCliWebInsightRuntime = createStaticInsightPluginRuntime(
desktopCliWebInsightBuiltins,
getLegacyInsightPages(platform)
)
export const desktopCliWebNavigationLayout = new NavigationLayoutController(desktopCliWebInsightRuntime)
+27 -8
View File
@@ -2,6 +2,7 @@ import type { RuntimePlatform } from '@/utils/platform-capabilities'
import type { InsightPageDefinition, InsightPluginRuntime } from './insight'
export interface LegacyInsightPage extends InsightPageDefinition {
navigationId: string
platforms: readonly RuntimePlatform[]
order: number
}
@@ -9,6 +10,7 @@ export interface LegacyInsightPage extends InsightPageDefinition {
export const LEGACY_INSIGHT_PAGES: readonly LegacyInsightPage[] = [
{
id: 'relationship-changes',
navigationId: 'insight.relationship-changes',
path: 'relationship-changes',
routeName: 'insight-relationship-changes',
title: { key: 'insight.tabs.relationshipChanges' },
@@ -22,14 +24,7 @@ export function listInsightShellPages(
runtime: InsightPluginRuntime,
legacyPages: readonly LegacyInsightPage[] = LEGACY_INSIGHT_PAGES
): InsightPageDefinition[] {
const pluginPages = runtime.listNavigation().map(({ entry, page }) => ({ page, order: entry.order }))
const availableLegacyPages = legacyPages
.filter((page) => page.platforms.includes(runtime.platform))
.map((page) => ({ page, order: page.order }))
const pages = [...pluginPages, ...availableLegacyPages]
.sort((left, right) => left.order - right.order || left.page.id.localeCompare(right.page.id))
.map(({ page }) => page)
const pages = listInsightShellNavigation(runtime, legacyPages).map(({ page }) => page)
const pageIds = new Set<string>()
for (const page of pages) {
@@ -39,6 +34,30 @@ export function listInsightShellPages(
return pages
}
export interface InsightShellNavigationItem {
entryId: string
order: number
page: InsightPageDefinition
}
export function listInsightShellNavigation(
runtime: InsightPluginRuntime,
legacyPages: readonly LegacyInsightPage[] = LEGACY_INSIGHT_PAGES
): InsightShellNavigationItem[] {
const pluginItems = runtime.listNavigation().map(({ entry, page }) => ({
entryId: entry.id,
order: entry.order,
page,
}))
const legacyItems = legacyPages
.filter((page) => page.platforms.includes(runtime.platform))
.map((page) => ({ entryId: page.navigationId, order: page.order, page }))
return [...pluginItems, ...legacyItems].sort(
(left, right) => left.order - right.order || left.entryId.localeCompare(right.entryId)
)
}
export function getLegacyInsightPages(platform: RuntimePlatform): InsightPageDefinition[] {
return LEGACY_INSIGHT_PAGES.filter((page) => page.platforms.includes(platform))
}
+2
View File
@@ -3,6 +3,7 @@ import { getLegacyInsightPages } from './insight-catalog'
import { createStaticInsightPluginRuntime } from './static-insight'
import { UiServiceRegistry } from './ui-host'
import { createVueUiHostContext } from './vue-ui-host'
import { NavigationLayoutController } from '@/navigation/layout'
export const webWasmInsightBuiltins = [timeInvestmentBuiltin] as const
export const webWasmUiServices = new UiServiceRegistry()
@@ -14,3 +15,4 @@ export const webWasmInsightRuntime = createStaticInsightPluginRuntime(
webWasmInsightBuiltins,
getLegacyInsightPages('web-wasm')
)
export const webWasmNavigationLayout = new NavigationLayoutController(webWasmInsightRuntime)