Release v0.1.2

This commit is contained in:
2026-06-25 19:58:20 +02:00
parent 15794e920e
commit 02564047e9
73 changed files with 4073 additions and 478 deletions

View File

@@ -0,0 +1,21 @@
import { createContext, type ReactNode, useContext } from "react";
import type { PlatformWebModule } from "../types";
import { moduleInstalled, uiCapability } from "./modules";
const PlatformModulesContext = createContext<PlatformWebModule[]>([]);
export function PlatformModulesProvider({ modules, children }: { modules: PlatformWebModule[]; children: ReactNode }) {
return <PlatformModulesContext.Provider value={modules}>{children}</PlatformModulesContext.Provider>;
}
export function usePlatformModules(): PlatformWebModule[] {
return useContext(PlatformModulesContext);
}
export function usePlatformModuleInstalled(moduleId: string): boolean {
return moduleInstalled(moduleId, usePlatformModules());
}
export function usePlatformUiCapability<T = unknown>(capabilityName: string): T | null {
return uiCapability<T>(capabilityName, usePlatformModules());
}

View File

@@ -0,0 +1,27 @@
import type { PlatformRouteContribution, PlatformWebModule } from "../types";
export function uiCapability<T = unknown>(capabilityName: string, modules: PlatformWebModule[]): T | null {
for (const module of modules) {
const value = module.uiCapabilities?.[capabilityName];
if (value !== undefined) return value as T;
}
return null;
}
export function hasUiCapability(capabilityName: string, modules: PlatformWebModule[]): boolean {
return uiCapability(capabilityName, modules) !== null;
}
export function moduleInstalled(moduleId: string, modules: PlatformWebModule[]): boolean {
return modules.some((module) => module.id === moduleId);
}
export function moduleIntegrationEnabled(moduleId: string, dependencyId: string, modules: PlatformWebModule[]): boolean {
const module = modules.find((item) => item.id === moduleId);
if (!module) return false;
return Boolean(module.dependencies?.includes(dependencyId) || (module.optionalDependencies?.includes(dependencyId) && moduleInstalled(dependencyId, modules)));
}
export function routeContributionsForModules(modules: PlatformWebModule[]): PlatformRouteContribution[] {
return modules.flatMap((module) => module.routes ?? []).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}

View File

@@ -1,15 +1,20 @@
import { Activity, FileText, Folder, Form, LayoutDashboard, Mail, Mails, Users, type LucideIcon } from "lucide-react";
import campaignModule from "@govoplan/campaign-webui";
import filesModule from "@govoplan/files-webui";
import mailModule from "@govoplan/mail-webui";
import type { AuthInfo, PlatformModuleInfo, PlatformNavItem, PlatformRouteContribution, PlatformWebModule } from "../types";
import installedWebModules from "virtual:govoplan-installed-modules";
import type { AuthInfo, PlatformModuleInfo, PlatformNavItem, PlatformWebModule } from "../types";
import {
hasUiCapability as hasUiCapabilityForModules,
moduleInstalled as moduleInstalledForModules,
moduleIntegrationEnabled as moduleIntegrationEnabledForModules,
routeContributionsForModules as routeContributionsForModuleList,
uiCapability as uiCapabilityForModules
} from "./moduleLogic";
import { adminReadScopes, hasAnyScope, hasScope } from "../utils/permissions";
export const shellNavItems: PlatformNavItem[] = [
{ to: "/dashboard", label: "Dashboard", iconName: "dashboard", order: 10 }
];
const legacyModules: PlatformWebModule[] = [campaignModule, filesModule, mailModule];
const localModules: PlatformWebModule[] = installedWebModules;
const iconByName: Record<string, LucideIcon> = {
activity: Activity,
@@ -47,6 +52,15 @@ function navFromMetadata(item: PlatformModuleInfo["nav"][number]): PlatformNavIt
};
}
function runtimeUiCapabilitiesForModule(module: PlatformWebModule, info: PlatformModuleInfo) {
const enabledNames = new Set(info.runtime_ui_capabilities ?? []);
if (enabledNames.size === 0 || !module.runtimeUiCapabilities) return {};
return Object.fromEntries(
Object.entries(module.runtimeUiCapabilities).filter(([name]) => enabledNames.has(name))
);
}
function applyServerMetadata(module: PlatformWebModule, info: PlatformModuleInfo): PlatformWebModule {
const backendNav = info.frontend?.nav.length ? info.frontend.nav : info.nav;
return {
@@ -55,14 +69,18 @@ function applyServerMetadata(module: PlatformWebModule, info: PlatformModuleInfo
version: info.version,
dependencies: info.dependencies,
optionalDependencies: info.optional_dependencies,
navItems: backendNav.length ? backendNav.map(navFromMetadata) : module.navItems
navItems: backendNav.length ? backendNav.map(navFromMetadata) : module.navItems,
uiCapabilities: {
...(module.uiCapabilities ?? {}),
...runtimeUiCapabilitiesForModule(module, info)
}
};
}
export function resolveInstalledWebModules(platformModules: PlatformModuleInfo[] | null | undefined): PlatformWebModule[] {
if (!platformModules?.length) return legacyModules;
if (!platformModules?.length) return localModules;
const localById = new Map(legacyModules.map((module) => [module.id, module]));
const localById = new Map(localModules.map((module) => [module.id, module]));
return platformModules
.filter((module) => module.enabled)
.map((module) => {
@@ -72,14 +90,29 @@ export function resolveInstalledWebModules(platformModules: PlatformModuleInfo[]
.filter((module): module is PlatformWebModule => module !== null);
}
export function moduleInstalled(moduleId: string, modules: PlatformWebModule[] = legacyModules): boolean {
return modules.some((module) => module.id === moduleId);
export function installedLocalWebModules(): PlatformWebModule[] {
return localModules;
}
export function moduleIntegrationEnabled(moduleId: string, dependencyId: string, modules: PlatformWebModule[] = legacyModules): boolean {
const module = modules.find((item) => item.id === moduleId);
if (!module) return false;
return Boolean(module.dependencies?.includes(dependencyId) || (module.optionalDependencies?.includes(dependencyId) && moduleInstalled(dependencyId, modules)));
export function uiCapability<T = unknown>(capabilityName: string, modules: PlatformWebModule[] = localModules): T | null {
return uiCapabilityForModules<T>(capabilityName, modules);
}
export function hasUiCapability(capabilityName: string, modules: PlatformWebModule[] = localModules): boolean {
return hasUiCapabilityForModules(capabilityName, modules);
}
export function moduleInstalled(moduleId: string, modules: PlatformWebModule[] = localModules): boolean {
return moduleInstalledForModules(moduleId, modules);
}
export function moduleIntegrationEnabled(moduleId: string, dependencyId: string, modules: PlatformWebModule[] = localModules): boolean {
return moduleIntegrationEnabledForModules(moduleId, dependencyId, modules);
}
export function routeContributionsForModules(modules: PlatformWebModule[]) {
return routeContributionsForModuleList(modules);
}
export function navItemsForModules(modules: PlatformWebModule[]): PlatformNavItem[] {
@@ -88,11 +121,7 @@ export function navItemsForModules(modules: PlatformWebModule[]): PlatformNavIte
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}
export function routeContributionsForModules(modules: PlatformWebModule[]): PlatformRouteContribution[] {
return modules.flatMap((module) => module.routes ?? []).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}
export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = legacyModules): PlatformNavItem[] {
export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = localModules): PlatformNavItem[] {
return navItemsForModules(modules).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;
@@ -100,7 +129,7 @@ export function visibleNavItems(auth: AuthInfo | null | undefined, modules: Plat
});
}
export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = legacyModules): string {
export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = localModules): string {
const preferred = visibleNavItems(auth, modules).find((item) => item.to !== "/dashboard");
if (preferred) return preferred.to;
if (hasAnyScope(auth, adminReadScopes)) return "/admin";