From d5107ac6d3ff64c9611435ae5b57ca708f47200b Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Mon, 23 Mar 2026 21:33:07 +0000 Subject: [PATCH] =?UTF-8?q?review:=20address=20feedback=20=E2=80=94=20upda?= =?UTF-8?q?te=20navigation=20tree=20on=20cut/paste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When nodes with subgraphs are cut and pasted to a different graph, reparent their tree entries so the sidebar tree and breadcrumbs reflect the new hierarchy. Co-Authored-By: Claude Sonnet 4.6 --- src/components/NodeContextMenu.tsx | 17 +++++++++++++ src/stores/TreeStore.tsx | 39 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/components/NodeContextMenu.tsx b/src/components/NodeContextMenu.tsx index 2572cba..d9f481e 100644 --- a/src/components/NodeContextMenu.tsx +++ b/src/components/NodeContextMenu.tsx @@ -62,6 +62,7 @@ export default function NodeContextMenu({ const graphsById = useGraphsStore((s) => (s as { graphsById: Map }).graphsById); const addTreeNode = useGraphLayersTreeStore(store => store.add); const removeTreeNode = useGraphLayersTreeStore(store => store.remove); + const moveTreeNode = useGraphLayersTreeStore(store => store.move); const setCut = useCutStore(store => store.setCut); const clearCut = useCutStore(store => store.clearCut); @@ -165,6 +166,22 @@ export default function NodeContextMenu({ } } + // Update navigation tree: reparent tree nodes from source graph to target graph. + // A tree node belongs to the source graph when its tree parent matches sourceGraphId + // (or is a root node when sourceGraphId is 'main'). + const treeState = useGraphLayersTreeStore.getState(); + const effectiveSourceParent = cutState.sourceGraphId === 'main' ? undefined : cutState.sourceGraphId ?? undefined; + const newTreeParent = graphContextValue.graphId === 'main' ? undefined : graphContextValue.graphId; + + for (const nodeId of cutState.cutNodeIds) { + if (treeState.nodesFlatById.has(nodeId)) { + const nodeTreeParent = treeState.parentIdByChildId.get(nodeId); + if (nodeTreeParent === effectiveSourceParent) { + moveTreeNode(nodeId, newTreeParent); + } + } + } + clearCut(); break; } diff --git a/src/stores/TreeStore.tsx b/src/stores/TreeStore.tsx index bd913e8..df2ee17 100644 --- a/src/stores/TreeStore.tsx +++ b/src/stores/TreeStore.tsx @@ -10,6 +10,7 @@ export interface TreeStore { tree: TreeDataNode[]; add: (childNode: NodeContext, parentNodeId: string | undefined) => void; remove: (nodeId: string) => void; + move: (nodeId: string, newParentId: string | undefined) => void; reset: () => void; } @@ -90,6 +91,44 @@ export const useGraphLayersTreeStore = create()((set) => ({ tree: createTree([...state.rootNodes], nodesFlatById) } }), + move: (nodeId, newParentId) => set((state) => { + const node = state.nodesFlatById.get(nodeId); + if (!node) return state; + + const nodesFlatById = new Map(state.nodesFlatById); + const parentIdByChildId = new Map(state.parentIdByChildId); + let rootNodes = [...state.rootNodes]; + + // Remove from old parent + const oldParentId = state.parentIdByChildId.get(nodeId); + if (oldParentId !== undefined) { + const oldParent = nodesFlatById.get(oldParentId); + if (oldParent) { + oldParent.children = oldParent.children?.filter(n => n.key !== nodeId) ?? []; + } + parentIdByChildId.delete(nodeId); + } else { + rootNodes = rootNodes.filter(n => n.key !== nodeId); + } + + // Add to new parent + if (newParentId !== undefined) { + const newParent = nodesFlatById.get(newParentId); + if (newParent) { + newParent.children = newParent.children ? [...newParent.children, node] : [node]; + parentIdByChildId.set(nodeId, newParentId); + } + } else { + rootNodes = [...rootNodes, node]; + } + + return { + nodesFlatById, + parentIdByChildId, + rootNodes, + tree: createTree(rootNodes, nodesFlatById), + }; + }), reset: () => set({ nodesFlatById: new Map(), parentIdByChildId: new Map(),