fix(builder): deep-clone node data on id regeneration to avoid shared props
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>
This commit is contained in:
@@ -129,4 +129,21 @@ describe('regenerateTreeIds', () => {
|
|||||||
regenerateTreeIds(input);
|
regenerateTreeIds(input);
|
||||||
expect(JSON.parse(JSON.stringify(input))).toEqual(snapshot);
|
expect(JSON.parse(JSON.stringify(input))).toEqual(snapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('output node data.props is a deep clone, not shared with the input node', () => {
|
||||||
|
const input = makeTree();
|
||||||
|
input.nodes['root-1'].data.props = { alignment: 'left' };
|
||||||
|
|
||||||
|
const output = regenerateTreeIds(input);
|
||||||
|
const outputRoot = output.nodes[output.rootNodeId];
|
||||||
|
|
||||||
|
// Clone must preserve content at the time of cloning.
|
||||||
|
expect(outputRoot.data.props).toEqual({ alignment: 'left' });
|
||||||
|
|
||||||
|
// Mutating the output's props (simulating Craft.js's in-place setProp)
|
||||||
|
// must NOT affect the input node's props object.
|
||||||
|
(outputRoot.data.props as { alignment: string }).alignment = 'right';
|
||||||
|
|
||||||
|
expect(input.nodes['root-1'].data.props).toEqual({ alignment: 'left' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,11 +43,18 @@ export function regenerateTreeIds(tree: NodeTree): NodeTree {
|
|||||||
newLinkedNodes[slot] = remapId(linkedId);
|
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 = {
|
const newNode: Node = {
|
||||||
...oldNode,
|
...oldNode,
|
||||||
id: newId,
|
id: newId,
|
||||||
data: {
|
data: {
|
||||||
...oldNode.data,
|
...clonedData,
|
||||||
parent: newParent,
|
parent: newParent,
|
||||||
nodes: (oldNode.data.nodes || []).map(remapId),
|
nodes: (oldNode.data.nodes || []).map(remapId),
|
||||||
linkedNodes: newLinkedNodes,
|
linkedNodes: newLinkedNodes,
|
||||||
|
|||||||
Reference in New Issue
Block a user