Add governed View surface runtime

This commit is contained in:
2026-07-28 21:04:54 +02:00
parent 13bc3d3b4e
commit ce9ef8d88f
24 changed files with 1215 additions and 39 deletions
+88 -9
View File
@@ -1,6 +1,6 @@
import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, Form, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
import installedWebModules from "virtual:govoplan-installed-modules";
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformWebModule } from "../types";
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
import {
hasUiCapability as hasUiCapabilityForModules,
moduleInstalled as moduleInstalledForModules,
@@ -11,6 +11,12 @@ import {
uiCapability as uiCapabilityForModules } from
"./moduleLogic";
import { hasAnyScope, hasScope } from "../utils/permissions";
import {
isViewSurfaceVisible,
navigationViewSurfaceId,
routeViewSurfaceId,
viewSurfaceCatalogueForModules
} from "./views";
export const fallbackDashboardNavItem: PlatformNavItem =
{ to: "/dashboard", label: "i18n:govoplan-core.dashboard.d87f47b4", iconName: "dashboard", order: 10 };
@@ -86,10 +92,65 @@ function navFromMetadata(item: PlatformModuleInfo["nav"][number]): PlatformNavIt
iconName: item.icon ?? null,
allOf: item.required_all,
anyOf: item.required_any,
order: item.order
order: item.order,
surfaceId: item.surface_id ?? undefined
};
}
function viewSurfaceFromMetadata(
item: NonNullable<NonNullable<PlatformModuleInfo["frontend"]>["view_surfaces"]>[number]
): PlatformViewSurface {
return {
id: item.id,
moduleId: item.module_id,
kind: item.kind,
label: item.label,
parentId: item.parent_id,
description: item.description,
order: item.order,
defaultVisible: item.default_visible,
required: item.required
};
}
function mergeViewSurfaces(
module: PlatformWebModule,
info: PlatformModuleInfo
): PlatformViewSurface[] | undefined {
const surfaces = new Map<string, PlatformViewSurface>();
for (const surface of info.frontend?.view_surfaces ?? []) {
const normalized = viewSurfaceFromMetadata(surface);
surfaces.set(normalized.id, normalized);
}
for (const surface of module.viewSurfaces ?? []) {
surfaces.set(surface.id, {
...surfaces.get(surface.id),
...surface,
required: Boolean(surfaces.get(surface.id)?.required || surface.required)
});
}
return surfaces.size ? [...surfaces.values()] : undefined;
}
function routesWithServerMetadata(
module: PlatformWebModule,
info: PlatformModuleInfo
) {
const routesByPath = new Map(
(info.frontend?.routes ?? []).map((route) => [route.path, route])
);
return (module.routes ?? []).map((route) => {
const metadata = routesByPath.get(route.path);
return {
...route,
surfaceId:
metadata?.surface_id ??
route.surfaceId ??
routeViewSurfaceId(module.id, route.path)
};
});
}
function runtimeUiCapabilitiesForModule(module: PlatformWebModule, info: PlatformModuleInfo) {
const enabledNames = new Set(info.runtime_ui_capabilities ?? []);
@@ -113,7 +174,9 @@ function applyServerMetadata(module: PlatformWebModule, info: PlatformModuleInfo
remoteAssetIntegrity: info.frontend?.asset_manifest_integrity ?? module.remoteAssetIntegrity,
remoteAssetContractVersion: info.frontend?.asset_manifest_contract_version ?? module.remoteAssetContractVersion,
navItems: backendNav.length ? backendNav.map(navFromMetadata) : module.navItems,
routes: routesWithServerMetadata(module, info),
publicRoutes: filterPublicRoutes(module, info.frontend?.public_routes),
viewSurfaces: mergeViewSurfaces(module, info),
uiCapabilities: {
...(module.uiCapabilities ?? {}),
...runtimeUiCapabilitiesForModule(module, info)
@@ -353,26 +416,42 @@ export function publicRouteContributionsForModules(modules: PlatformWebModule[])
return publicRouteContributionsForModuleList(modules);
}
export function dashboardWidgetsForModules(modules: PlatformWebModule[] = localModules): DashboardWidgetContribution[] {
export function dashboardWidgetsForModules(
modules: PlatformWebModule[] = localModules,
projection?: EffectiveViewProjection | null
): DashboardWidgetContribution[] {
const catalogue = viewSurfaceCatalogueForModules(modules);
return uiCapabilitiesForModules<DashboardWidgetsUiCapability>("dashboard.widgets", modules).
flatMap((capability) => capability.widgets ?? []).
filter((widget) => isViewSurfaceVisible(projection, widget.surfaceId, catalogue)).
sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}
export function navItemsForModules(modules: PlatformWebModule[]): PlatformNavItem[] {
return [...shellNavItemsForModules(modules), ...modules.flatMap((module) => module.navItems ?? [])].
export function navItemsForModules(
modules: PlatformWebModule[],
projection?: EffectiveViewProjection | null
): PlatformNavItem[] {
const catalogue = viewSurfaceCatalogueForModules(modules);
const moduleItems = modules.flatMap((module) =>
(module.navItems ?? []).map((item) => ({
...item,
surfaceId: item.surfaceId ?? navigationViewSurfaceId(module.id, item.to)
}))
);
return [...shellNavItemsForModules(modules), ...moduleItems].
map(resolveNavItemIcon).
filter((item) => isViewSurfaceVisible(projection, item.surfaceId, catalogue)).
sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}
export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = localModules): PlatformNavItem[] {
return navItemsForModules(modules).filter((item) => {
export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = localModules, projection?: EffectiveViewProjection | null): PlatformNavItem[] {
return navItemsForModules(modules, projection).filter((item) => {
if (item.allOf?.length && !item.allOf.every((scope) => hasScope(auth, scope))) return false;
if (item.anyOf?.length && !hasAnyScope(auth, item.anyOf)) return false;
return true;
});
}
export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = localModules): string {
return visibleNavItems(auth, modules)[0]?.to ?? "/dashboard";
export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = localModules, projection?: EffectiveViewProjection | null): string {
return visibleNavItems(auth, modules, projection)[0]?.to ?? "/dashboard";
}