mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 13:00:04 +08:00
fix(canvas): correct z-index layering for selected blocks and connected edges (#3698)
* fix(canvas): correct z-index layering for selected blocks and connected edges * fix(canvas): derive subflow edge z-index from connected node z-index * fix(canvas): fix nodesForRender early-return guard for regular blocks * lint * fix(canvas): ensure elevated edges and last-interacted nodes sit above siblings at same base z-index
This commit is contained in:
@@ -201,8 +201,6 @@ const edgeTypes: EdgeTypes = {
|
||||
const defaultEdgeOptions = { type: 'custom' }
|
||||
|
||||
const reactFlowStyles = [
|
||||
'[&_.react-flow__edges]:!z-0',
|
||||
'[&_.react-flow__node]:z-[21]',
|
||||
'[&_.react-flow__handle]:!z-[30]',
|
||||
'[&_.react-flow__edge-labels]:!z-[1001]',
|
||||
'[&_.react-flow__pane]:select-none',
|
||||
@@ -2478,6 +2476,7 @@ const WorkflowContent = React.memo(
|
||||
|
||||
// Local state for nodes - allows smooth drag without store updates on every frame
|
||||
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
|
||||
const [lastInteractedNodeId, setLastInteractedNodeId] = useState<string | null>(null)
|
||||
|
||||
const selectedNodeIds = useMemo(
|
||||
() => displayNodes.filter((node) => node.selected).map((node) => node.id),
|
||||
@@ -2489,6 +2488,14 @@ const WorkflowContent = React.memo(
|
||||
syncPanelWithSelection(selectedNodeIds)
|
||||
}, [selectedNodeIdsKey])
|
||||
|
||||
// Keep the most recently selected block on top even after deselection, so a
|
||||
// dragged block doesn't suddenly drop behind other overlapping blocks.
|
||||
useEffect(() => {
|
||||
if (selectedNodeIds.length > 0) {
|
||||
setLastInteractedNodeId(selectedNodeIds[selectedNodeIds.length - 1])
|
||||
}
|
||||
}, [selectedNodeIdsKey])
|
||||
|
||||
useEffect(() => {
|
||||
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
|
||||
if (pendingSelection && pendingSelection.length > 0) {
|
||||
@@ -3723,18 +3730,58 @@ const WorkflowContent = React.memo(
|
||||
[removeEdge, edges, blocks, addNotification, activeWorkflowId]
|
||||
)
|
||||
|
||||
// Elevate nodes using React Flow's native zIndex so selected/recent blocks
|
||||
// always sit above edges and other blocks.
|
||||
//
|
||||
// Z-index layers (regular blocks):
|
||||
// 21 — default
|
||||
// 22 — last interacted (dragged/selected, now deselected) so it stays on
|
||||
// top of siblings until another block is touched
|
||||
// 31 — currently selected (above connected edges at z-22 and handles at z-30)
|
||||
//
|
||||
// Subflow container nodes are skipped — they use depth-based zIndex for
|
||||
// correct parent/child layering and must not be bumped.
|
||||
// Child blocks inside containers already carry zIndex 1000 and are bumped by
|
||||
// +10 when selected so they stay above their sibling child blocks.
|
||||
const nodesForRender = useMemo(() => {
|
||||
return displayNodes.map((node) => {
|
||||
if (node.type === 'subflowNode') return node
|
||||
const base = node.zIndex ?? 21
|
||||
const target = node.selected
|
||||
? base + 10
|
||||
: node.id === lastInteractedNodeId
|
||||
? Math.max(base + 1, 22)
|
||||
: base
|
||||
if (target === (node.zIndex ?? 21)) return node
|
||||
return { ...node, zIndex: target }
|
||||
})
|
||||
}, [displayNodes, lastInteractedNodeId])
|
||||
|
||||
/** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */
|
||||
const edgesWithSelection = useMemo(() => {
|
||||
const nodeMap = new Map(displayNodes.map((n) => [n.id, n]))
|
||||
const elevatedNodeIdSet = new Set(
|
||||
lastInteractedNodeId ? [...selectedNodeIds, lastInteractedNodeId] : selectedNodeIds
|
||||
)
|
||||
|
||||
return edgesForDisplay.map((edge) => {
|
||||
const sourceNode = nodeMap.get(edge.source)
|
||||
const targetNode = nodeMap.get(edge.target)
|
||||
const parentLoopId = sourceNode?.parentId || targetNode?.parentId
|
||||
const edgeContextId = `${edge.id}${parentLoopId ? `-${parentLoopId}` : ''}`
|
||||
const connectedToElevated =
|
||||
elevatedNodeIdSet.has(edge.source) || elevatedNodeIdSet.has(edge.target)
|
||||
// Derive elevated z-index from connected nodes so edges inside subflows
|
||||
// (child nodes at z-1000) stay above their sibling child blocks.
|
||||
const elevatedZIndex = Math.max(
|
||||
22,
|
||||
(sourceNode?.zIndex ?? 21) + 1,
|
||||
(targetNode?.zIndex ?? 21) + 1
|
||||
)
|
||||
|
||||
return {
|
||||
...edge,
|
||||
zIndex: connectedToElevated ? elevatedZIndex : 0,
|
||||
data: {
|
||||
...edge.data,
|
||||
isSelected: selectedEdges.has(edgeContextId),
|
||||
@@ -3745,7 +3792,14 @@ const WorkflowContent = React.memo(
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [edgesForDisplay, displayNodes, selectedEdges, handleEdgeDelete])
|
||||
}, [
|
||||
edgesForDisplay,
|
||||
displayNodes,
|
||||
selectedNodeIds,
|
||||
selectedEdges,
|
||||
handleEdgeDelete,
|
||||
lastInteractedNodeId,
|
||||
])
|
||||
|
||||
/** Handles Delete/Backspace to remove selected edges or blocks. */
|
||||
useEffect(() => {
|
||||
@@ -3885,7 +3939,7 @@ const WorkflowContent = React.memo(
|
||||
{showTrainingModal && <TrainingModal />}
|
||||
|
||||
<ReactFlow
|
||||
nodes={displayNodes}
|
||||
nodes={nodesForRender}
|
||||
edges={edgesWithSelection}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
@@ -3952,7 +4006,7 @@ const WorkflowContent = React.memo(
|
||||
onNodeDragStart={effectivePermissions.canEdit ? onNodeDragStart : undefined}
|
||||
snapToGrid={snapToGrid}
|
||||
snapGrid={snapGrid}
|
||||
elevateEdgesOnSelect={true}
|
||||
elevateEdgesOnSelect={false}
|
||||
onlyRenderVisibleElements={false}
|
||||
deleteKeyCode={null}
|
||||
elevateNodesOnSelect={false}
|
||||
|
||||
Reference in New Issue
Block a user