Files
fusion/packages/dashboard/app/plugins/pluginViewRegistry.tsx
Fusion d20d45ea58 fix(FN-3161): migrate roadmap dashboard surface to bundled plugin
- Move RoadmapsView UI, hooks, and tests from dashboard app into fusion-plugin-roadmap
- Replace the built-in roadmaps route/nav wiring with plugin view registration and host rendering
- Add bundled plugin view registration bootstrap in dashboard startup
- Update roadmap plugin build/test config and add FN-3161 removal changeset

Fusion-Task-Id: FN-3161
2026-05-08 16:51:07 -07:00

88 lines
3.4 KiB
TypeScript

import type { PluginDashboardViewContext } from "./types";
import { AlertTriangle } from "lucide-react";
import { lazy, Suspense, type LazyExoticComponent, type ReactElement, type ReactNode } from "react";
import { ErrorBoundary } from "../components/ErrorBoundary";
import "./pluginViewRegistry.css";
export type PluginTaskView = `plugin:${string}:${string}`;
type PluginViewComponent = LazyExoticComponent<({ context }: { context?: PluginDashboardViewContext }) => ReactElement>;
const registry = new Map<string, PluginViewComponent>();
/** Build composite plugin task view ID: plugin:{pluginId}:{viewId}. */
export function getPluginViewId(pluginId: string, viewId: string): PluginTaskView {
return `plugin:${pluginId}:${viewId}`;
}
/** Parse composite plugin task view ID. Returns null for non-plugin IDs. */
export function parsePluginViewId(value: string): { pluginId: string; viewId: string } | null {
const match = /^plugin:([^:]+):(.+)$/u.exec(value);
if (!match) return null;
return { pluginId: match[1], viewId: match[2] };
}
/** True when a view ID matches the plugin composite ID format. */
export function isPluginViewId(value: string): value is PluginTaskView {
return parsePluginViewId(value) !== null;
}
/** Register a lazy plugin dashboard view component in the static host registry. */
export function registerPluginView(pluginId: string, viewId: string, lazyComponent: PluginViewComponent): void {
registry.set(getPluginViewId(pluginId, viewId), lazyComponent);
}
/** Resolve a plugin dashboard lazy component from the static host registry. */
export function getPluginViewComponent(pluginId: string, viewId: string): PluginViewComponent | null {
return registry.get(getPluginViewId(pluginId, viewId)) ?? null;
}
export function isPluginViewRegistered(pluginId: string, viewId: string): boolean {
return registry.has(getPluginViewId(pluginId, viewId));
}
/** Test helper for clearing global registry state. */
export function __test_clearPluginViewRegistry(): void {
registry.clear();
}
function PluginViewUnavailable({ viewId }: { viewId: string }): ReactNode {
return (
<section className="card plugin-dashboard-view-missing" data-testid="plugin-view-unavailable">
<h2 className="plugin-dashboard-view-missing-title">
<AlertTriangle />
Plugin view unavailable
</h2>
<p className="plugin-dashboard-view-missing-description">
No host registration found for <code>{viewId}</code>.
</p>
</section>
);
}
export function PluginDashboardViewHost({ viewId, context }: { viewId: PluginTaskView; context?: PluginDashboardViewContext }): ReactNode {
const parsed = parsePluginViewId(viewId);
if (!parsed) return <PluginViewUnavailable viewId={viewId} />;
const ViewComponent = getPluginViewComponent(parsed.pluginId, parsed.viewId);
if (!ViewComponent) {
return <PluginViewUnavailable viewId={viewId} />;
}
return (
<ErrorBoundary fallback={<PluginViewUnavailable viewId={viewId} />}>
<Suspense fallback={null}>
<ViewComponent context={context} />
</Suspense>
</ErrorBoundary>
);
}
// Backward-compatible aliases.
export const buildPluginTaskViewId = getPluginViewId;
export const parsePluginTaskViewId = parsePluginViewId;
export const resolvePluginDashboardView = getPluginViewComponent;
// Ensure lazy is referenced for plugin authors importing only this module pattern.
void lazy;