Browse Source

fix: use deterministic positions for group membership check on header drag

The previous implementation called getNodes() in handlePointerUp which
could return stale pre-drag positions in React Flow's controlled mode,
causing nodes to remain in groups after being dragged out. Also only
checked the header node, not all selected nodes moved together.

Now calculates final positions from drag delta (startPos + clientDelta/zoom)
and iterates all entries in startPositions to check group membership for
every moved node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
b377f89e86
  1. 74
      src/components/nodes/FloatingNodeHeader.tsx

74
src/components/nodes/FloatingNodeHeader.tsx

@ -263,50 +263,56 @@ export function FloatingNodeHeader({
} }
}; };
const handlePointerUp = () => { const handlePointerUp = (e: PointerEvent) => {
const wasDragging = isDraggingRef.current; const wasDragging = isDraggingRef.current;
document.removeEventListener('pointermove', handlePointerMove); document.removeEventListener('pointermove', handlePointerMove);
document.removeEventListener('pointerup', handlePointerUp); document.removeEventListener('pointerup', handlePointerUp);
isDraggingRef.current = false; isDraggingRef.current = false;
// Check group membership if the node was actually dragged // Check group membership for ALL moved nodes
if (wasDragging) { if (wasDragging) {
const store = useWorkflowStore.getState(); const store = useWorkflowStore.getState();
const allNodes = getNodes(); const { zoom } = getViewport();
const draggedNode = allNodes.find(n => n.id === id); const dx = (e.clientX - startX) / zoom;
const dy = (e.clientY - startY) / zoom;
if (!draggedNode) return;
for (const [nodeId, startPos] of startPositions) {
// Get node dimensions (prefer measured, fallback to defaults) // Calculate final position deterministically from drag delta
const defaults = defaultNodeDimensions[type] || { width: 300, height: 280 }; const finalX = startPos.x + dx;
const nodeWidth = draggedNode.measured?.width || (draggedNode.style?.width as number) || defaults.width; const finalY = startPos.y + dy;
const nodeHeight = draggedNode.measured?.height || (draggedNode.style?.height as number) || defaults.height;
// Get node dimensions from store (always fresh)
// Calculate node center const storeNode = store.nodes.find(n => n.id === nodeId);
const nodeCenterX = draggedNode.position.x + nodeWidth / 2; if (!storeNode) continue;
const nodeCenterY = draggedNode.position.y + nodeHeight / 2;
const nodeType = storeNode.type as NodeType;
// Check if node center is inside any group const defaults = defaultNodeDimensions[nodeType] || { width: 300, height: 280 };
let targetGroupId: string | undefined; const nodeWidth = storeNode.measured?.width || (storeNode.style?.width as number) || defaults.width;
const nodeHeight = storeNode.measured?.height || (storeNode.style?.height as number) || defaults.height;
for (const group of Object.values(store.groups)) {
const inBoundsX = nodeCenterX >= group.position.x && nodeCenterX <= group.position.x + group.size.width; // Calculate node center
const inBoundsY = nodeCenterY >= group.position.y && nodeCenterY <= group.position.y + group.size.height; const nodeCenterX = finalX + nodeWidth / 2;
const nodeCenterY = finalY + nodeHeight / 2;
if (inBoundsX && inBoundsY) {
targetGroupId = group.id; // Check if node center is inside any group
break; let targetGroupId: string | undefined;
for (const group of Object.values(store.groups)) {
const inBoundsX = nodeCenterX >= group.position.x && nodeCenterX <= group.position.x + group.size.width;
const inBoundsY = nodeCenterY >= group.position.y && nodeCenterY <= group.position.y + group.size.height;
if (inBoundsX && inBoundsY) {
targetGroupId = group.id;
break;
}
} }
}
// Get the node's current groupId // Update groupId if it changed
const currentNode = store.nodes.find((n) => n.id === id); const currentGroupId = storeNode.groupId;
const currentGroupId = currentNode?.groupId; if (targetGroupId !== currentGroupId) {
store.setNodeGroupId(nodeId, targetGroupId);
// Update groupId if it changed }
if (targetGroupId !== currentGroupId) {
store.setNodeGroupId(id, targetGroupId);
} }
} }
}; };

Loading…
Cancel
Save