fix(canvas): cancel-nest restores position; un-nest shrinks parent
Two follow-up polish items for drag-and-nest: 1. Cancelling the "Extract from team?" dialog now snaps the dragged card back to where the drag started. Before, a user who dragged a child out, saw the confirm dialog, then clicked Cancel ended up with the card stranded outside the parent at its drop-point position — which also got persisted via savePosition on drag-stop. Now onNodeDragStart captures the pre-drag position + parent, and cancelNest restores both the RF node position and fires savePosition with the absolute pre-drag coords so reload matches. 2. Un-nesting now clears the ex-parent's explicit width/height in the nodes array. growParentsToFitChildren is grow-only so it could never shrink the parent back down after a child left; the card stayed at its auto-grown size with empty space. Stripping width/height lets React Flow re-measure from the card's own min-width / min-height CSS, so the parent visually shrinks to fit whatever children remain. 923 canvas tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
512fdfd59d
commit
09053dfdeb
@ -56,6 +56,16 @@ export function useDragHandlers(): DragHandlers {
|
||||
alt: false,
|
||||
meta: false,
|
||||
});
|
||||
// Remember where the dragged node started so we can put it back on
|
||||
// cancel. React Flow tracks only the current position during drag;
|
||||
// if the user drags out → "Extract?" dialog → Cancel, we want the
|
||||
// card to go back inside its parent at its original coords rather
|
||||
// than stay dangling at the cancel-time position.
|
||||
const dragStartStateRef = useRef<{
|
||||
nodeId: string;
|
||||
parentId: string | null;
|
||||
position: { x: number; y: number };
|
||||
} | null>(null);
|
||||
const [pendingNest, setPendingNest] = useState<PendingNestState | null>(null);
|
||||
|
||||
// Absolute-bounds hit test. Tiebreakers in order: highest zIndex
|
||||
@ -102,11 +112,16 @@ export function useDragHandlers(): DragHandlers {
|
||||
);
|
||||
|
||||
const onNodeDragStart: OnNodeDrag<WorkspaceNode> = useCallback(
|
||||
(event) => {
|
||||
(event, node) => {
|
||||
dragModifiersRef.current = {
|
||||
alt: event.altKey,
|
||||
meta: event.metaKey || event.ctrlKey,
|
||||
};
|
||||
dragStartStateRef.current = {
|
||||
nodeId: node.id,
|
||||
parentId: node.data.parentId,
|
||||
position: { x: node.position.x, y: node.position.y },
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
@ -196,6 +211,7 @@ export function useDragHandlers(): DragHandlers {
|
||||
// showToast, so `void` is the right pattern here.
|
||||
const pending = pendingNest;
|
||||
setPendingNest(null);
|
||||
dragStartStateRef.current = null;
|
||||
const state = useCanvasStore.getState();
|
||||
if (
|
||||
state.selectedNodeIds.size > 1 &&
|
||||
@ -207,7 +223,44 @@ export function useDragHandlers(): DragHandlers {
|
||||
}
|
||||
}, [pendingNest, nestNode, batchNest]);
|
||||
|
||||
const cancelNest = useCallback(() => setPendingNest(null), []);
|
||||
const cancelNest = useCallback(() => {
|
||||
// Restore the dragged card to wherever it started. Without this,
|
||||
// a user who drags a child out of a parent then clicks Cancel
|
||||
// leaves the card stranded outside the parent with no visual
|
||||
// parent link — a state that doesn't match any save-backed
|
||||
// truth (the DB position was already written on drag-stop).
|
||||
const start = dragStartStateRef.current;
|
||||
if (start) {
|
||||
const { nodes } = useCanvasStore.getState();
|
||||
useCanvasStore.setState({
|
||||
nodes: nodes.map((n) =>
|
||||
n.id === start.nodeId
|
||||
? { ...n, position: start.position }
|
||||
: n,
|
||||
),
|
||||
});
|
||||
// Write the restore back to the DB so a reload shows the same
|
||||
// position. Convert the stored relative position back to absolute
|
||||
// via the parent's absolute origin before saving.
|
||||
const parent = start.parentId
|
||||
? nodes.find((n) => n.id === start.parentId)
|
||||
: null;
|
||||
const parentInternal = start.parentId
|
||||
? getInternalNode(start.parentId)
|
||||
: null;
|
||||
const parentAbs = parentInternal?.internals.positionAbsolute ?? {
|
||||
x: parent?.position.x ?? 0,
|
||||
y: parent?.position.y ?? 0,
|
||||
};
|
||||
savePosition(
|
||||
start.nodeId,
|
||||
start.position.x + parentAbs.x,
|
||||
start.position.y + parentAbs.y,
|
||||
);
|
||||
}
|
||||
dragStartStateRef.current = null;
|
||||
setPendingNest(null);
|
||||
}, [getInternalNode, savePosition]);
|
||||
|
||||
return {
|
||||
onNodeDragStart,
|
||||
|
||||
@ -653,6 +653,14 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
}
|
||||
}
|
||||
|
||||
// When a child leaves its parent, clear the parent's explicit
|
||||
// width/height. growParentsToFitChildren is grow-only so it can't
|
||||
// shrink on its own; without this, a parent that auto-grew to
|
||||
// contain the dragged child stays at that size after un-nest,
|
||||
// leaving a large empty frame. React Flow then measures the new
|
||||
// size from the card's own min-width/min-height CSS.
|
||||
const shrinkOldParent = !!currentParentId && targetId !== currentParentId;
|
||||
|
||||
set({
|
||||
nodes: nodes.map((n) => {
|
||||
if (n.id === draggedId) {
|
||||
@ -664,6 +672,11 @@ export const useCanvasStore = create<CanvasState>((set, get) => ({
|
||||
data: { ...n.data, parentId: targetId },
|
||||
};
|
||||
}
|
||||
if (shrinkOldParent && n.id === currentParentId) {
|
||||
const { width: _w, height: _h, ...rest } = n;
|
||||
void _w; void _h;
|
||||
return rest as typeof n;
|
||||
}
|
||||
if (movedIds.has(n.id) && depthDelta !== 0) {
|
||||
return { ...n, zIndex: (n.zIndex ?? 0) + depthDelta };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user