feat: clarify workflow graph presentation
This commit is contained in:
@@ -88,6 +88,22 @@ describe("DemoWorkflowScene", () => {
|
||||
expect(screen.queryByText("Retry live service")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("presents Scene 10 graph as a ten-node horizontal workflow diagram", () => {
|
||||
renderBeat("graph");
|
||||
|
||||
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-graph-layout", "horizontal");
|
||||
const graph = screen.getByRole("group", { name: /workflow graph/i });
|
||||
expect(graph).toHaveAttribute("data-graph-layout", "horizontal");
|
||||
expect(document.querySelectorAll('button[aria-label^="workflow node:"]')).toHaveLength(10);
|
||||
expect(screen.getByText(/revision requested/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/cancel: no submitted output/i)).not.toBeInTheDocument();
|
||||
|
||||
const legend = screen.getByLabelText("workflow graph node types");
|
||||
expect(legend).toHaveTextContent("Action");
|
||||
expect(legend).toHaveTextContent("Human boundary");
|
||||
expect(legend).toHaveTextContent("Outcome");
|
||||
});
|
||||
|
||||
it("keeps the run receipt visible when the graph takes over", () => {
|
||||
renderBeat("graph");
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ export const DemoWorkflowScene = ({
|
||||
className="demo-workflow-stage"
|
||||
data-beat={beat.id}
|
||||
data-demo-layout={layout}
|
||||
data-graph-layout={beat.id === "graph" ? "horizontal" : undefined}
|
||||
data-primary-surface={surface.primarySurface}
|
||||
data-support-surface={surface.supportSurface}
|
||||
aria-label="demo workflow stage"
|
||||
|
||||
@@ -66,6 +66,26 @@ describe("WorkflowGraphStage", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("exposes exactly the ten real workflow nodes as accessible controls", () => {
|
||||
render(
|
||||
<WorkflowGraphStage
|
||||
execution={{ completedNodeIds: [], currentNodeId: null }}
|
||||
selectedNodeId={null}
|
||||
selectNode={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const graph = screen.getByRole("group", { name: /workflow graph/i });
|
||||
// React Flow hides unmeasured node wrappers in jsdom, so assert the
|
||||
// rendered button contract directly rather than depending on its role tree.
|
||||
const workflowNodes = document.querySelectorAll<HTMLButtonElement>(
|
||||
'button[aria-label^="workflow node:"]',
|
||||
);
|
||||
expect(workflowNodes).toHaveLength(10);
|
||||
expect(screen.queryByText(/cancel: no submitted output/i)).not.toBeInTheDocument();
|
||||
expect(graph).toHaveTextContent("Revision requested");
|
||||
});
|
||||
|
||||
it("does not render graph proof count chips", () => {
|
||||
render(
|
||||
<WorkflowGraphStage
|
||||
@@ -127,7 +147,9 @@ describe("WorkflowGraphStage", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("group", { name: "workflow graph" })).toHaveAttribute("data-graph-direction", "horizontal");
|
||||
const graph = screen.getByRole("group", { name: "workflow graph" });
|
||||
expect(graph).toHaveAttribute("data-graph-direction", "horizontal");
|
||||
expect(graph).toHaveAttribute("data-graph-layout", "horizontal");
|
||||
expect(screen.getByRole("button", { name: /zoom in/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /zoom out/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /fit view/i })).toBeInTheDocument();
|
||||
|
||||
@@ -34,6 +34,20 @@ type PresentationNodeData = WorkflowGraphNodeData & {
|
||||
readonly selectNode: (nodeId: string) => void;
|
||||
};
|
||||
|
||||
const fullGraphLayout = {
|
||||
nodeWidth: 176,
|
||||
nodeHeight: 76,
|
||||
nodesep: 72,
|
||||
ranksep: 56,
|
||||
} as const;
|
||||
|
||||
const compactGraphLayout = {
|
||||
nodeWidth: 132,
|
||||
nodeHeight: 64,
|
||||
nodesep: 42,
|
||||
ranksep: 52,
|
||||
} as const;
|
||||
|
||||
const edgeActive = (edge: { source: string; target: string }, execution: GraphExecutionPresentation): boolean =>
|
||||
execution.completedNodeIds.includes(edge.source)
|
||||
&& (execution.completedNodeIds.includes(edge.target) || execution.currentNodeId === edge.target);
|
||||
@@ -47,7 +61,7 @@ const PresentationFlowNode = ({ data }: NodeProps<Node<PresentationNodeData>>) =
|
||||
data-kind={data.kind}
|
||||
data-selected={data.selected}
|
||||
aria-pressed={data.selected}
|
||||
aria-label={`${data.label}${data.detail ? `, ${data.detail}` : ""}`}
|
||||
aria-label={`workflow node: ${data.label}${data.detail ? `, ${data.detail}` : ""}`}
|
||||
title={data.nodeRef ?? undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
@@ -73,17 +87,16 @@ const WorkflowGraphStageInner = ({
|
||||
const model = useMemo(
|
||||
() => buildWorkflowGraph(presentationWorkflowPlan, {
|
||||
direction: "LR",
|
||||
nodeWidth: 150,
|
||||
nodeHeight: 64,
|
||||
nodesep: 35,
|
||||
ranksep: 80,
|
||||
// Keep Dagre's dimensions in lockstep with the CSS node boxes so React
|
||||
// Flow's measured handles stay attached when the graph is fit to view.
|
||||
...(variant === "compact" ? compactGraphLayout : fullGraphLayout),
|
||||
label: (node) => {
|
||||
if (typeof node.label === "string") return node.label;
|
||||
if (node.id === "review_issues") return "Review issues";
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[variant],
|
||||
);
|
||||
|
||||
const nodes: Node<PresentationNodeData>[] = useMemo(
|
||||
@@ -131,6 +144,7 @@ const WorkflowGraphStageInner = ({
|
||||
aria-label="workflow graph"
|
||||
data-graph-variant={variant}
|
||||
data-graph-direction="horizontal"
|
||||
data-graph-layout="horizontal"
|
||||
>
|
||||
<div className="workflow-graph-stage__legend" aria-label="workflow graph node types">
|
||||
<span data-kind="use"><i aria-hidden="true" />Action</span>
|
||||
@@ -149,7 +163,7 @@ const WorkflowGraphStageInner = ({
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={handleNodeClick}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.02, minZoom: 0.5, maxZoom: 1 }}
|
||||
fitViewOptions={{ padding: 0.08, minZoom: 0.42, maxZoom: 1 }}
|
||||
minZoom={0.25}
|
||||
maxZoom={1.5}
|
||||
nodesDraggable={false}
|
||||
|
||||
@@ -366,6 +366,22 @@
|
||||
stroke-dasharray: 5 7;
|
||||
}
|
||||
|
||||
.workflow-graph-stage .react-flow__edge-text {
|
||||
paint-order: stroke;
|
||||
stroke: oklch(0.105 0.02 250);
|
||||
stroke-width: 0.38rem;
|
||||
stroke-linejoin: round;
|
||||
fill: var(--text-primary);
|
||||
font: 700 0.68rem/1 var(--font-mono, monospace);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.workflow-graph-stage .react-flow__edge-textbg {
|
||||
fill: oklch(0.105 0.02 250);
|
||||
stroke: oklch(0.105 0.02 250);
|
||||
}
|
||||
|
||||
.workflow-graph-stage__edge--active .react-flow__edge-path {
|
||||
stroke: var(--accent-cyan);
|
||||
stroke-width: 2.6;
|
||||
@@ -425,6 +441,23 @@
|
||||
background: var(--stage-line);
|
||||
}
|
||||
|
||||
.workflow-graph-stage__legend span[data-kind="use"] i {
|
||||
border-radius: 0.12rem;
|
||||
background: oklch(0.62 0.12 195);
|
||||
}
|
||||
|
||||
.workflow-graph-stage__legend span[data-kind="interrupt"] i {
|
||||
border-radius: 0.1rem;
|
||||
background: var(--accent-amber);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.workflow-graph-stage__legend span[data-kind="end"] i {
|
||||
width: 0.66rem;
|
||||
border-radius: 999px;
|
||||
background: oklch(0.68 0.025 250);
|
||||
}
|
||||
|
||||
.workflow-graph-stage__legend i[data-state="current"] {
|
||||
background: var(--accent-cyan);
|
||||
}
|
||||
@@ -502,12 +535,12 @@
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
width: 9.2rem;
|
||||
min-height: 4.15rem;
|
||||
width: 11rem;
|
||||
min-height: 4.75rem;
|
||||
transform: none;
|
||||
border: 1px solid var(--stage-line);
|
||||
border-radius: 0.7rem;
|
||||
padding: 0.58rem 0.68rem;
|
||||
padding: 0.68rem 0.78rem;
|
||||
background: oklch(0.15 0.025 250 / 0.96);
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
@@ -516,14 +549,14 @@
|
||||
}
|
||||
|
||||
.presentation-route .workflow-graph-stage__node[data-kind="end"] {
|
||||
min-height: 3.65rem;
|
||||
min-height: 4.5rem;
|
||||
border-radius: 999px;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workflow-graph-stage__node strong {
|
||||
font-size: 0.84rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.05;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
@@ -534,7 +567,7 @@
|
||||
}
|
||||
|
||||
.workflow-graph-stage__node small {
|
||||
font-size: 0.62rem;
|
||||
font-size: 0.67rem;
|
||||
}
|
||||
|
||||
.workflow-graph-stage__node-state {
|
||||
@@ -928,7 +961,13 @@
|
||||
}
|
||||
|
||||
.presentation-route .workflow-graph-stage__node {
|
||||
width: 7rem;
|
||||
width: 11rem;
|
||||
padding-inline: 0.78rem;
|
||||
}
|
||||
|
||||
.workflow-graph-stage[data-graph-variant="compact"] .workflow-graph-stage__node {
|
||||
width: 8.25rem;
|
||||
min-height: 4rem;
|
||||
padding-inline: 0.55rem;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user