Skip to content

Commit e36e164

Browse files
committed
feat: Add zIndex Management for all Nodes
1 parent cd0d7e8 commit e36e164

6 files changed

Lines changed: 186 additions & 1 deletion

File tree

knip.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
"src/components/ui/**",
88
"src/components/shared/StatusFilterSelect/**",
99
"openapi-ts.config.ts",
10-
"vite.config.ghpages.js"
10+
"vite.config.ghpages.js",
11+
"src/components/shared/ReactFlow/FlowCanvas/utils/zIndex.ts",
12+
"src/components/shared/ReactFlow/FlowControls/StackingControls.tsx",
13+
"src/components/shared/ReactFlow/FlowCanvas/types.ts"
1114
],
1215
"ignoreDependencies": [
1316
"@radix-ui/react-accordion",

react-compiler.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const REACT_COMPILER_ENABLED_DIRS = [
4848
"src/components/shared/TaskDetails/DisplayNameEditor.tsx",
4949
"src/components/shared/TaskDetails/Actions/UnpackSubgraphButton.tsx",
5050
"src/components/shared/ReactFlow/FlowSidebar/components/ComponentHoverPopover.tsx",
51+
"src/components/shared/ReactFlow/FlowControls/StackingControls.tsx",
5152

5253
// 11-20 useCallback/useMemo
5354
// "src/components/ui", // 12

src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,7 @@ const FlowCanvas = ({
990990
connectOnClick={!readOnly}
991991
connectionLineComponent={ConnectionLine}
992992
proOptions={{ hideAttribution: true }}
993+
zIndexMode="manual"
993994
className={cn(
994995
(rest.selectionOnDrag || (shiftKeyPressed && !isConnecting)) &&
995996
"cursor-crosshair",

src/components/shared/ReactFlow/FlowCanvas/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export const edgeTypes: Record<string, ComponentType<any>> = {
2626
customEdge: SmoothEdge,
2727
};
2828

29+
export function isDefinedNode(node: Node): node is Node & { type: NodeType } {
30+
return !!node.type && node.type in nodeTypes;
31+
}
32+
2933
export function isTaskNodeType(type: string): type is TaskType {
3034
return type === "task" || type === "input" || type === "output";
3135
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { type Node } from "@xyflow/react";
2+
3+
import { isDefinedNode, type NodeType } from "../types";
4+
5+
type ZIndexDefinition = {
6+
min: number;
7+
max: number;
8+
default: number;
9+
};
10+
11+
export const Z_INDEX_RANGES: Record<NodeType, ZIndexDefinition> = {
12+
task: {
13+
min: -100,
14+
max: 100,
15+
default: 0,
16+
},
17+
input: {
18+
min: -100,
19+
max: 100,
20+
default: 0,
21+
},
22+
output: {
23+
min: -100,
24+
max: 100,
25+
default: 0,
26+
},
27+
ghost: {
28+
min: 100,
29+
max: 100,
30+
default: 100,
31+
},
32+
};
33+
34+
const getNodeZIndexProp = (
35+
node: Node,
36+
prop: keyof ZIndexDefinition,
37+
): number => {
38+
if (!isDefinedNode(node)) {
39+
return 0;
40+
}
41+
42+
return Z_INDEX_RANGES[node.type][prop];
43+
};
44+
45+
const getAllNodeZIndices = (nodes: Node[]): number[] => {
46+
return nodes
47+
.map((node) => node.zIndex ?? getNodeZIndexProp(node, "default"))
48+
.sort((a, b) => a - b);
49+
};
50+
51+
export const bringToFront = (node: Node, allNodes: Node[]): number => {
52+
const allZIndices = getAllNodeZIndices(allNodes);
53+
const maxZ = Math.max(...allZIndices, getNodeZIndexProp(node, "default"));
54+
const nodeMax = getNodeZIndexProp(node, "max");
55+
return Math.min(maxZ + 1, nodeMax);
56+
};
57+
58+
export const sendToBack = (node: Node, allNodes: Node[]): number => {
59+
const allZIndices = getAllNodeZIndices(allNodes);
60+
const minZ = Math.min(...allZIndices, getNodeZIndexProp(node, "default"));
61+
const nodeMin = getNodeZIndexProp(node, "min");
62+
return Math.max(minZ - 1, nodeMin);
63+
};
64+
65+
export const moveForward = (node: Node, allNodes: Node[]): number => {
66+
const currentZ = node.zIndex ?? getNodeZIndexProp(node, "default");
67+
const allZIndices = getAllNodeZIndices(allNodes);
68+
const nodeMax = getNodeZIndexProp(node, "max");
69+
70+
const higherIndices = allZIndices.filter((z) => z > currentZ);
71+
if (higherIndices.length === 0) {
72+
return currentZ;
73+
}
74+
75+
const nextZ = Math.min(...higherIndices);
76+
return Math.min(nextZ === currentZ + 1 ? nextZ + 1 : nextZ, nodeMax);
77+
};
78+
79+
export const moveBackward = (node: Node, allNodes: Node[]): number => {
80+
const currentZ = node.zIndex ?? getNodeZIndexProp(node, "default");
81+
const allZIndices = getAllNodeZIndices(allNodes);
82+
const nodeMin = getNodeZIndexProp(node, "min");
83+
84+
const lowerIndices = allZIndices.filter((z) => z < currentZ);
85+
if (lowerIndices.length === 0) {
86+
return currentZ;
87+
}
88+
89+
const prevZ = Math.max(...lowerIndices);
90+
return Math.max(prevZ === currentZ - 1 ? prevZ - 1 : prevZ, nodeMin);
91+
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useReactFlow } from "@xyflow/react";
2+
3+
import { Icon } from "@/components/ui/icon";
4+
import { InlineStack } from "@/components/ui/layout";
5+
6+
import TooltipButton from "../../Buttons/TooltipButton";
7+
import {
8+
bringToFront,
9+
moveBackward,
10+
moveForward,
11+
sendToBack,
12+
} from "../FlowCanvas/utils/zIndex";
13+
14+
export const StackingControls = ({
15+
nodeId,
16+
onChange,
17+
}: {
18+
nodeId: string;
19+
onChange: (newZIndex: number) => void;
20+
}) => {
21+
const { getNodes } = useReactFlow();
22+
23+
const updateZIndex = (
24+
operation: "front" | "back" | "forward" | "backward",
25+
) => {
26+
const nodes = getNodes();
27+
const currentNode = nodes.find((n) => n.id === nodeId);
28+
if (!currentNode) return;
29+
30+
let newZIndex: number;
31+
switch (operation) {
32+
case "front":
33+
newZIndex = bringToFront(currentNode, nodes);
34+
break;
35+
case "back":
36+
newZIndex = sendToBack(currentNode, nodes);
37+
break;
38+
case "forward":
39+
newZIndex = moveForward(currentNode, nodes);
40+
break;
41+
case "backward":
42+
newZIndex = moveBackward(currentNode, nodes);
43+
break;
44+
}
45+
46+
onChange(newZIndex);
47+
};
48+
49+
return (
50+
<InlineStack gap="2">
51+
<TooltipButton
52+
size="sm"
53+
variant="outline"
54+
onClick={() => updateZIndex("forward")}
55+
tooltip="Move Forward"
56+
>
57+
<Icon name="ArrowUpFromLine" />
58+
</TooltipButton>
59+
<TooltipButton
60+
size="sm"
61+
variant="outline"
62+
onClick={() => updateZIndex("backward")}
63+
tooltip="Move Backward"
64+
>
65+
<Icon name="ArrowDownFromLine" />
66+
</TooltipButton>
67+
<TooltipButton
68+
size="sm"
69+
variant="outline"
70+
onClick={() => updateZIndex("front")}
71+
tooltip="Bring to Front"
72+
>
73+
<Icon name="ListStart" />
74+
</TooltipButton>
75+
<TooltipButton
76+
size="sm"
77+
variant="outline"
78+
onClick={() => updateZIndex("back")}
79+
tooltip="Send to Back"
80+
>
81+
<Icon name="ListEnd" />
82+
</TooltipButton>
83+
</InlineStack>
84+
);
85+
};

0 commit comments

Comments
 (0)