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:
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{
|
||||
"viewId": "graph",
|
||||
"label": "Graph",
|
||||
"componentPath": "./src/DependencyGraphView.tsx",
|
||||
"componentPath": "./dashboard-view",
|
||||
"icon": "Network",
|
||||
"placement": "more",
|
||||
"order": 40
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 })} />);
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user