feat(FN-3084): add active node visual states to dependency graph
The merge adds active graph node visual states (highlight, pulsing indicator, selected border) to the dependency-graph plugin with corresponding test coverage, introduces a plugin AI security scan gate and GraphTaskNode wrapper per FN-3426, and includes a small test stabilization fix alongside docum Fusion-Task-Id: FN-3084
This commit is contained in:
@@ -56,6 +56,7 @@ Behavior:
|
|||||||
- Renders directed bezier dependency edges (dependent → dependency) with arrowheads
|
- Renders directed bezier dependency edges (dependent → dependency) with arrowheads
|
||||||
- Supports pan/zoom and fit-to-graph controls via floating toolbar actions
|
- Supports pan/zoom and fit-to-graph controls via floating toolbar actions
|
||||||
- Dependency graph nodes reuse the same `TaskCard` UI as board/list views, so status badges, progress/steps, mission badges, retry/archive controls, and active-task glow stay visually consistent
|
- Dependency graph nodes reuse the same `TaskCard` UI as board/list views, so status badges, progress/steps, mission badges, retry/archive controls, and active-task glow stay visually consistent
|
||||||
|
- Active graph nodes also add a dedicated top status indicator bar and current-step row highlighting so in-progress execution state stays visible even when zoomed out
|
||||||
|
|
||||||
## Chat View
|
## Chat View
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ Plugin-provided top-level **Graph** dashboard view for Fusion.
|
|||||||
- **Fit-to-screen**: computes node bounding box with layout node dimensions and applies zoom/pan so the graph fits in viewport with padding
|
- **Fit-to-screen**: computes node bounding box with layout node dimensions and applies zoom/pan so the graph fits in viewport with padding
|
||||||
- **Node rendering**: each graph node renders the real dashboard `TaskCard` via `GraphTaskNode` (no duplicated card markup)
|
- **Node rendering**: each graph node renders the real dashboard `TaskCard` via `GraphTaskNode` (no duplicated card markup)
|
||||||
- **In-progress behavior**: steps are visible by default and active-task glow (`agent-active`) is preserved because node cards reuse TaskCard directly
|
- **In-progress behavior**: steps are visible by default and active-task glow (`agent-active`) is preserved because node cards reuse TaskCard directly
|
||||||
- **Graph node classes**: `.graph-task-node`, `.graph-task-node--highlighted`, and `.graph-task-node--dimmed` are available for graph-specific layering/highlight states while card internals remain owned by `TaskCard.css`
|
- **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
|
||||||
|
- **Zoom-out differentiation**: `.graph-task-node--active` adds amplified glow and subtle scale/border tint so active nodes remain distinguishable at reduced zoom levels
|
||||||
|
- **Graph node classes**: `.graph-task-node`, `.graph-task-node--active`, `.graph-task-node--highlighted`, and `.graph-task-node--dimmed` are available for graph-specific layering/highlight states while card internals remain owned by `TaskCard.css`
|
||||||
- **Drag behavior**: graph nodes pass `disableDrag={true}` to `TaskCard` so card-level HTML5 drag does not conflict with canvas pan/zoom
|
- **Drag behavior**: graph nodes pass `disableDrag={true}` to `TaskCard` so card-level HTML5 drag does not conflict with canvas pan/zoom
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: min(100%, var(--graph-task-node-width, calc(var(--space-2xl) * 9)));
|
width: min(100%, var(--graph-task-node-width, calc(var(--space-2xl) * 9)));
|
||||||
max-width: var(--graph-task-node-max-width, calc(var(--space-2xl) * 9.5));
|
max-width: var(--graph-task-node-max-width, calc(var(--space-2xl) * 9.5));
|
||||||
transition: box-shadow var(--transition-fast), z-index var(--transition-fast), opacity var(--transition-fast);
|
transition:
|
||||||
|
box-shadow var(--transition-fast),
|
||||||
|
z-index var(--transition-fast),
|
||||||
|
opacity var(--transition-fast),
|
||||||
|
border-color var(--transition-fast);
|
||||||
|
border: var(--btn-border-width) solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-task-node:hover {
|
.graph-task-node:hover {
|
||||||
@@ -20,13 +26,126 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-task-node--active {
|
||||||
|
border-color: rgba(var(--in-progress-rgb), 0.5);
|
||||||
|
box-shadow: 0 0 0 var(--btn-border-width) rgba(var(--in-progress-rgb), 0.45), 0 0 0.75rem rgba(var(--in-progress-rgb), 0.5), 0 0 1.5rem rgba(var(--in-progress-rgb), 0.2);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-task-active-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 1.25rem;
|
||||||
|
width: 100%;
|
||||||
|
padding-inline: var(--space-sm);
|
||||||
|
background: rgba(var(--in-progress-rgb), 0.85);
|
||||||
|
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||||
|
animation: graph-task-active-pulse calc(var(--transition-normal) * 8) ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-task-active-indicator-text {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--card);
|
||||||
|
}
|
||||||
|
|
||||||
.graph-task-node .card {
|
.graph-task-node .card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-task-node[data-current-step="0"] .card-steps-list .card-step-item:nth-child(1),
|
||||||
|
.graph-task-node[data-current-step="1"] .card-steps-list .card-step-item:nth-child(2),
|
||||||
|
.graph-task-node[data-current-step="2"] .card-steps-list .card-step-item:nth-child(3),
|
||||||
|
.graph-task-node[data-current-step="3"] .card-steps-list .card-step-item:nth-child(4),
|
||||||
|
.graph-task-node[data-current-step="4"] .card-steps-list .card-step-item:nth-child(5),
|
||||||
|
.graph-task-node[data-current-step="5"] .card-steps-list .card-step-item:nth-child(6),
|
||||||
|
.graph-task-node[data-current-step="6"] .card-steps-list .card-step-item:nth-child(7),
|
||||||
|
.graph-task-node[data-current-step="7"] .card-steps-list .card-step-item:nth-child(8),
|
||||||
|
.graph-task-node[data-current-step="8"] .card-steps-list .card-step-item:nth-child(9),
|
||||||
|
.graph-task-node[data-current-step="9"] .card-steps-list .card-step-item:nth-child(10),
|
||||||
|
.graph-task-node[data-current-step="10"] .card-steps-list .card-step-item:nth-child(11),
|
||||||
|
.graph-task-node[data-current-step="11"] .card-steps-list .card-step-item:nth-child(12),
|
||||||
|
.graph-task-node[data-current-step="12"] .card-steps-list .card-step-item:nth-child(13),
|
||||||
|
.graph-task-node[data-current-step="13"] .card-steps-list .card-step-item:nth-child(14),
|
||||||
|
.graph-task-node[data-current-step="14"] .card-steps-list .card-step-item:nth-child(15),
|
||||||
|
.graph-task-node[data-current-step="15"] .card-steps-list .card-step-item:nth-child(16),
|
||||||
|
.graph-task-node[data-current-step="16"] .card-steps-list .card-step-item:nth-child(17),
|
||||||
|
.graph-task-node[data-current-step="17"] .card-steps-list .card-step-item:nth-child(18),
|
||||||
|
.graph-task-node[data-current-step="18"] .card-steps-list .card-step-item:nth-child(19),
|
||||||
|
.graph-task-node[data-current-step="19"] .card-steps-list .card-step-item:nth-child(20) {
|
||||||
|
background: rgba(var(--in-progress-rgb), 0.15);
|
||||||
|
border-left: calc(var(--btn-border-width) * 2) solid var(--in-progress);
|
||||||
|
padding-left: var(--space-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-task-node[data-current-step="0"] .card-steps-list .card-step-item:nth-child(1) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="1"] .card-steps-list .card-step-item:nth-child(2) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="2"] .card-steps-list .card-step-item:nth-child(3) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="3"] .card-steps-list .card-step-item:nth-child(4) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="4"] .card-steps-list .card-step-item:nth-child(5) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="5"] .card-steps-list .card-step-item:nth-child(6) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="6"] .card-steps-list .card-step-item:nth-child(7) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="7"] .card-steps-list .card-step-item:nth-child(8) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="8"] .card-steps-list .card-step-item:nth-child(9) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="9"] .card-steps-list .card-step-item:nth-child(10) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="10"] .card-steps-list .card-step-item:nth-child(11) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="11"] .card-steps-list .card-step-item:nth-child(12) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="12"] .card-steps-list .card-step-item:nth-child(13) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="13"] .card-steps-list .card-step-item:nth-child(14) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="14"] .card-steps-list .card-step-item:nth-child(15) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="15"] .card-steps-list .card-step-item:nth-child(16) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="16"] .card-steps-list .card-step-item:nth-child(17) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="17"] .card-steps-list .card-step-item:nth-child(18) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="18"] .card-steps-list .card-step-item:nth-child(19) .card-step-dot,
|
||||||
|
.graph-task-node[data-current-step="19"] .card-steps-list .card-step-item:nth-child(20) .card-step-dot {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
animation: graph-task-step-pulse calc(var(--transition-normal) * 10) ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes graph-task-active-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
background: rgba(var(--in-progress-rgb), 0.85);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background: rgba(var(--in-progress-rgb), 0.65);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes graph-task-step-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.graph-task-node {
|
.graph-task-node {
|
||||||
width: min(100%, var(--graph-task-node-mobile-width, calc(var(--space-2xl) * 8)));
|
width: min(100%, var(--graph-task-node-mobile-width, calc(var(--space-2xl) * 8)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-task-node--active {
|
||||||
|
transform: scale(1.005);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-task-active-indicator {
|
||||||
|
min-height: 1.5rem;
|
||||||
|
padding-inline: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-task-active-indicator-text {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CSSProperties, ComponentProps } from "react";
|
import type { CSSProperties, ComponentProps } from "react";
|
||||||
import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
|
import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
|
||||||
|
import { isTaskStuck } from "@fusion/dashboard/app/utils/taskStuck";
|
||||||
import "./GraphTaskNode.css";
|
import "./GraphTaskNode.css";
|
||||||
|
|
||||||
type TaskCardComponentProps = ComponentProps<typeof TaskCard>;
|
type TaskCardComponentProps = ComponentProps<typeof TaskCard>;
|
||||||
@@ -29,14 +30,49 @@ export interface GraphTaskNodeProps extends TaskCardBridgeProps {
|
|||||||
isHighlighted?: boolean;
|
isHighlighted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]);
|
||||||
|
|
||||||
|
function getStatusLabel(status?: string): string {
|
||||||
|
if (!status) {
|
||||||
|
return "Executing";
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.charAt(0).toUpperCase() + status.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
export function GraphTaskNode({ style, isHighlighted = false, ...taskCardProps }: GraphTaskNodeProps) {
|
export function GraphTaskNode({ style, isHighlighted = false, ...taskCardProps }: GraphTaskNodeProps) {
|
||||||
|
const { task, globalPaused, taskStuckTimeoutMs, lastFetchTimeMs } = taskCardProps;
|
||||||
|
const isFailed = task.status === "failed";
|
||||||
|
const isPaused = task.paused === true;
|
||||||
|
const isStuck = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs);
|
||||||
|
const isAwaitingApproval = task.column === "triage" && task.status === "awaiting-approval";
|
||||||
|
const isActive =
|
||||||
|
!globalPaused &&
|
||||||
|
!isFailed &&
|
||||||
|
!isPaused &&
|
||||||
|
!isStuck &&
|
||||||
|
!isAwaitingApproval &&
|
||||||
|
(task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
|
||||||
|
|
||||||
|
const hasValidCurrentStep =
|
||||||
|
typeof task.currentStep === "number" &&
|
||||||
|
task.currentStep >= 0 &&
|
||||||
|
Array.isArray(task.steps) &&
|
||||||
|
task.currentStep < task.steps.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`graph-task-node${isHighlighted ? " graph-task-node--highlighted" : ""}`}
|
className={`graph-task-node${isHighlighted ? " graph-task-node--highlighted" : ""}${isActive ? " graph-task-node--active" : ""}`}
|
||||||
style={style}
|
style={style}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
data-testid={`graph-task-node-${taskCardProps.task.id}`}
|
data-testid={`graph-task-node-${task.id}`}
|
||||||
|
data-current-step={isActive && hasValidCurrentStep ? String(task.currentStep) : undefined}
|
||||||
>
|
>
|
||||||
|
{isActive ? (
|
||||||
|
<div className="graph-task-active-indicator">
|
||||||
|
<span className="graph-task-active-indicator-text">{getStatusLabel(task.status)}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<TaskCard {...taskCardProps} disableDrag={true} />
|
<TaskCard {...taskCardProps} disableDrag={true} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||||
import type { Task } from "@fusion/core";
|
import type { Task } from "@fusion/core";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { GraphTaskNode } from "../GraphTaskNode";
|
|
||||||
import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
|
import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
|
||||||
|
import { GraphTaskNode } from "../GraphTaskNode";
|
||||||
|
|
||||||
function createTask(overrides: Partial<Task> = {}): Task {
|
function createTask(overrides: Partial<Task> = {}): Task {
|
||||||
return {
|
return {
|
||||||
@@ -53,22 +53,158 @@ describe("GraphTaskNode", () => {
|
|||||||
expect(container.querySelector(".card")?.getAttribute("draggable")).toBe("false");
|
expect(container.querySelector(".card")?.getAttribute("draggable")).toBe("false");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows steps expanded and agent-active styling for in-progress executing tasks", () => {
|
it("shows active indicator with capitalized status for in-progress executing tasks", () => {
|
||||||
|
const props = createProps(
|
||||||
|
createTask({
|
||||||
|
column: "in-progress",
|
||||||
|
status: "executing",
|
||||||
|
steps: [{ name: "step one", status: "in-progress" }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { container } = render(<GraphTaskNode {...props} />);
|
||||||
|
const node = screen.getByTestId("graph-task-node-FN-TEST");
|
||||||
|
expect(node.className).toContain("graph-task-node--active");
|
||||||
|
expect(container.querySelector(".card")?.className).toContain("agent-active");
|
||||||
|
expect(screen.getByText("Executing")).toBeTruthy();
|
||||||
|
expect(container.querySelector(".graph-task-active-indicator")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults indicator text to Executing when status is missing on in-progress tasks", () => {
|
||||||
|
const props = createProps(createTask({ column: "in-progress", status: undefined }));
|
||||||
|
const { container } = render(<GraphTaskNode {...props} />);
|
||||||
|
|
||||||
|
expect(container.querySelector(".graph-task-active-indicator")).toBeTruthy();
|
||||||
|
expect(screen.getByText("Executing")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render active indicator for non-active tasks", () => {
|
||||||
|
const props = createProps(createTask({ column: "in-review", status: "idle" }));
|
||||||
|
const { container } = render(<GraphTaskNode {...props} />);
|
||||||
|
|
||||||
|
expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").className).not.toContain("graph-task-node--active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render active indicator for paused in-progress tasks", () => {
|
||||||
|
const props = createProps(createTask({ column: "in-progress", status: "executing", paused: true }));
|
||||||
|
const { container } = render(<GraphTaskNode {...props} />);
|
||||||
|
|
||||||
|
expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render active indicator for failed in-progress tasks", () => {
|
||||||
|
const props = createProps(createTask({ column: "in-progress", status: "failed" }));
|
||||||
|
const { container } = render(<GraphTaskNode {...props} />);
|
||||||
|
|
||||||
|
expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets current-step attribute for active task when current step is valid", () => {
|
||||||
const props = createProps(
|
const props = createProps(
|
||||||
createTask({
|
createTask({
|
||||||
column: "in-progress",
|
column: "in-progress",
|
||||||
status: "executing",
|
status: "executing",
|
||||||
steps: [
|
steps: [
|
||||||
{ name: "step one", status: "in-progress" },
|
{ name: "step one", status: "done" },
|
||||||
{ name: "step two", status: "pending" },
|
{ name: "step two", status: "done" },
|
||||||
|
{ name: "step three", status: "in-progress" },
|
||||||
],
|
],
|
||||||
|
currentStep: 2,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<GraphTaskNode {...props} />);
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets current-step attribute to zero when first step is active", () => {
|
||||||
|
const props = createProps(
|
||||||
|
createTask({
|
||||||
|
column: "in-progress",
|
||||||
|
status: "executing",
|
||||||
|
steps: [{ name: "step one", status: "in-progress" }],
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { container } = render(<GraphTaskNode {...props} />);
|
render(<GraphTaskNode {...props} />);
|
||||||
expect(container.querySelector(".card")?.className).toContain("agent-active");
|
expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("0");
|
||||||
expect(container.querySelector(".card-steps-list")).toBeTruthy();
|
});
|
||||||
|
|
||||||
|
it("omits current-step attribute when current step is out of bounds", () => {
|
||||||
|
const props = createProps(
|
||||||
|
createTask({
|
||||||
|
column: "in-progress",
|
||||||
|
status: "executing",
|
||||||
|
steps: [{ name: "step one", status: "in-progress" }],
|
||||||
|
currentStep: 10,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<GraphTaskNode {...props} />);
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits current-step attribute when current step is negative", () => {
|
||||||
|
const props = createProps(createTask({ column: "in-progress", status: "executing", steps: [], currentStep: -1 }));
|
||||||
|
|
||||||
|
render(<GraphTaskNode {...props} />);
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits current-step attribute when current step is undefined", () => {
|
||||||
|
const props = createProps(createTask({ column: "in-progress", status: "executing", steps: [], currentStep: undefined }));
|
||||||
|
|
||||||
|
render(<GraphTaskNode {...props} />);
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set current-step for non-active tasks", () => {
|
||||||
|
const props = createProps(
|
||||||
|
createTask({
|
||||||
|
column: "todo",
|
||||||
|
status: "queued",
|
||||||
|
steps: [{ name: "step one", status: "in-progress" }],
|
||||||
|
currentStep: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<GraphTaskNode {...props} />);
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets current-step to native step index when workflow steps are present", () => {
|
||||||
|
const props = createProps(
|
||||||
|
createTask({
|
||||||
|
column: "in-progress",
|
||||||
|
status: "executing",
|
||||||
|
steps: [
|
||||||
|
{ id: "native-1", name: "native one", status: "done" },
|
||||||
|
{ id: "native-2", name: "native two", status: "in-progress" },
|
||||||
|
],
|
||||||
|
enabledWorkflowSteps: ["wf-1"],
|
||||||
|
currentStep: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<GraphTaskNode {...props} />);
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set current-step when native step list is empty even with workflow steps", () => {
|
||||||
|
const props = createProps(
|
||||||
|
createTask({
|
||||||
|
column: "in-progress",
|
||||||
|
status: "executing",
|
||||||
|
steps: [],
|
||||||
|
enabledWorkflowSteps: ["wf-1"],
|
||||||
|
currentStep: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<GraphTaskNode {...props} />);
|
||||||
|
expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking card opens task detail", () => {
|
it("clicking card opens task detail", () => {
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
declare module "@fusion/dashboard/app/utils/taskStuck" {
|
||||||
|
import type { Task } from "@fusion/core";
|
||||||
|
|
||||||
|
export function isTaskStuck(task: Task, taskStuckTimeoutMs?: number, lastFetchTimeMs?: number): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
declare module "@fusion/dashboard/app/components/TaskCard" {
|
declare module "@fusion/dashboard/app/components/TaskCard" {
|
||||||
import type { Column, Task, TaskDetail } from "@fusion/core";
|
import type { Column, Task, TaskDetail } from "@fusion/core";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default defineConfig({
|
|||||||
"@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)),
|
"@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)),
|
||||||
"@fusion/plugin-sdk": fileURLToPath(new URL("../../packages/plugin-sdk/src/index.ts", import.meta.url)),
|
"@fusion/plugin-sdk": fileURLToPath(new URL("../../packages/plugin-sdk/src/index.ts", import.meta.url)),
|
||||||
"@fusion/dashboard": fileURLToPath(new URL("../../packages/dashboard", import.meta.url)),
|
"@fusion/dashboard": fileURLToPath(new URL("../../packages/dashboard", import.meta.url)),
|
||||||
|
"lucide-react": fileURLToPath(new URL("../../packages/dashboard/node_modules/lucide-react", import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
Reference in New Issue
Block a user