Release v0.1.2
This commit is contained in:
21
webui/src/platform/ModuleContext.tsx
Normal file
21
webui/src/platform/ModuleContext.tsx
Normal 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());
|
||||
}
|
||||
27
webui/src/platform/moduleLogic.ts
Normal file
27
webui/src/platform/moduleLogic.ts
Normal 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));
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user