Add product presentation extension contracts

This commit is contained in:
2026-08-06 19:02:53 +02:00
parent d65d7a8e5f
commit 32c234fbdb
19 changed files with 772 additions and 29 deletions
+33 -1
View File
@@ -1,6 +1,6 @@
import { Activity, Bell, BookUser, BriefcaseBusiness, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, FolderKanban, Form, Gavel, Inbox, Landmark, LayoutDashboard, LayoutTemplate, ListChecks, ListTree, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Vote, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
import installedWebModuleLoaderSource from "virtual:govoplan-installed-modules";
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule, ProductAreaContribution, QuickAccessToolMetadata } from "../types";
import {
hasUiCapability as hasUiCapabilityForModules,
moduleInstalled as moduleInstalledForModules,
@@ -168,6 +168,36 @@ function routesWithServerMetadata(
});
}
function productAreasFromMetadata(info: PlatformModuleInfo): ProductAreaContribution[] {
return (info.frontend?.product_areas ?? []).map((area) => ({
id: area.id,
moduleId: area.module_id,
label: area.label,
description: area.description,
iconName: area.icon,
surfaceIds: area.surface_ids,
order: area.order
}));
}
function quickAccessToolsFromMetadata(info: PlatformModuleInfo): QuickAccessToolMetadata[] {
return (info.frontend?.quick_access_tools ?? []).map((tool) => ({
id: tool.id,
moduleId: tool.module_id,
categoryId: tool.category_id,
label: tool.label,
description: tool.description,
surfaceId: tool.surface_id,
iconName: tool.icon,
fullPagePath: tool.full_page_path,
allOf: tool.required_all,
anyOf: tool.required_any,
order: tool.order,
defaultEnabled: tool.default_enabled,
modes: tool.modes
}));
}
function runtimeUiCapabilitiesForModule(module: PlatformWebModule, info: PlatformModuleInfo) {
const enabledNames = new Set(info.runtime_ui_capabilities ?? []);
@@ -194,6 +224,8 @@ function applyServerMetadata(module: PlatformWebModule, info: PlatformModuleInfo
routes: routesWithServerMetadata(module, info),
publicRoutes: filterPublicRoutes(module, info.frontend?.public_routes),
viewSurfaces: mergeViewSurfaces(module, info),
productAreas: productAreasFromMetadata(info),
quickAccessTools: quickAccessToolsFromMetadata(info),
helpContexts: info.help_contexts ?? module.helpContexts,
uiCapabilities: {
...(module.uiCapabilities ?? {}),
+91
View File
@@ -0,0 +1,91 @@
import type {
PlatformNavItem,
ProductAreaContribution,
ViewPresentation
} from "../types";
export type NavigationGroup = {
id: string;
label?: string;
areaLabel?: string;
items: PlatformNavItem[];
};
export function groupNavigationItems(
items: PlatformNavItem[],
contributions: ProductAreaContribution[],
presentation?: ViewPresentation
): NavigationGroup[] {
if (presentation?.navigationMode === "flat" || contributions.length === 0) {
return [{ id: "all-tools", items }];
}
const areaById = new Map<
string,
{
id: string;
label: string;
order: number;
surfaceIds: Set<string>;
}
>();
for (const contribution of contributions) {
const existing = areaById.get(contribution.id);
if (existing) {
contribution.surfaceIds.forEach((id) => existing.surfaceIds.add(id));
existing.order = Math.min(existing.order, contribution.order ?? 100);
continue;
}
areaById.set(contribution.id, {
id: contribution.id,
label:
presentation?.productAreaLabels?.[contribution.id] ??
contribution.label,
order: contribution.order ?? 100,
surfaceIds: new Set(contribution.surfaceIds)
});
}
const configuredOrder = new Map(
(presentation?.productAreaOrder ?? []).map((id, index) => [id, index])
);
const areas = [...areaById.values()].sort(
(left, right) =>
(configuredOrder.get(left.id) ?? 10_000) -
(configuredOrder.get(right.id) ?? 10_000) ||
left.order - right.order ||
left.label.localeCompare(right.label)
);
const assigned = new Set<string>();
const groups: NavigationGroup[] = [];
const pinned = items.filter((item) => item.to === "/dashboard");
pinned.forEach((item) => assigned.add(item.to));
if (pinned.length) groups.push({ id: "overview", items: pinned });
for (const area of areas) {
const areaItems = items.filter(
(item) =>
!assigned.has(item.to) &&
Boolean(item.surfaceId && area.surfaceIds.has(item.surfaceId))
);
if (!areaItems.length) continue;
areaItems.forEach((item) => assigned.add(item.to));
groups.push({
id: `product-area:${area.id}`,
label: area.label,
areaLabel: area.label,
items: areaItems
});
}
const remaining = items.filter((item) => !assigned.has(item.to));
if (remaining.length) {
groups.push({
id: "more-tools",
label: "i18n:govoplan-core.more_tools",
items: remaining
});
}
return groups;
}