FN-6024: fix built-in workflow graph edges
Ensure built-in workflow editor graphs render connected, clickable edges above swimlane backgrounds. - add explicit React Flow z-index layering so built-in workflow edges render above column bands while nodes stay interactive - keep swimlane band backgrounds translucent and preserve failure/rework edge styling for read-only built-in workflows - expand workflow editor tests to cover built-in/custom edge projection, handle rendering, and CSS edge visibility contracts - document the built-in workflow editor edge behavior and add a patch changeset for @runfusion/fusion Files changed: .changeset/FN-6024-built-in-workflow-edges.md | 5 + docs/dashboard-guide.md | 1 + packages/dashboard/app/components/WorkflowNodeEditor.css | 2 +- packages/dashboard/app/components/__tests__/WorkflowNodeEditor.css.test.ts | 25 ++++ - packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx | 106 ++++++++++++++++++++- packages/dashboard/app/components/workflow-flow-mapping.ts | 15 ++- 6 files changed, 145 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6024 Fusion-Task-Lineage: 7151d59b-b591-4642-95ca-93cc35198d41
This commit is contained in:
5
.changeset/FN-6024-built-in-workflow-edges.md
Normal file
5
.changeset/FN-6024-built-in-workflow-edges.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix built-in workflow editor graph edge visibility so read-only built-in workflows render connected, clickable React Flow edges for success, failure, and rework paths.
|
||||||
@@ -107,6 +107,7 @@ Navigation:
|
|||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
- Opens a workflow node editor with a workflow list/sidebar, canvas, inspector, and settings/authoring panels
|
- Opens a workflow node editor with a workflow list/sidebar, canvas, inspector, and settings/authoring panels
|
||||||
|
- Read-only built-in workflows are inspectable in the same canvas as custom workflows, including connected success, failure, and rework edges for their graph topology.
|
||||||
- The Settings panel is value-first for built-in workflows and groups workflow settings by Models, Review & Approval, Step Execution, and Advanced. Definitions remain available for custom workflow schema authoring.
|
- The Settings panel is value-first for built-in workflows and groups workflow settings by Models, Review & Approval, Step Execution, and Advanced. Definitions remain available for custom workflow schema authoring.
|
||||||
- The main Settings modal also exposes the default workflow's Plan/Triage, Executor, and Reviewer model lanes from **Project Models**; those controls write workflow setting values for the active default workflow.
|
- The main Settings modal also exposes the default workflow's Plan/Triage, Executor, and Reviewer model lanes from **Project Models**; those controls write workflow setting values for the active default workflow.
|
||||||
- On desktop, the editor uses a multi-panel layout for editing the graph and adjacent workflow metadata
|
- On desktop, the editor uses a multi-panel layout for editing the graph and adjacent workflow metadata
|
||||||
|
|||||||
@@ -802,7 +802,7 @@
|
|||||||
|
|
||||||
.wf-column-band {
|
.wf-column-band {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
background: var(--bg-secondary);
|
background: color-mix(in srgb, var(--bg-secondary) 65%, transparent);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,30 @@ function extractMediaBlocks(css: string, query: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findRule(blocks: string[], selector: RegExp): string {
|
function findRule(blocks: string[], selector: RegExp): string {
|
||||||
const rule = blocks.map((block) => block.match(selector)?.[0] ?? "").find(Boolean) ?? "";
|
const globalSelector = new RegExp(selector.source, selector.flags.includes("g") ? selector.flags : `${selector.flags}g`);
|
||||||
|
const matches = blocks.flatMap((block) => [...block.matchAll(globalSelector)].map((match) => match[0]));
|
||||||
|
const rule = matches.at(-1) ?? "";
|
||||||
expect(rule).toBeTruthy();
|
expect(rule).toBeTruthy();
|
||||||
return rule;
|
return rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("WorkflowNodeEditor edge visibility CSS contract", () => {
|
||||||
|
it("keeps swimlane bands translucent so built-in workflow edges remain visible", () => {
|
||||||
|
const editorCss = readComponentCss("WorkflowNodeEditor.css");
|
||||||
|
const columnBandRule = findRule([editorCss], /\.wf-column-band\s*\{[^}]*\}/);
|
||||||
|
expect(columnBandRule).toMatch(/background\s*:\s*color-mix\(in srgb, var\(--bg-secondary\) 65%, transparent\)\s*;/);
|
||||||
|
expect(columnBandRule).toMatch(/pointer-events\s*:\s*none\s*;/);
|
||||||
|
|
||||||
|
const reworkRule = findRule([editorCss], /\.wf-edge-rework \.react-flow__edge-path\s*\{[^}]*\}/);
|
||||||
|
expect(reworkRule).toMatch(/stroke\s*:\s*var\(--accent, var\(--ws-info\)\)\s*;/);
|
||||||
|
expect(reworkRule).toMatch(/stroke-dasharray\s*:\s*5 4\s*;/);
|
||||||
|
|
||||||
|
const failureRule = findRule([editorCss], /\.react-flow__edge\.wf-edge-failure \.react-flow__edge-path\s*\{[^}]*\}/);
|
||||||
|
expect(failureRule).toMatch(/stroke\s*:\s*var\(--ws-error\)\s*;/);
|
||||||
|
expect(failureRule).toMatch(/stroke-dasharray\s*:\s*2 4\s*;/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("WorkflowNodeEditor mobile CSS contract", () => {
|
describe("WorkflowNodeEditor mobile CSS contract", () => {
|
||||||
it("FN-5992 preserves desktop editor min-width while adding full-screen mobile overrides", () => {
|
it("FN-5992 preserves desktop editor min-width while adding full-screen mobile overrides", () => {
|
||||||
const baseCss = loadAllAppCssBaseOnly();
|
const baseCss = loadAllAppCssBaseOnly();
|
||||||
@@ -49,7 +68,7 @@ describe("WorkflowNodeEditor mobile CSS contract", () => {
|
|||||||
expect(editorModalRule).toMatch(/border-radius\s*:\s*0\s*;/);
|
expect(editorModalRule).toMatch(/border-radius\s*:\s*0\s*;/);
|
||||||
expect(editorModalRule).toMatch(/resize\s*:\s*none\s*;/);
|
expect(editorModalRule).toMatch(/resize\s*:\s*none\s*;/);
|
||||||
|
|
||||||
const sidebarRule = findRule(mobileBlocks, /\.wf-editor-sidebar\s*\{[^}]*\}/);
|
const sidebarRule = findRule(mobileBlocks, /\.wf-editor-body--list-stage \.wf-editor-sidebar\s*\{[^}]*\}/);
|
||||||
expect(sidebarRule).toMatch(/width\s*:\s*100%\s*;/);
|
expect(sidebarRule).toMatch(/width\s*:\s*100%\s*;/);
|
||||||
|
|
||||||
const inspectorRule = findRule(mobileBlocks, /\.wf-editor-inspector\s*\{[^}]*\}/);
|
const inspectorRule = findRule(mobileBlocks, /\.wf-editor-inspector\s*\{[^}]*\}/);
|
||||||
@@ -60,7 +79,7 @@ describe("WorkflowNodeEditor mobile CSS contract", () => {
|
|||||||
expect(settingsRule).toMatch(/min-width\s*:\s*0\s*;/);
|
expect(settingsRule).toMatch(/min-width\s*:\s*0\s*;/);
|
||||||
|
|
||||||
const canvasWrapRule = findRule(mobileBlocks, /\.wf-editor-canvas-wrap\s*\{[^}]*\}/);
|
const canvasWrapRule = findRule(mobileBlocks, /\.wf-editor-canvas-wrap\s*\{[^}]*\}/);
|
||||||
expect(canvasWrapRule).toMatch(/min-height\s*:\s*40vh\s*;/);
|
expect(canvasWrapRule).toMatch(/min-height\s*:\s*0\s*;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("FN-5992 covers create dialog and AI panel mobile overlays", () => {
|
it("FN-5992 covers create dialog and AI panel mobile overlays", () => {
|
||||||
|
|||||||
@@ -2,8 +2,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|||||||
import { render, screen, waitFor, cleanup, within } from "@testing-library/react";
|
import { render, screen, waitFor, cleanup, within } from "@testing-library/react";
|
||||||
import type { WorkflowDefinition, Settings } from "@fusion/core";
|
import type { WorkflowDefinition, Settings } from "@fusion/core";
|
||||||
import type { Agent } from "../../api";
|
import type { Agent } from "../../api";
|
||||||
import { irToFlow, flowToIr, emptyWorkflowIr, emptyWorkflowLayout, foreachChildFlowId } from "../workflow-flow-mapping";
|
import {
|
||||||
import { BUILTIN_CODING_WORKFLOW_IR, BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "@fusion/core";
|
irToFlow,
|
||||||
|
flowToIr,
|
||||||
|
emptyWorkflowIr,
|
||||||
|
emptyWorkflowLayout,
|
||||||
|
foreachChildFlowId,
|
||||||
|
WF_EDGE_INTERACTION_WIDTH,
|
||||||
|
} from "../workflow-flow-mapping";
|
||||||
|
import {
|
||||||
|
BUILTIN_CODING_WORKFLOW_IR,
|
||||||
|
BUILTIN_PR_WORKFLOW_IR,
|
||||||
|
BUILTIN_STEPWISE_CODING_WORKFLOW_IR,
|
||||||
|
BUILTIN_WORKFLOWS,
|
||||||
|
} from "@fusion/core";
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("../../api", () => ({
|
||||||
fetchWorkflows: vi.fn(),
|
fetchWorkflows: vi.fn(),
|
||||||
@@ -133,6 +145,36 @@ function builtinDef(): WorkflowDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function builtinPrDef(): WorkflowDefinition {
|
||||||
|
return {
|
||||||
|
id: "builtin:pr-workflow",
|
||||||
|
kind: "workflow",
|
||||||
|
name: "PR lifecycle (built-in)",
|
||||||
|
description: "Ships with Fusion",
|
||||||
|
ir: BUILTIN_PR_WORKFLOW_IR,
|
||||||
|
layout: {},
|
||||||
|
createdAt: "2026-06-03T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-06-03T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function edgeRenderableAssertion(definition: WorkflowDefinition) {
|
||||||
|
const flow = irToFlow(definition);
|
||||||
|
const nodeIds = new Set(flow.nodes.map((node) => node.id));
|
||||||
|
expect(flow.edges.length, `${definition.id} should project edges`).toBeGreaterThan(0);
|
||||||
|
for (const edge of flow.edges) {
|
||||||
|
expect(nodeIds.has(edge.source), `${definition.id} edge ${edge.id} source ${edge.source}`).toBe(true);
|
||||||
|
expect(nodeIds.has(edge.target), `${definition.id} edge ${edge.id} target ${edge.target}`).toBe(true);
|
||||||
|
expect(edge.interactionWidth, `${definition.id} edge ${edge.id} interaction width`).toBe(
|
||||||
|
WF_EDGE_INTERACTION_WIDTH,
|
||||||
|
);
|
||||||
|
expect(edge.zIndex, `${definition.id} edge ${edge.id} z-index`).toBeGreaterThan(0);
|
||||||
|
expect(edge.sourceHandle, `${definition.id} edge ${edge.id} source handle`).toBeUndefined();
|
||||||
|
expect(edge.targetHandle, `${definition.id} edge ${edge.id} target handle`).toBeUndefined();
|
||||||
|
}
|
||||||
|
return flow;
|
||||||
|
}
|
||||||
|
|
||||||
function fragmentDef(): WorkflowDefinition {
|
function fragmentDef(): WorkflowDefinition {
|
||||||
return {
|
return {
|
||||||
id: "WF-FRAG",
|
id: "WF-FRAG",
|
||||||
@@ -203,6 +245,37 @@ describe("workflow-flow-mapping", () => {
|
|||||||
expect(ir.edges).toEqual([{ from: "start", to: "end", condition: "success" }]);
|
expect(ir.edges).toEqual([{ from: "start", to: "end", condition: "success" }]);
|
||||||
expect(emptyWorkflowLayout().start).toBeDefined();
|
expect(emptyWorkflowLayout().start).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("projects every built-in workflow to connected, clickable React Flow edges", () => {
|
||||||
|
expect(BUILTIN_WORKFLOWS.map((workflow) => workflow.id).sort()).toEqual(
|
||||||
|
expect.arrayContaining(["builtin:coding", "builtin:stepwise-coding", "builtin:pr-workflow"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const workflow of BUILTIN_WORKFLOWS) {
|
||||||
|
edgeRenderableAssertion(workflow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps built-in edge endpoints connected when layout is empty, undefined, or populated", () => {
|
||||||
|
const populated = BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:pr-workflow");
|
||||||
|
expect(populated).toBeDefined();
|
||||||
|
edgeRenderableAssertion(populated!);
|
||||||
|
edgeRenderableAssertion({ ...populated!, layout: {} });
|
||||||
|
edgeRenderableAssertion({ ...populated!, layout: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects custom v1 and v2 workflows to the same connected edge contract", () => {
|
||||||
|
edgeRenderableAssertion(def());
|
||||||
|
edgeRenderableAssertion(v2Def());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves duplicate and parallel built-in edges with valid endpoints and hit targets", () => {
|
||||||
|
const { edges } = edgeRenderableAssertion(builtinDef());
|
||||||
|
const failuresToEnd = edges.filter((edge) => edge.target === "end" && edge.data?.condition === "failure");
|
||||||
|
expect(failuresToEnd.map((edge) => edge.source).sort()).toEqual(["execute", "merge", "review"]);
|
||||||
|
expect(new Set(failuresToEnd.map((edge) => edge.id)).size).toBe(failuresToEnd.length);
|
||||||
|
expect(failuresToEnd.every((edge) => edge.interactionWidth === WF_EDGE_INTERACTION_WIDTH)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("WorkflowNodeEditor", () => {
|
describe("WorkflowNodeEditor", () => {
|
||||||
@@ -767,6 +840,7 @@ describe("WorkflowNodeEditor — built-in stepwise selection render path", () =>
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(fetchTraits).mockResolvedValue(TRAIT_CATALOG);
|
vi.mocked(fetchTraits).mockResolvedValue(TRAIT_CATALOG);
|
||||||
vi.mocked(fetchStepParsers).mockResolvedValue(["step-headings", "json-steps"]);
|
vi.mocked(fetchStepParsers).mockResolvedValue(["step-headings", "json-steps"]);
|
||||||
|
vi.mocked(fetchModels).mockResolvedValue({ models: [] });
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -799,6 +873,31 @@ describe("WorkflowNodeEditor — built-in stepwise selection render path", () =>
|
|||||||
expect(flowNodeIds).toContain(foreachChildFlowId("steps", "step-done"));
|
expect(flowNodeIds).toContain(foreachChildFlowId("steps", "step-done"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders built-in and custom workflow nodes with handles matching projected edges", async () => {
|
||||||
|
vi.mocked(fetchWorkflows).mockResolvedValue([builtinPrDef(), v2Def()]);
|
||||||
|
const { container } = render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||||
|
|
||||||
|
await screen.findByTestId("wf-readonly-banner");
|
||||||
|
await waitFor(() => expect(screen.getAllByTestId("wf-node-hold").length).toBeGreaterThan(0));
|
||||||
|
const builtInFlow = irToFlow(builtinPrDef());
|
||||||
|
for (const edge of builtInFlow.edges.filter((candidate) => candidate.source === "pr-create")) {
|
||||||
|
expect(container.querySelector(`.react-flow__handle[data-nodeid="${edge.source}"][data-handlepos="right"]`)).toBeInTheDocument();
|
||||||
|
expect(container.querySelector(`.react-flow__handle[data-nodeid="${edge.target}"][data-handlepos="left"]`)).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
expect(builtInFlow.edges.some((edge) => edge.label === "open")).toBe(true);
|
||||||
|
expect(builtInFlow.edges.some((edge) => edge.label === "failed")).toBe(true);
|
||||||
|
expect(builtInFlow.edges.some((edge) => edge.label === "failure")).toBe(true);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Custom"));
|
||||||
|
await waitFor(() => expect(screen.queryByTestId("wf-readonly-banner")).not.toBeInTheDocument());
|
||||||
|
const customFlow = irToFlow(v2Def());
|
||||||
|
for (const edge of customFlow.edges) {
|
||||||
|
expect(container.querySelector(`.react-flow__handle[data-nodeid="${edge.source}"][data-handlepos="right"]`)).toBeInTheDocument();
|
||||||
|
expect(container.querySelector(`.react-flow__handle[data-nodeid="${edge.target}"][data-handlepos="left"]`)).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
expect(customFlow.edges.every((edge) => edge.label === "success")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("irToFlow on the built-in stepwise IR yields a foreach group + rework-styled template edge (editor load path)", () => {
|
it("irToFlow on the built-in stepwise IR yields a foreach group + rework-styled template edge (editor load path)", () => {
|
||||||
// Mirrors exactly what the editor's load effect feeds React Flow:
|
// Mirrors exactly what the editor's load effect feeds React Flow:
|
||||||
// const flow = irToFlow(activeWorkflow)
|
// const flow = irToFlow(activeWorkflow)
|
||||||
@@ -817,6 +916,7 @@ describe("WorkflowNodeEditor — built-in stepwise selection render path", () =>
|
|||||||
// its rework styling so the editor shows the bounded loop-back.
|
// its rework styling so the editor shows the bounded loop-back.
|
||||||
const reworkEdges = edges.filter((e) => e.data?.kind === "rework");
|
const reworkEdges = edges.filter((e) => e.data?.kind === "rework");
|
||||||
expect(reworkEdges.length).toBeGreaterThan(0);
|
expect(reworkEdges.length).toBeGreaterThan(0);
|
||||||
|
expect(group?.zIndex).toBeLessThan(reworkEdges[0]?.zIndex ?? 0);
|
||||||
expect(reworkEdges.every((e) => e.animated === true && e.className === "wf-edge-rework")).toBe(true);
|
expect(reworkEdges.every((e) => e.animated === true && e.className === "wf-edge-rework")).toBe(true);
|
||||||
expect(reworkEdges.some((e) => e.source === foreachChildFlowId("steps", "step-review"))).toBe(true);
|
expect(reworkEdges.some((e) => e.source === foreachChildFlowId("steps", "step-review"))).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -864,7 +964,7 @@ describe("WorkflowNodeEditor — built-in stepwise selection render path", () =>
|
|||||||
fireEvent.click(executeNode!);
|
fireEvent.click(executeNode!);
|
||||||
|
|
||||||
expect((screen.getByLabelText("Prompt") as HTMLTextAreaElement).value).toContain(
|
expect((screen.getByLabelText("Prompt") as HTMLTextAreaElement).value).toContain(
|
||||||
"Fusion's standard implementation prompt",
|
"task execution agent",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ export const COLUMN_BAND_HEIGHT = 220;
|
|||||||
export const COLUMN_BAND_WIDTH = 5000;
|
export const COLUMN_BAND_WIDTH = 5000;
|
||||||
export const COLUMN_BAND_X = -40;
|
export const COLUMN_BAND_X = -40;
|
||||||
export const COLUMN_BAND_TOP = 0;
|
export const COLUMN_BAND_TOP = 0;
|
||||||
|
/** Layering: swimlane/template groups sit below routed edges; step cards sit above. */
|
||||||
|
const WF_BACKGROUND_GROUP_Z_INDEX = 0;
|
||||||
|
const WF_EDGE_Z_INDEX = 1;
|
||||||
|
const WF_STEP_NODE_Z_INDEX = 2;
|
||||||
/** React Flow node id for a column band group node. */
|
/** React Flow node id for a column band group node. */
|
||||||
export const columnBandNodeId = (columnId: string): string => `__col__:${columnId}`;
|
export const columnBandNodeId = (columnId: string): string => `__col__:${columnId}`;
|
||||||
export const isColumnBandNode = (id: string): boolean => id.startsWith("__col__:");
|
export const isColumnBandNode = (id: string): boolean => id.startsWith("__col__:");
|
||||||
@@ -153,8 +157,8 @@ export function columnsToBandNodes(columns: WorkflowIrColumn[]): FlowNode<Workfl
|
|||||||
draggable: false,
|
draggable: false,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
deletable: false,
|
deletable: false,
|
||||||
// Bands sit behind step nodes so steps remain clickable/draggable.
|
// Bands sit behind routed edges and step nodes so built-in topology stays visible.
|
||||||
zIndex: -1,
|
zIndex: WF_BACKGROUND_GROUP_Z_INDEX,
|
||||||
style: {
|
style: {
|
||||||
width: COLUMN_BAND_WIDTH,
|
width: COLUMN_BAND_WIDTH,
|
||||||
height: COLUMN_BAND_HEIGHT,
|
height: COLUMN_BAND_HEIGHT,
|
||||||
@@ -212,6 +216,7 @@ function irEdgeToFlow(edge: WorkflowIrEdge, index: number, idScope = ""): FlowEd
|
|||||||
className: edgeClassName(condition, isRework),
|
className: edgeClassName(condition, isRework),
|
||||||
interactionWidth: WF_EDGE_INTERACTION_WIDTH,
|
interactionWidth: WF_EDGE_INTERACTION_WIDTH,
|
||||||
markerEnd: undefined,
|
markerEnd: undefined,
|
||||||
|
zIndex: WF_EDGE_Z_INDEX,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +274,7 @@ export function irToFlow(def: WorkflowDefinition): {
|
|||||||
extent: "parent",
|
extent: "parent",
|
||||||
data: { kind: innerKind, label: nodeLabel(inner), config: { ...(inner.config ?? {}) } },
|
data: { kind: innerKind, label: nodeLabel(inner), config: { ...(inner.config ?? {}) } },
|
||||||
deletable: true,
|
deletable: true,
|
||||||
|
zIndex: WF_STEP_NODE_Z_INDEX,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
template.edges.forEach((edge, eIdx) => {
|
template.edges.forEach((edge, eIdx) => {
|
||||||
@@ -289,6 +295,7 @@ export function irToFlow(def: WorkflowDefinition): {
|
|||||||
},
|
},
|
||||||
style: { width: FOREACH_GROUP_WIDTH, height: FOREACH_GROUP_HEIGHT },
|
style: { width: FOREACH_GROUP_WIDTH, height: FOREACH_GROUP_HEIGHT },
|
||||||
deletable: true,
|
deletable: true,
|
||||||
|
zIndex: WF_BACKGROUND_GROUP_Z_INDEX,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +310,7 @@ export function irToFlow(def: WorkflowDefinition): {
|
|||||||
column,
|
column,
|
||||||
},
|
},
|
||||||
deletable: node.kind !== "start" && node.kind !== "end",
|
deletable: node.kind !== "start" && node.kind !== "end",
|
||||||
|
zIndex: WF_STEP_NODE_Z_INDEX,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -941,6 +949,7 @@ function irNodeToFlowNode(
|
|||||||
position,
|
position,
|
||||||
data: { kind, label: nodeLabel(node), config: { ...(node.config ?? {}) } },
|
data: { kind, label: nodeLabel(node), config: { ...(node.config ?? {}) } },
|
||||||
deletable: node.kind !== "start" && node.kind !== "end",
|
deletable: node.kind !== "start" && node.kind !== "end",
|
||||||
|
zIndex: WF_STEP_NODE_Z_INDEX,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,6 +1027,7 @@ export function insertFragment(
|
|||||||
extent: "parent",
|
extent: "parent",
|
||||||
data: { kind: innerKind, label: nodeLabel(inner), config: { ...(inner.config ?? {}) } },
|
data: { kind: innerKind, label: nodeLabel(inner), config: { ...(inner.config ?? {}) } },
|
||||||
deletable: true,
|
deletable: true,
|
||||||
|
zIndex: WF_STEP_NODE_Z_INDEX,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
template.edges.forEach((edge, eIdx) => {
|
template.edges.forEach((edge, eIdx) => {
|
||||||
@@ -1037,6 +1047,7 @@ export function insertFragment(
|
|||||||
},
|
},
|
||||||
style: { width: FOREACH_GROUP_WIDTH, height: FOREACH_GROUP_HEIGHT },
|
style: { width: FOREACH_GROUP_WIDTH, height: FOREACH_GROUP_HEIGHT },
|
||||||
deletable: true,
|
deletable: true,
|
||||||
|
zIndex: WF_BACKGROUND_GROUP_Z_INDEX,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return irNodeToFlowNode(node, id, pos);
|
return irNodeToFlowNode(node, id, pos);
|
||||||
|
|||||||
Reference in New Issue
Block a user