1a88baa95d
regenerateTreeIds shallow-copied each node's data, leaving data.props (and data.custom) as the same object reference between the original node and its duplicate/pasted copy. Craft.js's setProp mutates data.props in place, so editing the duplicate's props silently mutated the original too. Deep-clone data via structuredClone before applying id remaps so no mutable sub-object is shared between original and regenerated nodes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
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);
|
|
}
|
|
|
|
// Deep-clone data so mutable sub-objects (props, custom, etc.) are never
|
|
// shared by reference between the original node and its regenerated
|
|
// copy. Craft.js's setProp mutates data.props in place, so a shallow
|
|
// copy here would let edits to the duplicate silently corrupt the
|
|
// original.
|
|
const clonedData = structuredClone(oldNode.data);
|
|
|
|
const newNode: Node = {
|
|
...oldNode,
|
|
id: newId,
|
|
data: {
|
|
...clonedData,
|
|
parent: newParent,
|
|
nodes: (oldNode.data.nodes || []).map(remapId),
|
|
linkedNodes: newLinkedNodes,
|
|
},
|
|
};
|
|
|
|
newNodes[newId] = newNode;
|
|
}
|
|
|
|
return {
|
|
rootNodeId: remapId(tree.rootNodeId),
|
|
nodes: newNodes,
|
|
};
|
|
}
|