fix(builder): regenerate node ids on duplicate/paste to prevent state corruption
Craft.js duplicate (ContextMenu + keyboard shortcut) and paste were reusing the original node's toNodeTree() output verbatim, so addNodeTree() inserted duplicate node ids into the editor tree. Added regenerateTreeIds() which deep-clones a NodeTree and remaps rootNodeId, node map keys, node.id, internal node.data.parent, node.data.nodes, and node.data.linkedNodes via Craft.js's own getRandomId(). Also fixed pasteNode to insert as a sibling of the right-clicked node (using its parent) instead of using a leaf node as the new parent, which previously threw. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import type { NodeTree, Node, NodeId } from '@craftjs/core';
|
||||
import { getRandomId } from '@craftjs/utils';
|
||||
|
||||
/**
|
||||
* Returns a deep-cloned NodeTree where every node id in the tree
|
||||
* (rootNodeId, the `nodes` map keys, each `node.id`, each internal
|
||||
* `node.data.parent`, every `node.data.nodes` entry, and every
|
||||
* `node.data.linkedNodes` value) has been remapped to a fresh,
|
||||
* collision-free id generated by Craft.js's own id generator.
|
||||
*
|
||||
* The subtree root's `data.parent` is deliberately left untouched --
|
||||
* `actions.addNodeTree(tree, parentId)` sets that itself when the tree is
|
||||
* attached to its new parent.
|
||||
*
|
||||
* The input tree is not mutated.
|
||||
*/
|
||||
export function regenerateTreeIds(tree: NodeTree): NodeTree {
|
||||
const oldIds = Object.keys(tree.nodes);
|
||||
|
||||
// Build old -> new id map first so all cross-references can be rewritten
|
||||
// consistently regardless of iteration order.
|
||||
const idMap = new Map<NodeId, NodeId>();
|
||||
for (const oldId of oldIds) {
|
||||
idMap.set(oldId, getRandomId());
|
||||
}
|
||||
|
||||
const remapId = (id: NodeId): NodeId => idMap.get(id) ?? id;
|
||||
|
||||
const newNodes: Record<NodeId, Node> = {};
|
||||
for (const oldId of oldIds) {
|
||||
const oldNode = tree.nodes[oldId];
|
||||
const newId = idMap.get(oldId)!;
|
||||
|
||||
const newParent =
|
||||
oldId === tree.rootNodeId
|
||||
? oldNode.data.parent // subtree root's parent is set by the caller
|
||||
: oldNode.data.parent !== null && oldNode.data.parent !== undefined
|
||||
? remapId(oldNode.data.parent)
|
||||
: oldNode.data.parent;
|
||||
|
||||
const newLinkedNodes: Record<string, NodeId> = {};
|
||||
for (const [slot, linkedId] of Object.entries(oldNode.data.linkedNodes || {})) {
|
||||
newLinkedNodes[slot] = remapId(linkedId);
|
||||
}
|
||||
|
||||
const newNode: Node = {
|
||||
...oldNode,
|
||||
id: newId,
|
||||
data: {
|
||||
...oldNode.data,
|
||||
parent: newParent,
|
||||
nodes: (oldNode.data.nodes || []).map(remapId),
|
||||
linkedNodes: newLinkedNodes,
|
||||
},
|
||||
};
|
||||
|
||||
newNodes[newId] = newNode;
|
||||
}
|
||||
|
||||
return {
|
||||
rootNodeId: remapId(tree.rootNodeId),
|
||||
nodes: newNodes,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user