diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 3997177b..499696cb 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -98,7 +98,8 @@ const BOX_SELECTION_EXPAND_MIN_DISTANCE = 4; const MULTI_SELECTION_BOUNDS_PADDING = 40; const GROUP_NODE_Z_INDEX = 0; const DEFAULT_NODE_Z_INDEX = 1; -const INITIAL_ACTIVE_NODE_Z_INDEX = 100; +const INITIAL_TOP_STACK_Z_INDEX = 100; +const INITIAL_TOP_MEMBER_Z_INDEX = 1; type ScreenPoint = { x: number; @@ -572,10 +573,10 @@ export function WorkflowCanvas() { const [isMiniMapOpen, setIsMiniMapOpen] = useState(false); const [zoomPercent, setZoomPercent] = useState(100); const [draggingGroupId, setDraggingGroupId] = useState(null); - const [activeNodeLayer, setActiveNodeLayer] = useState<{ nodeId: string | null; zIndex: number }>({ - nodeId: null, - zIndex: INITIAL_ACTIVE_NODE_Z_INDEX, - }); + const [stackLayerMap, setStackLayerMap] = useState>({}); + const [memberLayerMap, setMemberLayerMap] = useState>({}); + const [topStackLayer, setTopStackLayer] = useState(INITIAL_TOP_STACK_Z_INDEX); + const topMemberLayerRef = useRef(INITIAL_TOP_MEMBER_Z_INDEX); const canvasGridColor = "var(--canvas-grid)"; const miniMapMaskColor = "var(--surface-overlay)"; const miniMapNodeColor = "var(--text-disabled)"; @@ -588,18 +589,39 @@ export function WorkflowCanvas() { const gestureZoomStartRef = useRef<{ zoom: number } | null>(null); const [boxSelectionRect, setBoxSelectionRect] = useState(null); - const bringNodeToFront = useCallback((nodeId: string) => { - setActiveNodeLayer((current) => ({ - nodeId, - zIndex: current.zIndex + 1, - })); - }, []); + const bringNodeToFront = useCallback((node: Node) => { + const nodeGroupId = node.type === "group" + ? (node.data as { groupId?: string }).groupId + : (node as Node & { groupId?: string }).groupId; + const groupNodeId = nodeGroupId ? `group-node-${nodeGroupId}` : null; + + if (node.type !== "group" && nodeGroupId && groupNodeId) { + topMemberLayerRef.current += 1; + const nextMemberLayer = topMemberLayerRef.current; + setMemberLayerMap((layers) => ({ + ...layers, + [node.id]: nextMemberLayer, + })); + + setTopStackLayer((current) => { + const nextStackLayer = current + 1; + setStackLayerMap((layers) => ({ + ...layers, + [groupNodeId]: nextStackLayer, + })); + return nextStackLayer; + }); + return; + } - const resetActiveNodeLayer = useCallback(() => { - setActiveNodeLayer((current) => ({ - ...current, - nodeId: null, - })); + setTopStackLayer((current) => { + const nextStackLayer = current + 1; + setStackLayerMap((layers) => ({ + ...layers, + [node.id]: nextStackLayer, + })); + return nextStackLayer; + }); }, []); const onNodesChange = useCallback((changes: NodeChange[]) => { @@ -893,13 +915,15 @@ export function WorkflowCanvas() { // Apply visual state that React Flow can turn into DOM classes and z-index. const allNodes = useMemo(() => { const draggingGroupNodeId = draggingGroupId ? `group-node-${draggingGroupId}` : null; - const activeNode = activeNodeLayer.nodeId - ? nodes.find((node) => node.id === activeNodeLayer.nodeId) - : undefined; - const activeGroupId = activeNode?.type === "group" - ? (activeNode.data as { groupId?: string }).groupId - : undefined; - const activeGroupNodeId = activeGroupId ? `group-node-${activeGroupId}` : null; + const groupStackLayerById = new Map(); + for (const node of nodes) { + if (node.type !== "group") continue; + const groupId = (node.data as { groupId?: string }).groupId; + const stackLayer = stackLayerMap[node.id]; + if (groupId && stackLayer !== undefined) { + groupStackLayerById.set(groupId, stackLayer); + } + } return nodes.map((node) => { // Never dim Switch or ConditionalSwitch nodes themselves @@ -919,29 +943,27 @@ export function WorkflowCanvas() { // Preserve existing className if any, add/remove dimmed/skipped classes const baseClass = (node.className || "").replace(/\bswitch-dimmed\b/g, "").replace(/\bnode-skipped\b/g, "").trim(); const newClass = extraClasses ? `${baseClass} ${extraClasses}`.trim() : baseClass; - let zIndex = node.type === "group" ? GROUP_NODE_Z_INDEX : DEFAULT_NODE_Z_INDEX; - - if (activeGroupId && node.id === activeGroupNodeId) { - zIndex = activeNodeLayer.zIndex; - } else if (activeGroupId && node.type !== "group" && (node.groupId === activeGroupId || node.parentId === activeGroupNodeId)) { - zIndex = activeNodeLayer.zIndex + 1; - } else if (!activeGroupId && node.id === activeNodeLayer.nodeId) { - zIndex = activeNodeLayer.zIndex; - } + const nodeGroupId = (node as Node & { groupId?: string }).groupId; + const groupStackLayer = nodeGroupId ? groupStackLayerById.get(nodeGroupId) : undefined; + let zIndex = node.type === "group" + ? stackLayerMap[node.id] ?? GROUP_NODE_Z_INDEX + : nodeGroupId && groupStackLayer !== undefined + ? groupStackLayer + (memberLayerMap[node.id] ?? DEFAULT_NODE_Z_INDEX) + : stackLayerMap[node.id] ?? DEFAULT_NODE_Z_INDEX; if (isDraggingGroupShell) { - zIndex = Math.max(zIndex, activeNodeLayer.zIndex); + zIndex = Math.max(zIndex, topStackLayer); } else if (isDraggingGroupMember) { - zIndex = Math.max(zIndex, activeNodeLayer.zIndex + 1); + zIndex = Math.max(zIndex, topStackLayer + (memberLayerMap[node.id] ?? DEFAULT_NODE_Z_INDEX)); } else if (node.dragging) { - zIndex = Math.max(zIndex, activeNodeLayer.zIndex + 1); + zIndex = Math.max(zIndex, topStackLayer + 1); } // Only create new node object if className changed if (node.className === newClass && node.zIndex === zIndex) return node; return { ...node, className: newClass, zIndex }; }); - }, [nodes, activeNodeLayer, dimmedNodeIds, draggingGroupId, skippedNodeIds]); + }, [nodes, dimmedNodeIds, draggingGroupId, memberLayerMap, skippedNodeIds, stackLayerMap, topStackLayer]); const absoluteNodePositions = useMemo(() => { const byId = new Map(allNodes.map((node) => [node.id, node])); @@ -3078,12 +3100,12 @@ export function WorkflowCanvas() { onMoveStart={() => { isPanningRef.current = true; setHoveredNodeId(null); document.documentElement.classList.add("canvas-interacting"); }} onMoveEnd={() => { isPanningRef.current = false; document.documentElement.classList.remove("canvas-interacting"); syncZoomPercent(); }} onNodeClick={(_event, node) => { - bringNodeToFront(node.id); + bringNodeToFront(node); }} onNodeDragStart={(_event, node) => { isDraggingNodeRef.current = true; document.documentElement.classList.add("canvas-interacting"); - bringNodeToFront(node.id); + bringNodeToFront(node); setDraggingGroupId(node.type === "group" ? (node.data as { groupId?: string }).groupId ?? null : null); }} onNodeDragStop={(event, node) => { @@ -3092,7 +3114,6 @@ export function WorkflowCanvas() { setDraggingGroupId(null); handleNodeDragStop(event, node); }} - onPaneClick={resetActiveNodeLayer} onPaneContextMenu={openCanvasContextMenu} onNodeContextMenu={openNodeContextMenu} onSelectionStart={handleSelectionStart}