feat(FN-3087): document graph plugin in plugin authoring guide
Documentation-only finish for FN-3087, adding changeset and README updates for the dependency graph plugin and plugin authoring guide. Fusion-Task-Id: FN-3087
This commit is contained in:
@@ -93,6 +93,7 @@ Navigation placement in this iteration:
|
||||
- **Mobile:** `MobileNavBar` More sheet
|
||||
|
||||
`fusion-plugin-dependency-graph` registers `graph` and is host-resolved through an explicit static registry (`app/plugins/pluginViewRegistry.tsx`) for bundle-safe rendering. CLI dashboard/serve/daemon startup now auto-installs this bundled plugin when missing.
|
||||
Graph view cards now use the same host-provided `openTaskDetail` flow as board/list cards, so clicking a graph node opens the native task detail modal while preserving plugin-owned graph interactions (drag/pan/highlighting).
|
||||
|
||||
### Mobile Bottom Navigation
|
||||
The dashboard now includes a dedicated bottom tab navigation pattern for mobile viewports (`≤768px`) via `MobileNavBar` (`app/components/MobileNavBar.tsx`). This pattern is designed for narrow screens and Capacitor-wrapped app usage where bottom-tab navigation is the primary interaction model.
|
||||
|
||||
@@ -275,6 +275,10 @@ vi.mock("../../components/AgentsView", () => ({
|
||||
AgentsView: () => <div className="agents-view">Agents view</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@fusion-plugin-examples/dependency-graph/dashboard-view", () => ({
|
||||
DependencyGraphDashboardView: () => <div data-testid="dependency-graph">No active tasks to display in graph view.</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../../components/ResearchView", () => ({
|
||||
ResearchView: ({ addToast }: { addToast?: (message: string, type?: "success" | "error" | "info") => void }) => (
|
||||
<div data-testid="research-view">
|
||||
@@ -1777,7 +1781,8 @@ describe("App view switching", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Plugin view unavailable")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dependency-graph")).toBeInTheDocument();
|
||||
expect(screen.getByText("No active tasks to display in graph view.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
@@ -1797,7 +1802,7 @@ describe("App view switching", () => {
|
||||
|
||||
const first = render(<App />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Plugin view unavailable")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dependency-graph")).toBeInTheDocument();
|
||||
});
|
||||
first.unmount();
|
||||
|
||||
@@ -1818,7 +1823,7 @@ describe("App view switching", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Plugin view unavailable")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dependency-graph")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PluginDashboardViewHost as RegistryPluginDashboardViewHost } from "./pluginViewRegistry";
|
||||
import type { PluginTaskView } from "./pluginViewRegistry";
|
||||
import type { PluginDashboardViewContext, PluginTaskView } from "./pluginViewRegistry";
|
||||
|
||||
export function PluginDashboardViewHost({ taskView }: { taskView: PluginTaskView; context?: unknown }) {
|
||||
return <RegistryPluginDashboardViewHost viewId={taskView} />;
|
||||
export function PluginDashboardViewHost({ taskView, context }: { taskView: PluginTaskView; context?: PluginDashboardViewContext }) {
|
||||
return <RegistryPluginDashboardViewHost viewId={taskView} context={context} />;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,17 @@ describe("pluginViewRegistry", () => {
|
||||
expect(await screen.findByText("Rendered Plugin View")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("forwards host context to registered components", async () => {
|
||||
const View = lazy(async () => ({
|
||||
default: ({ context }: { context?: { projectId?: string } }) => <div>{context?.projectId ?? "none"}</div>,
|
||||
}));
|
||||
registerPluginView("plugin-a", "main", View);
|
||||
|
||||
render(<>{PluginDashboardViewHost({ viewId: "plugin:plugin-a:main", context: { projectId: "proj-1", tasks: [], workflowSteps: [], openTaskDetail: () => {} } })}</>);
|
||||
|
||||
expect(await screen.findByText("proj-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders unavailable fallback for unregistered views", () => {
|
||||
render(<>{PluginDashboardViewHost({ viewId: "plugin:plugin-a:missing" })}</>);
|
||||
expect(screen.getByTestId("plugin-view-unavailable")).toBeInTheDocument();
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { lazy, Suspense, type LazyExoticComponent, type ReactElement, type ReactNode } from "react";
|
||||
import { ErrorBoundary } from "../components/ErrorBoundary";
|
||||
import type { DetailTaskTab } from "../hooks/useModalManager";
|
||||
import "./pluginViewRegistry.css";
|
||||
|
||||
export type PluginTaskView = `plugin:${string}:${string}`;
|
||||
|
||||
type PluginViewComponent = LazyExoticComponent<() => ReactElement>;
|
||||
export interface PluginDashboardViewContext {
|
||||
projectId?: string;
|
||||
tasks: Task[];
|
||||
workflowSteps: WorkflowStep[];
|
||||
openTaskDetail: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => void;
|
||||
renderTaskCard?: (task: Task | TaskDetail) => ReactNode;
|
||||
}
|
||||
|
||||
type PluginViewComponent = LazyExoticComponent<({ context }: { context?: PluginDashboardViewContext }) => ReactElement>;
|
||||
|
||||
const registry = new Map<string, PluginViewComponent>();
|
||||
|
||||
@@ -39,8 +49,22 @@ export function getPluginViewComponent(pluginId: string, viewId: string): Plugin
|
||||
/** Test helper for clearing global registry state. */
|
||||
export function __test_clearPluginViewRegistry(): void {
|
||||
registry.clear();
|
||||
registerBundledPluginViews();
|
||||
}
|
||||
|
||||
function registerBundledPluginViews(): void {
|
||||
registerPluginView(
|
||||
"fusion-plugin-dependency-graph",
|
||||
"graph",
|
||||
lazy(async () => {
|
||||
const mod = await import("@fusion-plugin-examples/dependency-graph/dashboard-view");
|
||||
return { default: mod.DependencyGraphDashboardView };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
registerBundledPluginViews();
|
||||
|
||||
function PluginViewUnavailable({ viewId }: { viewId: string }): ReactNode {
|
||||
return (
|
||||
<section className="card plugin-dashboard-view-missing" data-testid="plugin-view-unavailable">
|
||||
@@ -55,7 +79,7 @@ function PluginViewUnavailable({ viewId }: { viewId: string }): ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginDashboardViewHost({ viewId }: { viewId: PluginTaskView }): ReactNode {
|
||||
export function PluginDashboardViewHost({ viewId, context }: { viewId: PluginTaskView; context?: PluginDashboardViewContext }): ReactNode {
|
||||
const parsed = parsePluginViewId(viewId);
|
||||
if (!parsed) return <PluginViewUnavailable viewId={viewId} />;
|
||||
|
||||
@@ -67,7 +91,7 @@ export function PluginDashboardViewHost({ viewId }: { viewId: PluginTaskView }):
|
||||
return (
|
||||
<ErrorBoundary fallback={<PluginViewUnavailable viewId={viewId} />}>
|
||||
<Suspense fallback={null}>
|
||||
<ViewComponent />
|
||||
<ViewComponent context={context} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user