Adds a Graph view header workflow dropdown that filters dependency graph tasks by the selected workflow. - Portal the workflow switcher into the Graph view header and clear selection when unmounted. - Scope plugin graph task context using workflow assignments and default workflow fallback. - Cover the header integration, task filtering, portal lifecycle, and disabled workflow mode. - Document Graph workflow filtering and add the published package changeset. Files changed: .changeset/fn-6941-graph-workflow-dropdown.md | 7 + docs/dashboard-guide.md | 1 + packages/dashboard/app/App.tsx | 4 + .../app/__tests__/graph-workflow-header.test.tsx | 127 +++++++++++++++ .../app/components/GraphWorkflowSwitcherSlot.tsx | 110 +++++++++++++ .../__tests__/GraphWorkflowSwitcherSlot.test.tsx | 173 +++++++++++++++++++++ .../app/components/dashboard/MainContent.tsx | 23 ++- .../dashboard/app/components/dashboard/types.ts | 3 + 8 files changed, 446 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-6941 Fusion-Task-Lineage: a77bc99e-00fe-42fe-a976-d88ffd803388
128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
import { useState } from "react";
|
|
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { BoardWorkflowDefinition, BoardWorkflowsPayload } from "../api";
|
|
import {
|
|
filterTasksByGraphWorkflowSelection,
|
|
GraphWorkflowSwitcherSlot,
|
|
type GraphWorkflowSelection,
|
|
} from "../components/GraphWorkflowSwitcherSlot";
|
|
|
|
const fetchBoardWorkflowsMock = vi.fn();
|
|
const subscribeSseMock = vi.fn(() => vi.fn());
|
|
|
|
vi.mock("../api", () => ({
|
|
fetchBoardWorkflows: (...args: unknown[]) => fetchBoardWorkflowsMock(...args),
|
|
}));
|
|
|
|
vi.mock("../sse-bus", () => ({
|
|
subscribeSse: (...args: unknown[]) => subscribeSseMock(...args),
|
|
}));
|
|
|
|
const DEFAULT_WORKFLOW: BoardWorkflowDefinition = {
|
|
id: "builtin:coding",
|
|
name: "Coding",
|
|
columns: [],
|
|
};
|
|
|
|
const REVIEW_WORKFLOW: BoardWorkflowDefinition = {
|
|
id: "wf-review",
|
|
name: "Review",
|
|
columns: [],
|
|
};
|
|
|
|
const TASKS = [
|
|
{ id: "FN-default", title: "Default task" },
|
|
{ id: "FN-unassigned", title: "Unassigned task" },
|
|
{ id: "FN-review", title: "Review task" },
|
|
{ id: "FN-unknown", title: "Unknown task" },
|
|
];
|
|
|
|
function workflowPayload(overrides: Partial<BoardWorkflowsPayload> = {}): BoardWorkflowsPayload {
|
|
return {
|
|
flagEnabled: true,
|
|
defaultWorkflowId: DEFAULT_WORKFLOW.id,
|
|
workflows: [DEFAULT_WORKFLOW, REVIEW_WORKFLOW],
|
|
taskWorkflowIds: {
|
|
"FN-review": REVIEW_WORKFLOW.id,
|
|
"FN-unknown": "wf-missing",
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function GraphPluginContextHarness({ projectId = "project-graph" }: { projectId?: string }) {
|
|
const [selection, setSelection] = useState<GraphWorkflowSelection | null>(null);
|
|
const pluginTasks = filterTasksByGraphWorkflowSelection(TASKS, projectId, selection);
|
|
|
|
return (
|
|
<>
|
|
<div id="header-workflow-slot" data-testid="header-workflow-slot" />
|
|
<GraphWorkflowSwitcherSlot projectId={projectId} onWorkflowSelectionChange={setSelection} />
|
|
<ul data-testid="graph-plugin-context-tasks">
|
|
{pluginTasks.map((task) => (
|
|
<li key={task.id} data-testid={`graph-context-task-${task.id}`}>{task.title}</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
sessionStorage.clear();
|
|
fetchBoardWorkflowsMock.mockReset();
|
|
subscribeSseMock.mockClear();
|
|
fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload());
|
|
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("Graph workflow header integration", () => {
|
|
it("ports the dropdown into the header and scopes graph plugin tasks by selected workflow", async () => {
|
|
render(<GraphPluginContextHarness />);
|
|
|
|
const headerSlot = screen.getByTestId("header-workflow-slot");
|
|
const selector = await screen.findByTestId("workflow-switcher");
|
|
expect(headerSlot.contains(selector)).toBe(true);
|
|
|
|
const contextTasks = screen.getByTestId("graph-plugin-context-tasks");
|
|
await waitFor(() => {
|
|
expect(within(contextTasks).getByTestId("graph-context-task-FN-default")).toBeInTheDocument();
|
|
expect(within(contextTasks).getByTestId("graph-context-task-FN-unassigned")).toBeInTheDocument();
|
|
expect(within(contextTasks).queryByTestId("graph-context-task-FN-review")).toBeNull();
|
|
expect(within(contextTasks).queryByTestId("graph-context-task-FN-unknown")).toBeNull();
|
|
});
|
|
|
|
fireEvent.click(selector);
|
|
fireEvent.click(screen.getByTestId("workflow-switcher-option-wf-review"));
|
|
|
|
await waitFor(() => {
|
|
expect(within(contextTasks).getByTestId("graph-context-task-FN-review")).toBeInTheDocument();
|
|
expect(within(contextTasks).queryByTestId("graph-context-task-FN-default")).toBeNull();
|
|
expect(within(contextTasks).queryByTestId("graph-context-task-FN-unassigned")).toBeNull();
|
|
expect(within(contextTasks).queryByTestId("graph-context-task-FN-unknown")).toBeNull();
|
|
});
|
|
});
|
|
|
|
it("keeps graph plugin tasks unfiltered when workflow mode is disabled or no project is selected", async () => {
|
|
fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({ flagEnabled: false, workflows: [] }));
|
|
const { rerender } = render(<GraphPluginContextHarness />);
|
|
|
|
await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledWith("project-graph"));
|
|
expect(screen.queryByTestId("workflow-switcher")).toBeNull();
|
|
for (const task of TASKS) {
|
|
expect(screen.getByTestId(`graph-context-task-${task.id}`)).toBeInTheDocument();
|
|
}
|
|
|
|
fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload());
|
|
rerender(<GraphPluginContextHarness projectId="" />);
|
|
await waitFor(() => expect(fetchBoardWorkflowsMock).toHaveBeenCalledWith(""));
|
|
for (const task of TASKS) {
|
|
expect(screen.getByTestId(`graph-context-task-${task.id}`)).toBeInTheDocument();
|
|
}
|
|
});
|
|
});
|