feat: support horizontal workflow graph layout
This commit is contained in:
@@ -95,6 +95,35 @@ describe("buildWorkflowGraph", () => {
|
|||||||
expect(okEdge?.label).toBe("ok");
|
expect(okEdge?.label).toBe("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the default layout top-to-bottom", () => {
|
||||||
|
const model = buildWorkflowGraph(samplePlan);
|
||||||
|
const open = model.nodes.find((node) => node.id === "open");
|
||||||
|
const end = model.nodes.find((node) => node.id === "__end__");
|
||||||
|
|
||||||
|
expect(open?.position.y).toBeLessThan(end?.position.y ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports horizontal layout options without changing edge labels", () => {
|
||||||
|
const model = buildWorkflowGraph(samplePlan, {
|
||||||
|
direction: "LR",
|
||||||
|
nodeWidth: 190,
|
||||||
|
nodeHeight: 72,
|
||||||
|
nodesep: 55,
|
||||||
|
ranksep: 100,
|
||||||
|
});
|
||||||
|
const open = model.nodes.find((node) => node.id === "open");
|
||||||
|
const end = model.nodes.find((node) => node.id === "__end__");
|
||||||
|
|
||||||
|
expect(open?.position.x).toBeLessThan(end?.position.x ?? 0);
|
||||||
|
expect(model.edges.map((edge) => edge.label)).toEqual([
|
||||||
|
"ok",
|
||||||
|
"ok",
|
||||||
|
"true",
|
||||||
|
"false",
|
||||||
|
"approved",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("assigns deterministic coordinates", () => {
|
it("assigns deterministic coordinates", () => {
|
||||||
const model1 = buildWorkflowGraph(samplePlan);
|
const model1 = buildWorkflowGraph(samplePlan);
|
||||||
const model2 = buildWorkflowGraph(structuredClone(samplePlan));
|
const model2 = buildWorkflowGraph(structuredClone(samplePlan));
|
||||||
|
|||||||
@@ -36,8 +36,21 @@ export type WorkflowGraphModel = {
|
|||||||
readonly edges: ReadonlyArray<WorkflowGraphEdge>;
|
readonly edges: ReadonlyArray<WorkflowGraphEdge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NODE_WIDTH = 180;
|
export type WorkflowGraphLayoutOptions = {
|
||||||
const NODE_HEIGHT = 60;
|
readonly direction?: "TB" | "LR";
|
||||||
|
readonly nodeWidth?: number;
|
||||||
|
readonly nodeHeight?: number;
|
||||||
|
readonly nodesep?: number;
|
||||||
|
readonly ranksep?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_LAYOUT: Required<WorkflowGraphLayoutOptions> = {
|
||||||
|
direction: "TB",
|
||||||
|
nodeWidth: 180,
|
||||||
|
nodeHeight: 60,
|
||||||
|
nodesep: 50,
|
||||||
|
ranksep: 80,
|
||||||
|
};
|
||||||
|
|
||||||
const mapNodeKind = (type: string): WorkflowGraphNodeKind => {
|
const mapNodeKind = (type: string): WorkflowGraphNodeKind => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -88,17 +101,26 @@ export const buildWorkflowGraph = (
|
|||||||
nodes: ReadonlyArray<Record<string, unknown>>;
|
nodes: ReadonlyArray<Record<string, unknown>>;
|
||||||
edges: ReadonlyArray<Record<string, unknown>>;
|
edges: ReadonlyArray<Record<string, unknown>>;
|
||||||
},
|
},
|
||||||
|
options: WorkflowGraphLayoutOptions = {},
|
||||||
): WorkflowGraphModel => {
|
): WorkflowGraphModel => {
|
||||||
|
const layout = { ...DEFAULT_LAYOUT, ...options };
|
||||||
const sortedNodes = [...plan.nodes].sort((a, b) =>
|
const sortedNodes = [...plan.nodes].sort((a, b) =>
|
||||||
String(a.id).localeCompare(String(b.id)),
|
String(a.id).localeCompare(String(b.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const g = new dagre.graphlib.Graph();
|
const g = new dagre.graphlib.Graph();
|
||||||
g.setDefaultEdgeLabel(() => ({}));
|
g.setDefaultEdgeLabel(() => ({}));
|
||||||
g.setGraph({ rankdir: "TB", nodesep: 50, ranksep: 80 });
|
g.setGraph({
|
||||||
|
rankdir: layout.direction,
|
||||||
|
nodesep: layout.nodesep,
|
||||||
|
ranksep: layout.ranksep,
|
||||||
|
});
|
||||||
|
|
||||||
for (const node of sortedNodes) {
|
for (const node of sortedNodes) {
|
||||||
g.setNode(String(node.id), { width: NODE_WIDTH, height: NODE_HEIGHT });
|
g.setNode(String(node.id), {
|
||||||
|
width: layout.nodeWidth,
|
||||||
|
height: layout.nodeHeight,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const edge of plan.edges) {
|
for (const edge of plan.edges) {
|
||||||
@@ -119,7 +141,10 @@ export const buildWorkflowGraph = (
|
|||||||
nodeRef: (node.node as string | null) ?? null,
|
nodeRef: (node.node as string | null) ?? null,
|
||||||
raw: node as Record<string, unknown>,
|
raw: node as Record<string, unknown>,
|
||||||
},
|
},
|
||||||
position: { x: pos.x - NODE_WIDTH / 2, y: pos.y - NODE_HEIGHT / 2 },
|
position: {
|
||||||
|
x: pos.x - layout.nodeWidth / 2,
|
||||||
|
y: pos.y - layout.nodeHeight / 2,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user