perf(webui): lazily load module descriptors
This commit is contained in:
+85
-34
@@ -1,5 +1,5 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { lazy, useEffect, useMemo, useState } from "react";
|
||||
import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth";
|
||||
import { fetchPlatformModules, fetchPlatformPublicModules, fetchPlatformStatus } from "./api/platform";
|
||||
import { AUTH_REQUIRED_EVENT, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
|
||||
@@ -8,7 +8,7 @@ import AppShell from "./layout/AppShell";
|
||||
import PublicLandingPage from "./features/auth/PublicLandingPage";
|
||||
import LoginModal from "./features/auth/LoginModal";
|
||||
import { PermissionBoundary } from "./components/AccessBoundary";
|
||||
import { firstAccessibleRoute, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, resolveInstalledPublicWebModules, resolveInstalledWebModules, routeContributionsForModules, uiCapability } from "./platform/modules";
|
||||
import { firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebModules, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, routeContributionsForModules, uiCapability } from "./platform/modules";
|
||||
import { PlatformModulesProvider } from "./platform/ModuleContext";
|
||||
import { PlatformViewProvider } from "./platform/ViewContext";
|
||||
import { PLATFORM_VIEW_CHANGED_EVENT } from "./platform/views";
|
||||
@@ -16,6 +16,7 @@ import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents";
|
||||
import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard";
|
||||
import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext";
|
||||
import ViewSurfaceRouteBoundary from "./components/ViewSurfaceRouteBoundary";
|
||||
import ModuleLoadBoundary from "./components/ModuleLoadBoundary";
|
||||
|
||||
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
|
||||
const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
|
||||
@@ -29,22 +30,24 @@ const DEFAULT_UI_PREFERENCES: UserUiPreferences = {
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const location = useLocation();
|
||||
const [settings, setSettings] = useState<ApiSettings>(() => loadApiSettings());
|
||||
const [auth, setAuth] = useState<AuthInfo | null>(null);
|
||||
const [checkingSession, setCheckingSession] = useState(true);
|
||||
const [platformModules, setPlatformModules] = useState<PlatformModuleInfo[] | null>(null);
|
||||
const [platformPublicModules, setPlatformPublicModules] = useState<PlatformPublicModuleInfo[] | null>(null);
|
||||
const [localWebModules, setLocalWebModules] = useState<PlatformWebModule[]>([]);
|
||||
const [localPublicWebModules, setLocalPublicWebModules] = useState<PlatformWebModule[]>([]);
|
||||
const [remoteWebModules, setRemoteWebModules] = useState<PlatformWebModule[]>([]);
|
||||
const [remotePublicWebModules, setRemotePublicWebModules] = useState<PlatformWebModule[]>([]);
|
||||
const [webModulesLoading, setWebModulesLoading] = useState(true);
|
||||
const [maintenanceMode, setMaintenanceMode] = useState<{enabled: boolean;message?: string | null;}>({ enabled: false, message: null });
|
||||
const [backendReachable, setBackendReachable] = useState(true);
|
||||
const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null);
|
||||
const [reloginMessage, setReloginMessage] = useState("");
|
||||
const [viewProjection, setViewProjection] = useState<EffectiveViewProjection | null>(null);
|
||||
|
||||
const localWebModules = useMemo(() => resolveInstalledWebModules(platformModules), [platformModules]);
|
||||
const webModules = useMemo(() => mergeWebModules(localWebModules, remoteWebModules), [localWebModules, remoteWebModules]);
|
||||
const localPublicWebModules = useMemo(() => resolveInstalledPublicWebModules(platformPublicModules), [platformPublicModules]);
|
||||
const publicWebModules = useMemo(() => mergeWebModules(localPublicWebModules, remotePublicWebModules), [localPublicWebModules, remotePublicWebModules]);
|
||||
const viewsRuntime = useMemo(
|
||||
() => uiCapability<ViewsRuntimeUiCapability>("views.runtime", webModules),
|
||||
@@ -112,6 +115,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
function handlePublicLogin(response: LoginResponse) {
|
||||
setWebModulesLoading(true);
|
||||
updateAuth(authFromLoginResponse(response), "");
|
||||
}
|
||||
|
||||
@@ -186,6 +190,7 @@ export default function App() {
|
||||
const shellAuth = await fetchShellAuth(settings);
|
||||
if (!cancelled) {
|
||||
setBackendReachable(true);
|
||||
setWebModulesLoading(true);
|
||||
setAuth(normalizeAuthInfo(shellAuth));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -196,6 +201,7 @@ export default function App() {
|
||||
saveApiSettings(cleared);
|
||||
setAuth(null);
|
||||
setPlatformModules(null);
|
||||
setWebModulesLoading(false);
|
||||
setRemoteWebModules([]);
|
||||
}
|
||||
} finally {
|
||||
@@ -218,7 +224,12 @@ export default function App() {
|
||||
lastRefreshAt = Date.now();
|
||||
return fetchPlatformModules(settings).
|
||||
then((response) => {if (!cancelled) setPlatformModules(response.modules);}).
|
||||
catch(() => {if (!cancelled) setPlatformModules(null);}).
|
||||
catch(() => {
|
||||
if (!cancelled) {
|
||||
setPlatformModules(null);
|
||||
setWebModulesLoading(false);
|
||||
}
|
||||
}).
|
||||
finally(() => {inFlight = false;});
|
||||
}
|
||||
|
||||
@@ -245,6 +256,70 @@ export default function App() {
|
||||
};
|
||||
}, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!auth) {
|
||||
setLocalWebModules([]);
|
||||
setRemoteWebModules([]);
|
||||
setWebModulesLoading(false);
|
||||
return () => {cancelled = true;};
|
||||
}
|
||||
if (platformModules === null) {
|
||||
return () => {cancelled = true;};
|
||||
}
|
||||
if (platformModules.length === 0) {
|
||||
setLocalWebModules([]);
|
||||
setRemoteWebModules([]);
|
||||
setWebModulesLoading(false);
|
||||
return () => {cancelled = true;};
|
||||
}
|
||||
|
||||
async function loadWebModules() {
|
||||
const local = await loadInstalledWebModules(platformModules);
|
||||
if (cancelled) return;
|
||||
setLocalWebModules(local);
|
||||
setWebModulesLoading(false);
|
||||
const remote = await loadRemoteWebModules(platformModules, local);
|
||||
if (!cancelled) setRemoteWebModules(remote);
|
||||
}
|
||||
|
||||
void loadWebModules().catch((error) => {
|
||||
console.error("Failed to load platform WebUI modules", error);
|
||||
if (!cancelled) {
|
||||
setLocalWebModules([]);
|
||||
setRemoteWebModules([]);
|
||||
setWebModulesLoading(false);
|
||||
}
|
||||
});
|
||||
return () => {cancelled = true;};
|
||||
}, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, platformModules]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!platformPublicModules?.length) {
|
||||
setLocalPublicWebModules([]);
|
||||
setRemotePublicWebModules([]);
|
||||
return () => {cancelled = true;};
|
||||
}
|
||||
|
||||
async function loadPublicWebModules() {
|
||||
const local = await loadInstalledPublicWebModules(platformPublicModules);
|
||||
if (cancelled) return;
|
||||
setLocalPublicWebModules(local);
|
||||
const remote = await loadRemotePublicWebModules(platformPublicModules, local);
|
||||
if (!cancelled) setRemotePublicWebModules(remote);
|
||||
}
|
||||
|
||||
void loadPublicWebModules().catch((error) => {
|
||||
console.error("Failed to load public WebUI modules", error);
|
||||
if (!cancelled) {
|
||||
setLocalPublicWebModules([]);
|
||||
setRemotePublicWebModules([]);
|
||||
}
|
||||
});
|
||||
return () => {cancelled = true;};
|
||||
}, [platformPublicModules]);
|
||||
|
||||
useEffect(() => {
|
||||
const preferences = auth?.user.ui_preferences ?? DEFAULT_UI_PREFERENCES;
|
||||
const root = document.documentElement;
|
||||
@@ -279,30 +354,6 @@ export default function App() {
|
||||
auth?.user.ui_preferences?.theme
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!auth || !platformModules?.length) {
|
||||
setRemoteWebModules([]);
|
||||
return () => {cancelled = true;};
|
||||
}
|
||||
loadRemoteWebModules(platformModules, localWebModules).
|
||||
then((modules) => {if (!cancelled) setRemoteWebModules(modules);}).
|
||||
catch(() => {if (!cancelled) setRemoteWebModules([]);});
|
||||
return () => {cancelled = true;};
|
||||
}, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, platformModules, localWebModules]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!platformPublicModules?.length) {
|
||||
setRemotePublicWebModules([]);
|
||||
return () => {cancelled = true;};
|
||||
}
|
||||
loadRemotePublicWebModules(platformPublicModules, localPublicWebModules).
|
||||
then((modules) => {if (!cancelled) setRemotePublicWebModules(modules);}).
|
||||
catch(() => {if (!cancelled) setRemotePublicWebModules([]);});
|
||||
return () => {cancelled = true;};
|
||||
}, [platformPublicModules, localPublicWebModules]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth) return;
|
||||
|
||||
@@ -380,7 +431,7 @@ export default function App() {
|
||||
<PlatformViewProvider modules={publicWebModules} projection={null}>
|
||||
<UnsavedChangesProvider>
|
||||
<AppShell settings={settings} auth={null} onSettingsChange={updateSettings} onAuthChange={updateAuth} publicMode navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
|
||||
<Suspense fallback={<div className="content-pad"><p className="muted">i18n:govoplan-core.loading_module.50161f3c</p></div>}>
|
||||
<ModuleLoadBoundary resetKey={location.pathname}>
|
||||
<Routes>
|
||||
{publicRoutes.map((route) =>
|
||||
<Route
|
||||
@@ -391,7 +442,7 @@ export default function App() {
|
||||
)}
|
||||
<Route path="*" element={<PublicLandingPage settings={settings} maintenanceMode={maintenanceMode} onLogin={handlePublicLogin} />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ModuleLoadBoundary>
|
||||
</AppShell>
|
||||
</UnsavedChangesProvider>
|
||||
</PlatformViewProvider>
|
||||
@@ -424,7 +475,7 @@ export default function App() {
|
||||
<PlatformViewProvider modules={webModules} projection={viewProjection}>
|
||||
<UnsavedChangesProvider>
|
||||
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
|
||||
<Suspense fallback={<div className="content-pad"><p className="muted">i18n:govoplan-core.loading_module.50161f3c</p></div>}>
|
||||
<ModuleLoadBoundary resetKey={location.pathname} loading={webModulesLoading}>
|
||||
<Routes key={(auth.active_tenant ?? auth.tenant).id}>
|
||||
<Route path="/" element={<Navigate to={defaultRoute} replace />} />
|
||||
{!dashboardModuleInstalled && <Route path="/dashboard" element={<DashboardPage />} />}
|
||||
@@ -455,7 +506,7 @@ export default function App() {
|
||||
<Route path="/settings" element={<SettingsPage settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} />} />
|
||||
<Route path="*" element={<Navigate to={defaultRoute} replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ModuleLoadBoundary>
|
||||
{reloginMessage &&
|
||||
<LoginModal
|
||||
settings={settings}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, Suspense, type ErrorInfo, type ReactNode } from "react";
|
||||
import Button from "./Button";
|
||||
import DismissibleAlert from "./DismissibleAlert";
|
||||
import LoadingIndicator from "./LoadingIndicator";
|
||||
|
||||
type ModuleLoadBoundaryProps = {
|
||||
children: ReactNode;
|
||||
resetKey: string;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
type ModuleLoadBoundaryState = {
|
||||
error: Error | null;
|
||||
};
|
||||
|
||||
export default class ModuleLoadBoundary extends Component<
|
||||
ModuleLoadBoundaryProps,
|
||||
ModuleLoadBoundaryState
|
||||
> {
|
||||
state: ModuleLoadBoundaryState = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): ModuleLoadBoundaryState {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error("GovOPlaN module route failed to load", error, info);
|
||||
}
|
||||
|
||||
componentDidUpdate(previousProps: ModuleLoadBoundaryProps) {
|
||||
if (
|
||||
previousProps.resetKey !== this.props.resetKey &&
|
||||
this.state.error
|
||||
) {
|
||||
this.setState({ error: null });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
return <ModuleLoadProgress />;
|
||||
}
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="content-pad module-load-error">
|
||||
<DismissibleAlert tone="danger" dismissible={false}>
|
||||
<p>i18n:govoplan-core.the_resource_could_not_be_loaded.0d1b6cbf</p>
|
||||
<Button type="button" onClick={() => window.location.reload()}>
|
||||
i18n:govoplan-core.reload.cce71553
|
||||
</Button>
|
||||
</DismissibleAlert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<ModuleLoadProgress />}
|
||||
>
|
||||
{this.props.children}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ModuleLoadProgress() {
|
||||
return (
|
||||
<div className="content-pad module-load-progress">
|
||||
<LoadingIndicator
|
||||
label="i18n:govoplan-core.loading_module.50161f3c"
|
||||
size="md"
|
||||
/>
|
||||
<span>i18n:govoplan-core.loading_module.50161f3c</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Activity, Bell, BookUser, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, Form, Inbox, LayoutDashboard, LayoutTemplate, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
|
||||
import installedWebModules from "virtual:govoplan-installed-modules";
|
||||
import installedWebModuleLoaderSource from "virtual:govoplan-installed-modules";
|
||||
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
|
||||
import {
|
||||
hasUiCapability as hasUiCapabilityForModules,
|
||||
@@ -21,12 +21,20 @@ import {
|
||||
export const fallbackDashboardNavItem: PlatformNavItem =
|
||||
{ to: "/dashboard", label: "i18n:govoplan-core.dashboard.d87f47b4", iconName: "dashboard", order: 10 };
|
||||
|
||||
type InstalledWebModuleLoader = {
|
||||
packageName: string;
|
||||
load: () => Promise<{ default: PlatformWebModule }>;
|
||||
};
|
||||
|
||||
const installedWebModuleLoaders =
|
||||
installedWebModuleLoaderSource as unknown as InstalledWebModuleLoader[];
|
||||
|
||||
export function shellNavItemsForModules(modules: PlatformWebModule[]): PlatformNavItem[] {
|
||||
return moduleInstalledForModules("dashboard", modules) ? [] : [fallbackDashboardNavItem];
|
||||
}
|
||||
|
||||
|
||||
const localModules: PlatformWebModule[] = installedWebModules;
|
||||
const localModuleCache = new Map<string, Promise<PlatformWebModule | null>>();
|
||||
const loadedLocalModules = new Map<string, PlatformWebModule>();
|
||||
const remoteModuleCache = new Map<string, Promise<PlatformWebModule | null>>();
|
||||
|
||||
declare global {
|
||||
@@ -214,44 +222,39 @@ function publicModuleInfo(info: PlatformPublicModuleInfo): PlatformModuleInfo {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveInstalledPublicWebModules(
|
||||
export async function loadInstalledPublicWebModules(
|
||||
platformModules: PlatformPublicModuleInfo[] | null | undefined
|
||||
): PlatformWebModule[] {
|
||||
): Promise<PlatformWebModule[]> {
|
||||
if (!platformModules?.length) return [];
|
||||
const localById = new Map(localModules.map((module) => [module.id, module]));
|
||||
return platformModules.flatMap((info) => {
|
||||
const local = localById.get(info.id);
|
||||
if (!local) return [];
|
||||
const resolved = applyServerMetadata(local, publicModuleInfo(info));
|
||||
return resolved.publicRoutes?.length ? [resolved] : [];
|
||||
});
|
||||
const loaded = await loadInstalledWebModules(platformModules.map(publicModuleInfo));
|
||||
return loaded.filter((module) => module.publicRoutes?.length);
|
||||
}
|
||||
|
||||
export async function loadRemotePublicWebModules(
|
||||
platformModules: PlatformPublicModuleInfo[] | null | undefined,
|
||||
alreadyLoaded: PlatformWebModule[] = resolveInstalledPublicWebModules(platformModules)
|
||||
alreadyLoaded: PlatformWebModule[] = []
|
||||
): Promise<PlatformWebModule[]> {
|
||||
if (!platformModules?.length) return [];
|
||||
const loaded = await loadRemoteWebModules(platformModules.map(publicModuleInfo), alreadyLoaded);
|
||||
return loaded.filter((module) => module.publicRoutes?.length);
|
||||
}
|
||||
|
||||
export function resolveInstalledWebModules(platformModules: PlatformModuleInfo[] | null | undefined): PlatformWebModule[] {
|
||||
if (!platformModules?.length) return localModules;
|
||||
export async function loadInstalledWebModules(
|
||||
platformModules: PlatformModuleInfo[] | null | undefined
|
||||
): Promise<PlatformWebModule[]> {
|
||||
if (!platformModules?.length) return [];
|
||||
|
||||
const localById = new Map(localModules.map((module) => [module.id, module]));
|
||||
return platformModules.
|
||||
filter((module) => module.enabled).
|
||||
map((module) => {
|
||||
const local = localById.get(module.id);
|
||||
return local ? applyServerMetadata(local, module) : null;
|
||||
}).
|
||||
filter((module): module is PlatformWebModule => module !== null);
|
||||
const enabledModules = platformModules.filter((module) => module.enabled);
|
||||
const resolved = await Promise.all(enabledModules.map(async (info) => {
|
||||
const local = await loadInstalledWebModule(info);
|
||||
return local ? applyServerMetadata(local, info) : null;
|
||||
}));
|
||||
return resolved.filter((module): module is PlatformWebModule => module !== null);
|
||||
}
|
||||
|
||||
export async function loadRemoteWebModules(
|
||||
platformModules: PlatformModuleInfo[] | null | undefined,
|
||||
alreadyLoaded: PlatformWebModule[] = resolveInstalledWebModules(platformModules))
|
||||
alreadyLoaded: PlatformWebModule[] = [])
|
||||
: Promise<PlatformWebModule[]> {
|
||||
if (!platformModules?.length) return [];
|
||||
const loadedIds = new Set(alreadyLoaded.map((module) => module.id));
|
||||
@@ -274,6 +277,56 @@ alreadyLoaded: PlatformWebModule[] = resolveInstalledWebModules(platformModules)
|
||||
return loaded.filter((module): module is PlatformWebModule => module !== null);
|
||||
}
|
||||
|
||||
async function loadInstalledWebModule(info: PlatformModuleInfo): Promise<PlatformWebModule | null> {
|
||||
if (!info.frontend) return null;
|
||||
const packageName = info.frontend?.package_name;
|
||||
if (packageName) {
|
||||
const loader = installedWebModuleLoaders.find((candidate) => candidate.packageName === packageName);
|
||||
return loader ? loadInstalledWebModulePackage(loader, info.id) : null;
|
||||
}
|
||||
|
||||
// Legacy manifests may omit package_name. Keep compatibility without making
|
||||
// every modern module eager: only this fallback imports all installed descriptors.
|
||||
const candidates = await Promise.all(
|
||||
installedWebModuleLoaders.map((loader) => loadInstalledWebModulePackage(loader))
|
||||
);
|
||||
return candidates.find((candidate) => candidate?.id === info.id) ?? null;
|
||||
}
|
||||
|
||||
async function loadInstalledWebModulePackage(
|
||||
loader: InstalledWebModuleLoader,
|
||||
expectedModuleId?: string
|
||||
): Promise<PlatformWebModule | null> {
|
||||
let promise = localModuleCache.get(loader.packageName);
|
||||
if (!promise) {
|
||||
promise = loader.load().
|
||||
then((imported) => {
|
||||
const module = imported.default;
|
||||
if (!isPlatformWebModule(module)) {
|
||||
throw new Error(`${loader.packageName} does not export a PlatformWebModule`);
|
||||
}
|
||||
loadedLocalModules.set(loader.packageName, module);
|
||||
return module;
|
||||
}).
|
||||
catch((error) => {
|
||||
localModuleCache.delete(loader.packageName);
|
||||
console.warn("GovOPlaN installed WebUI module was not loaded:", loader.packageName, error);
|
||||
return null;
|
||||
});
|
||||
localModuleCache.set(loader.packageName, promise);
|
||||
}
|
||||
const module = await promise;
|
||||
if (module && expectedModuleId && module.id !== expectedModuleId) {
|
||||
console.warn(
|
||||
"GovOPlaN installed WebUI package id mismatch:",
|
||||
loader.packageName,
|
||||
`expected ${expectedModuleId}, received ${module.id}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
async function loadRemoteWebModule(info: PlatformModuleInfo): Promise<PlatformWebModule | null> {
|
||||
const frontend = info.frontend;
|
||||
if (!frontend?.asset_manifest) return null;
|
||||
@@ -387,26 +440,26 @@ function constantTimeEqual(left: string, right: string): boolean {
|
||||
}
|
||||
|
||||
export function installedLocalWebModules(): PlatformWebModule[] {
|
||||
return localModules;
|
||||
return [...loadedLocalModules.values()];
|
||||
}
|
||||
|
||||
export function uiCapability<T = unknown>(capabilityName: string, modules: PlatformWebModule[] = localModules): T | null {
|
||||
export function uiCapability<T = unknown>(capabilityName: string, modules: PlatformWebModule[] = installedLocalWebModules()): T | null {
|
||||
return uiCapabilityForModules<T>(capabilityName, modules);
|
||||
}
|
||||
|
||||
export function uiCapabilities<T = unknown>(capabilityName: string, modules: PlatformWebModule[] = localModules): T[] {
|
||||
export function uiCapabilities<T = unknown>(capabilityName: string, modules: PlatformWebModule[] = installedLocalWebModules()): T[] {
|
||||
return uiCapabilitiesForModules<T>(capabilityName, modules);
|
||||
}
|
||||
|
||||
export function hasUiCapability(capabilityName: string, modules: PlatformWebModule[] = localModules): boolean {
|
||||
export function hasUiCapability(capabilityName: string, modules: PlatformWebModule[] = installedLocalWebModules()): boolean {
|
||||
return hasUiCapabilityForModules(capabilityName, modules);
|
||||
}
|
||||
|
||||
export function moduleInstalled(moduleId: string, modules: PlatformWebModule[] = localModules): boolean {
|
||||
export function moduleInstalled(moduleId: string, modules: PlatformWebModule[] = installedLocalWebModules()): boolean {
|
||||
return moduleInstalledForModules(moduleId, modules);
|
||||
}
|
||||
|
||||
export function moduleIntegrationEnabled(moduleId: string, dependencyId: string, modules: PlatformWebModule[] = localModules): boolean {
|
||||
export function moduleIntegrationEnabled(moduleId: string, dependencyId: string, modules: PlatformWebModule[] = installedLocalWebModules()): boolean {
|
||||
return moduleIntegrationEnabledForModules(moduleId, dependencyId, modules);
|
||||
}
|
||||
|
||||
@@ -419,7 +472,7 @@ export function publicRouteContributionsForModules(modules: PlatformWebModule[])
|
||||
}
|
||||
|
||||
export function dashboardWidgetsForModules(
|
||||
modules: PlatformWebModule[] = localModules,
|
||||
modules: PlatformWebModule[] = installedLocalWebModules(),
|
||||
projection?: EffectiveViewProjection | null
|
||||
): DashboardWidgetContribution[] {
|
||||
const catalogue = viewSurfaceCatalogueForModules(modules);
|
||||
@@ -446,7 +499,7 @@ export function navItemsForModules(
|
||||
sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
||||
}
|
||||
|
||||
export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = localModules, projection?: EffectiveViewProjection | null): PlatformNavItem[] {
|
||||
export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = installedLocalWebModules(), 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;
|
||||
@@ -454,6 +507,6 @@ export function visibleNavItems(auth: AuthInfo | null | undefined, modules: Plat
|
||||
});
|
||||
}
|
||||
|
||||
export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = localModules, projection?: EffectiveViewProjection | null): string {
|
||||
export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = installedLocalWebModules(), projection?: EffectiveViewProjection | null): string {
|
||||
return visibleNavItems(auth, modules, projection)[0]?.to ?? "/dashboard";
|
||||
}
|
||||
|
||||
@@ -2204,6 +2204,29 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.module-load-progress {
|
||||
display: flex;
|
||||
min-height: 120px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.module-load-error .alert {
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.module-load-error .alert-message {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.module-load-error .alert-message p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Collapsible cards */
|
||||
.card-collapsible .card-header {
|
||||
|
||||
Vendored
+6
-3
@@ -4,7 +4,10 @@
|
||||
declare module "virtual:govoplan-installed-modules" {
|
||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||
|
||||
const installedWebModules: PlatformWebModule[];
|
||||
export { installedWebModules };
|
||||
export default installedWebModules;
|
||||
const installedWebModuleLoaders: Array<{
|
||||
packageName: string;
|
||||
load: () => Promise<{ default: PlatformWebModule }>;
|
||||
}>;
|
||||
|
||||
export default installedWebModuleLoaders;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user