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:
Fusion
2026-05-07 08:19:24 -07:00
committed by gsxdsm
parent 213ed79d5f
commit e7deab16b8
14 changed files with 104 additions and 15 deletions

View File

@@ -15,6 +15,7 @@ Plugin-provided top-level **Graph** dashboard view for Fusion.
- **Position persistence**: dragged node positions are stored per project in browser localStorage and restored on reload
- **Animated transitions**: fit/reset operations animate `transform` (`var(--transition-normal)`), while continuous drag/wheel/pinch stays transition-free for responsiveness
- **Node rendering**: each graph node renders the real dashboard `TaskCard` via `GraphTaskNode` (no duplicated card markup)
- **Task detail integration**: clicking a graph card opens the native dashboard task detail modal through host context (`openTaskDetail`), matching board/list behavior
- **In-progress behavior**: steps are visible by default and active-task glow (`agent-active`) is preserved because node cards reuse TaskCard directly
- **Active-state indicator bar**: active nodes render a compact top bar (`.graph-task-active-indicator`) with the current execution status label (for example `Executing`, `Planning`) and pulsing `--in-progress` emphasis
- **Current-step highlighting**: active nodes set `data-current-step` for valid native step indices so CSS selectors highlight the currently executing `.card-step-item` and pulse its step dot

View File

@@ -7,7 +7,7 @@
{
"viewId": "graph",
"label": "Graph",
"componentPath": "./src/DependencyGraphView.tsx",
"componentPath": "./dashboard-view",
"icon": "Network",
"placement": "more",
"order": 40

View File

@@ -10,8 +10,8 @@
"import": "./src/index.ts"
},
"./dashboard-view": {
"types": "./src/DependencyGraph.tsx",
"import": "./src/DependencyGraph.tsx"
"types": "./src/index.ts",
"import": "./src/index.ts"
}
},
"scripts": {

View File

@@ -117,4 +117,11 @@ describe("DependencyGraph", () => {
fireEvent.click(screen.getByTestId("task-A"));
expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "A" }));
});
it("falls back to onOpenTaskDetail when onOpenDetail is not provided", () => {
const onOpenTaskDetail = vi.fn();
render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenTaskDetail={onOpenTaskDetail} />);
fireEvent.click(screen.getByTestId("task-A"));
expect(onOpenTaskDetail).toHaveBeenCalledWith("A");
});
});

View File

@@ -40,6 +40,19 @@ afterEach(() => {
});
describe("GraphTaskNode drag", () => {
it("does not open detail after drag threshold is exceeded", () => {
const onOpenDetail = vi.fn();
render(<GraphTaskNode {...props({ onOpenDetail })} />);
const node = screen.getByTestId("graph-task-node-FN-1");
fireEvent.pointerDown(node, { pointerId: 1, clientX: 10, clientY: 10, isPrimary: true });
fireEvent.pointerMove(node, { pointerId: 1, clientX: 25, clientY: 25, isPrimary: true });
fireEvent.pointerUp(node, { pointerId: 1, clientX: 25, clientY: 25, isPrimary: true });
fireEvent.click(node);
expect(onOpenDetail).not.toHaveBeenCalled();
});
it("applies dragging class only after threshold move", () => {
const onNodePositionChange = vi.fn();
render(<GraphTaskNode {...props({ onNodePositionChange })} />);

View File

@@ -11,7 +11,7 @@ describe("dependency graph plugin host integration contract", () => {
expect.objectContaining({
viewId: "graph",
label: "Graph",
componentPath: "./src/DependencyGraph.tsx",
componentPath: "./dashboard-view",
placement: "more",
}),
]);

View File

@@ -1,4 +1,8 @@
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/pluginViewRegistry";
import { definePlugin } from "@fusion/plugin-sdk";
import { createElement } from "react";
import { DependencyGraph } from "./DependencyGraph";
const plugin = definePlugin({
manifest: {
@@ -13,7 +17,7 @@ const plugin = definePlugin({
{
viewId: "graph",
label: "Graph",
componentPath: "./src/DependencyGraph.tsx",
componentPath: "./dashboard-view",
icon: "Network",
placement: "more",
order: 40,
@@ -21,5 +25,18 @@ const plugin = definePlugin({
],
});
function createWorkflowStepNameLookup(workflowSteps: WorkflowStep[] | undefined): ReadonlyMap<string, string> {
return new Map((workflowSteps ?? []).map((step) => [step.id, step.name] as const));
}
export function DependencyGraphDashboardView({ context }: { context?: PluginDashboardViewContext }) {
return createElement(DependencyGraph, {
tasks: context?.tasks ?? [],
projectId: context?.projectId,
workflowStepNameLookup: createWorkflowStepNameLookup(context?.workflowSteps),
onOpenDetail: context?.openTaskDetail as ((task: Task | TaskDetail) => void) | undefined,
});
}
export default plugin;
export { DependencyGraph } from "./DependencyGraph";
export { DependencyGraph };