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
+21
View File
@@ -36,6 +36,7 @@
"@govoplan/portal-webui": "file:../../govoplan-portal/webui",
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
"@govoplan/projects-webui": "file:../../govoplan-projects/webui",
"@govoplan/quick-access-webui": "file:../../govoplan-quick-access/webui",
"@govoplan/records-webui": "file:../../govoplan-records/webui",
"@govoplan/reporting-webui": "file:../../govoplan-reporting/webui",
"@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
@@ -557,6 +558,22 @@
}
}
},
"../../govoplan-quick-access/webui": {
"name": "@govoplan/quick-access-webui",
"version": "0.1.18",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
}
},
"../../govoplan-records/webui": {
"name": "@govoplan/records-webui",
"version": "0.1.18",
@@ -1601,6 +1618,10 @@
"resolved": "../../govoplan-projects/webui",
"link": true
},
"node_modules/@govoplan/quick-access-webui": {
"resolved": "../../govoplan-quick-access/webui",
"link": true
},
"node_modules/@govoplan/records-webui": {
"resolved": "../../govoplan-records/webui",
"link": true
+1
View File
@@ -80,6 +80,7 @@
"@govoplan/portal-webui": "file:../../govoplan-portal/webui",
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
"@govoplan/projects-webui": "file:../../govoplan-projects/webui",
"@govoplan/quick-access-webui": "file:../../govoplan-quick-access/webui",
"@govoplan/reporting-webui": "file:../../govoplan-reporting/webui",
"@govoplan/records-webui": "file:../../govoplan-records/webui",
"@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
+4 -1
View File
@@ -30,6 +30,7 @@ const packageByModule = {
portal: "@govoplan/portal-webui",
postbox: "@govoplan/postbox-webui",
projects: "@govoplan/projects-webui",
quick_access: "@govoplan/quick-access-webui",
reporting: "@govoplan/reporting-webui",
records: "@govoplan/records-webui",
risk_compliance: "@govoplan/risk-compliance-webui",
@@ -81,6 +82,8 @@ const cases = [
{ name: "postbox-only", modules: ["postbox"] },
{ name: "portal-only", modules: ["portal"] },
{ name: "projects-only", modules: ["projects"] },
{ name: "quick-access-only", modules: ["quick_access"] },
{ name: "quick-access-with-providers", modules: ["quick_access", "tasks", "calendar", "mail", "postbox", "files"] },
{ name: "reporting-only", modules: ["reporting"] },
{ name: "records-only", modules: ["access", "records"] },
{ name: "records-with-sources", modules: ["access", "cases", "files", "records"] },
@@ -97,7 +100,7 @@ const cases = [
{ name: "tasks-only", modules: ["access", "tasks"] },
{ name: "tasks-with-contributors", modules: ["access", "approvals", "postbox", "workflow", "dashboard", "tasks"] },
{ name: "voting-only", modules: ["access", "voting"] },
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity_trust", "encryption", "cases", "committee", "campaigns", "files", "forms", "forms_runtime", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "portal", "postbox", "projects", "reporting", "records", "risk_compliance", "search", "tasks", "voting"] }
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity_trust", "encryption", "cases", "committee", "campaigns", "files", "forms", "forms_runtime", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "voting"] }
];
const npmExec = process.env.npm_execpath;
+18
View File
@@ -76,6 +76,15 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.append_successfully_sent_messages_to_sent.002fd67e": "Append successfully sent messages to Sent",
"i18n:govoplan-core.append_target_folder.0aaacc0c": "Append target folder",
"i18n:govoplan-core.application_notices": "Application notices",
"i18n:govoplan-core.more_tools": "More tools",
"i18n:govoplan-core.product_area.work": "Work",
"i18n:govoplan-core.product_area.communication": "Communication",
"i18n:govoplan-core.product_area.records_documents": "Records and documents",
"i18n:govoplan-core.product_area.meetings_decisions": "Meetings and decisions",
"i18n:govoplan-core.product_area.work_description": "Tasks, approvals, handoffs, and other work requiring attention.",
"i18n:govoplan-core.product_area.communication_description": "Messages, campaigns, notifications, and institutional communication.",
"i18n:govoplan-core.product_area.records_documents_description": "Documents, files, evidence, templates, and records.",
"i18n:govoplan-core.product_area.meetings_decisions_description": "Scheduling, meetings, participation, and institutional decisions.",
"i18n:govoplan-core.application.b291beb8": "Application",
"i18n:govoplan-core.attachment_base_path.66e9940b": "Attachment base path",
"i18n:govoplan-core.attachment_value.01801a54": "Attachment {value0}",
@@ -734,6 +743,15 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.append_successfully_sent_messages_to_sent.002fd67e": "Append successfully sent messages to Sent",
"i18n:govoplan-core.append_target_folder.0aaacc0c": "Append target folder",
"i18n:govoplan-core.application_notices": "Anwendungshinweise",
"i18n:govoplan-core.more_tools": "Weitere Werkzeuge",
"i18n:govoplan-core.product_area.work": "Arbeit",
"i18n:govoplan-core.product_area.communication": "Kommunikation",
"i18n:govoplan-core.product_area.records_documents": "Akten und Dokumente",
"i18n:govoplan-core.product_area.meetings_decisions": "Termine und Entscheidungen",
"i18n:govoplan-core.product_area.work_description": "Aufgaben, Freigaben, Übergaben und andere zu bearbeitende Arbeit.",
"i18n:govoplan-core.product_area.communication_description": "Nachrichten, Kampagnen, Benachrichtigungen und institutionelle Kommunikation.",
"i18n:govoplan-core.product_area.records_documents_description": "Akten, Dokumente, Nachweise, Vorlagen und Dateien.",
"i18n:govoplan-core.product_area.meetings_decisions_description": "Terminplanung, Sitzungen, Beteiligung und institutionelle Entscheidungen.",
"i18n:govoplan-core.application.b291beb8": "Application",
"i18n:govoplan-core.attachment_base_path.66e9940b": "Attachment base path",
"i18n:govoplan-core.attachment_value.01801a54": "Attachment {value0}",
+32 -2
View File
@@ -1,8 +1,13 @@
import { useLocation } from "react-router";
import type { ApiSettings, AuthInfo, AuthUpdate, PlatformNavItem } from "../types";
import { useMemo } from "react";
import type { ApiSettings, AuthInfo, AuthUpdate, PlatformNavItem, ProductAreaContribution, QuickAccessRuntimeUiCapability } from "../types";
import IconRail from "./IconRail";
import Titlebar from "./Titlebar";
import BreadcrumbBar from "./BreadcrumbBar";
import { usePlatformModules, usePlatformUiCapability } from "../platform/ModuleContext";
import { useEffectiveView, useViewSurfaces } from "../platform/ViewContext";
import { isViewSurfaceVisible } from "../platform/views";
import { hasAnyScope, hasScope } from "../utils/permissions";
type Props = {
children: React.ReactNode;
@@ -28,6 +33,23 @@ export default function AppShell({
backendReachable = true
}: Props) {
const location = useLocation();
const modules = usePlatformModules();
const quickAccess = usePlatformUiCapability<QuickAccessRuntimeUiCapability>("quickAccess.runtime");
const projection = useEffectiveView();
const surfaces = useViewSurfaces();
const quickAccessTools = useMemo(
() => modules.flatMap((module) => module.quickAccessTools ?? []).filter((tool) => {
if (tool.allOf?.length && !tool.allOf.every((scope) => hasScope(auth, scope))) return false;
if (tool.anyOf?.length && !hasAnyScope(auth, tool.anyOf)) return false;
return isViewSurfaceVisible(projection, tool.surfaceId, surfaces);
}).sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
[auth, modules, projection, surfaces]
);
const QuickAccessRail = quickAccess?.Rail;
const productAreas = useMemo<ProductAreaContribution[]>(
() => modules.flatMap((module) => module.productAreas ?? []),
[modules]
);
if (publicMode) {
return (
@@ -43,12 +65,20 @@ export default function AppShell({
return (
<div className="app-shell">
<IconRail auth={auth} navItems={navItems} />
<IconRail
auth={auth}
navItems={navItems}
productAreas={productAreas}
presentation={projection?.presentation}
/>
<div className="app-main">
<Titlebar settings={settings} auth={auth} onSettingsChange={onSettingsChange} onAuthChange={onAuthChange} maintenanceMode={maintenanceMode} backendReachable={backendReachable} />
<BreadcrumbBar pathname={location.pathname} />
<main className="app-content">{children}</main>
</div>
{QuickAccessRail && auth && (
<QuickAccessRail settings={settings} auth={auth} tools={quickAccessTools} />
)}
</div>
);
}
+53 -20
View File
@@ -1,10 +1,16 @@
import { PanelLeftClose, PanelLeftOpen, Settings } from "lucide-react";
import { NavLink, useLocation } from "react-router";
import { useEffect, useMemo, useState, type MouseEvent } from "react";
import type { AuthInfo, PlatformNavItem } from "../types";
import type {
AuthInfo,
PlatformNavItem,
ProductAreaContribution,
ViewPresentation
} from "../types";
import { hasAnyScope, hasScope } from "../utils/permissions";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import { useGuardedNavigate } from "../components/UnsavedChangesGuard";
import { groupNavigationItems } from "../platform/productAreas";
const MODULE_NAV_STORAGE_KEY = "govoplan.lastModuleNav";
const RAIL_EXPANDED_STORAGE_KEY = "govoplan.iconRailExpanded";
@@ -22,8 +28,16 @@ function visibleNavItems(auth: AuthInfo | null | undefined, navItems: PlatformNa
export default function IconRail({
compact = false,
auth = null,
navItems = []
}: {compact?: boolean;auth?: AuthInfo | null;navItems?: PlatformNavItem[];}) {
navItems = [],
productAreas = [],
presentation
}: {
compact?: boolean;
auth?: AuthInfo | null;
navItems?: PlatformNavItem[];
productAreas?: ProductAreaContribution[];
presentation?: ViewPresentation;
}) {
const location = useLocation();
const items = visibleNavItems(auth, navItems);
const [rememberedTargets, setRememberedTargets] = useState<Record<string, string>>(() => loadRememberedTargets());
@@ -31,6 +45,10 @@ export default function IconRail({
const topLevelItems = useMemo(() => items.map((item) => item.to), [items]);
const { translateText } = usePlatformLanguage();
const navigate = useGuardedNavigate();
const navigationGroups = useMemo(
() => groupNavigationItems(items, productAreas, presentation),
[items, presentation, productAreas]
);
useEffect(() => {
const currentRoot = topLevelItems.find((root) => modulePathActive(location.pathname, root));
@@ -70,23 +88,38 @@ export default function IconRail({
<>
<div className="icon-rail-scroll">
<nav className="icon-nav">
{items.map(({ to, label, icon: Icon }) => {
const target = rememberedTargets[to] ?? to;
const active = modulePathActive(location.pathname, to);
const renderedLabel = translateText(label);
return (
<NavLink
key={to}
to={target}
className={`icon-nav-item ${active ? "active" : ""}`}
title={renderedLabel}
onClick={(event) => handleNavClick(event, target)}
>
{Icon ? <Icon size={20} /> : <span className="icon-nav-fallback">{renderedLabel.slice(0, 1)}</span>}
<span className="icon-nav-label">{renderedLabel}</span>
</NavLink>
);
})}
{navigationGroups.map((group) => (
<div className="icon-nav-group" key={group.id}>
{group.label && (
<div className="icon-nav-group-label">
{translateText(group.label)}
</div>
)}
{group.items.map(({ to, label, icon: Icon }) => {
const target = rememberedTargets[to] ?? to;
const active = modulePathActive(location.pathname, to);
const renderedLabel = translateText(label);
const areaLabel = group.areaLabel
? translateText(group.areaLabel)
: null;
const title = areaLabel
? `${areaLabel}: ${renderedLabel}`
: renderedLabel;
return (
<NavLink
key={to}
to={target}
className={`icon-nav-item ${active ? "active" : ""}`}
title={title}
onClick={(event) => handleNavClick(event, target)}
>
{Icon ? <Icon size={20} /> : <span className="icon-nav-fallback">{renderedLabel.slice(0, 1)}</span>}
<span className="icon-nav-label">{renderedLabel}</span>
</NavLink>
);
})}
</div>
))}
</nav>
</div>
<div className="icon-rail-bottom">
+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;
}
+5 -1
View File
@@ -1,4 +1,4 @@
.app-shell { height: 100vh; min-height: 0; display: grid; grid-template-columns: auto 1fr; overflow: hidden; }
.app-shell { height: 100vh; min-height: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; overflow: hidden; }
.icon-rail { width: 58px; background: var(--rail-bg); color: var(--rail-text); display: flex; flex-direction: column; align-items: center; height: 100vh; min-height: 0; box-shadow: var(--shadow-rail); z-index: 1000; transition: width .16s ease; }
.icon-rail.expanded { width: 208px; align-items: stretch; }
.icon-rail-header { width: 100%; min-height: 63px; display: flex; flex: 0 0 auto; align-items: center; justify-content: center; padding: 0 10px; box-sizing: border-box; }
@@ -18,6 +18,10 @@
.icon-rail-toggle:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
.icon-rail-scroll { width: 100%; min-width: 0; min-height: 0; flex: 1 1 auto; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-color: var(--rail-text-muted) transparent; scrollbar-width: thin; }
.icon-nav { width: 100%; display: flex; flex-direction: column; min-width: 0; }
.icon-nav-group { min-width: 0; }
.icon-nav-group-label { display: none; min-height: 26px; align-items: end; padding: 6px 14px 4px 16px; color: var(--rail-text-muted); font-size: 11px; font-weight: 700; letter-spacing: 0; }
.icon-rail.expanded .icon-nav-group-label { display: flex; }
.icon-rail.expanded .icon-nav-group + .icon-nav-group .icon-nav-group-label { border-top: 1px solid var(--rail-bg-active); }
.icon-nav-item { width: 100%; height: 52px; display: grid; grid-template-columns: 55px minmax(0, 1fr); align-items: center; color: var(--rail-text-muted); border-left: 3px solid transparent; text-decoration: none; box-sizing: border-box; }
.icon-nav-item svg,
.icon-nav-item > :first-child:not(.icon-nav-label) { justify-self: center; }
+85 -1
View File
@@ -244,7 +244,9 @@ export type PlatformViewSurfaceKind =
| "route"
| "section"
| "action"
| "selector";
| "selector"
| "product_area"
| "quick_access";
export type PlatformViewSurface = {
id: string;
@@ -271,6 +273,32 @@ export type PlatformNavItem = {
surfaceId?: string;
};
export type ProductAreaContribution = {
id: string;
moduleId: string;
label: string;
description?: string | null;
iconName: PlatformIconName;
surfaceIds: string[];
order?: number;
};
export type QuickAccessToolMetadata = {
id: string;
moduleId: string;
categoryId: string;
label: string;
description?: string | null;
surfaceId: string;
iconName: PlatformIconName;
fullPagePath?: string | null;
allOf?: string[];
anyOf?: string[];
order?: number;
defaultEnabled?: boolean;
modes?: string[];
};
export type PlatformRouteContext = {
settings: ApiSettings;
auth: AuthInfo;
@@ -424,6 +452,8 @@ export type PlatformWebModule = {
uiCapabilities?: PlatformUiCapabilities;
runtimeUiCapabilities?: PlatformUiCapabilities;
viewSurfaces?: PlatformViewSurface[];
productAreas?: ProductAreaContribution[];
quickAccessTools?: QuickAccessToolMetadata[];
helpContexts?: PlatformDocumentationHelpContext[];
};
@@ -439,6 +469,7 @@ export type EffectiveViewProjection = {
activeRevisionId: string | null;
activeViewName: string | null;
visibleSurfaceIds: string[];
presentation?: ViewPresentation;
projectionActive?: boolean;
locked: boolean;
availableViews: EffectiveViewOption[];
@@ -456,6 +487,12 @@ export type EffectiveViewProjection = {
}>;
};
export type ViewPresentation = {
navigationMode?: "grouped" | "flat";
productAreaOrder?: string[];
productAreaLabels?: Record<string, string>;
};
export type ViewSelectorProps = {
settings: ApiSettings;
auth: AuthInfo;
@@ -507,6 +544,28 @@ export type SearchRuntimeUiCapability = {
anyOf?: string[];
};
export type QuickAccessRailProps = PlatformRouteContext & {
tools: QuickAccessToolMetadata[];
};
export type QuickAccessRuntimeUiCapability = {
Rail: ComponentType<QuickAccessRailProps>;
};
export type QuickAccessToolRenderContext = PlatformRouteContext & {
close: () => void;
active: boolean;
};
export type QuickAccessToolContribution = {
id: string;
render: (context: QuickAccessToolRenderContext) => ReactNode;
};
export type QuickAccessToolsUiCapability = {
tools: QuickAccessToolContribution[];
};
export type DashboardWidgetSize = "small" | "medium" | "wide" | "full";
export type DashboardWidgetTone = "neutral" | "good" | "warning" | "danger" | "info";
@@ -1049,6 +1108,31 @@ export type PlatformFrontendModuleInfo = {
default_visible: boolean;
required: boolean;
}>;
presentation_contract_version?: string | null;
product_areas?: Array<{
id: string;
module_id: string;
label: string;
description?: string | null;
icon: string;
surface_ids: string[];
order: number;
}>;
quick_access_tools?: Array<{
id: string;
module_id: string;
category_id: string;
label: string;
description?: string | null;
surface_id: string;
icon: string;
full_page_path?: string | null;
required_all: string[];
required_any: string[];
order: number;
default_enabled: boolean;
modes: string[];
}>;
};
export type PlatformDocumentationHelpContext = {
+44
View File
@@ -16,6 +16,7 @@ import {
viewSurfaceCatalogueForModules,
visibleRoutesForProjection
} from "../src/platform/views";
import { groupNavigationItems } from "../src/platform/productAreas";
import { scopeGrants } from "../src/utils/permissions";
function assert(condition: unknown, message: string): void {
@@ -148,6 +149,49 @@ functionAction.onClick({
assert(selectedFunctionId === "function-1", "organization function actions receive their row context");
assert(!("render" in functionAction), "organization function actions expose metadata instead of arbitrary rendered controls");
const productNavigation = [
{ to: "/dashboard", label: "Dashboard", surfaceId: "dashboard.nav.dashboard" },
{ to: "/tasks", label: "Tasks", surfaceId: "tasks.nav.tasks" },
{ to: "/files", label: "Files", surfaceId: "files.nav.files" },
{ to: "/admin", label: "Administration", surfaceId: "admin.nav.admin" }
];
const productAreas = [
{
id: "work",
moduleId: "tasks",
label: "Work",
iconName: "list-checks" as const,
surfaceIds: ["tasks.nav.tasks"],
order: 10
},
{
id: "records-documents",
moduleId: "files",
label: "Records and documents",
iconName: "folder" as const,
surfaceIds: ["files.nav.files"],
order: 20
}
];
const groupedNavigation = groupNavigationItems(productNavigation, productAreas, {
navigationMode: "grouped",
productAreaOrder: ["records-documents", "work"],
productAreaLabels: { work: "My work" }
});
assert(
groupedNavigation.map((group) => group.id).join(",") ===
"overview,product-area:records-documents,product-area:work,more-tools",
"product-area navigation should preserve dashboard, configured order, and unclassified tools"
);
assert(
groupedNavigation[2]?.label === "My work",
"View presentation should override a product-area label"
);
assert(
groupNavigationItems(productNavigation, productAreas, { navigationMode: "flat" })[0]?.items.length === 4,
"flat navigation should retain every authorized destination"
);
const viewAwareFiles: PlatformWebModule = {
...files,
navItems: [{ to: "/files", label: "Files", order: 20 }],
+1
View File
@@ -20,6 +20,7 @@
"tests/help-context.test.ts",
"tests/definition-graph.test.ts",
"src/platform/moduleLogic.ts",
"src/platform/productAreas.ts",
"src/utils/helpContext.ts",
"src/features/privacy/policyLogic.ts",
"src/definitionGraph.ts",
+2
View File
@@ -41,6 +41,7 @@ const defaultWebModulePackages = [
"@govoplan/portal-webui",
"@govoplan/postbox-webui",
"@govoplan/projects-webui",
"@govoplan/quick-access-webui",
"@govoplan/reporting-webui",
"@govoplan/records-webui",
"@govoplan/risk-compliance-webui",
@@ -270,6 +271,7 @@ export default defineConfig({
fileURLToPath(new URL('../../govoplan-policy/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-portal/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-postbox/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-quick-access/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-risk-compliance/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-scheduling/webui', import.meta.url)),
fileURLToPath(new URL('../../govoplan-search/webui', import.meta.url)),