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:
@@ -3,6 +3,7 @@ import { useEditor } from '@craftjs/core';
|
||||
import { findDeletableTarget } from '../../utils/craft-helpers';
|
||||
import { useSitesmithModal } from '../../state/SitesmithContext';
|
||||
import { buildSitesmithTarget } from '../../utils/sitesmith-target';
|
||||
import { regenerateTreeIds } from '../../utils/craft-tree';
|
||||
|
||||
interface ContextMenuProps {
|
||||
visible: boolean;
|
||||
@@ -65,15 +66,11 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
const duplicate = useCallback(() => {
|
||||
if (!nodeId || nodeId === 'ROOT') return;
|
||||
try {
|
||||
const tree = query.node(nodeId).toSerializedNode();
|
||||
const parentId = getParentId();
|
||||
if (!parentId) return;
|
||||
|
||||
// Get the full subtree
|
||||
const freshTree = query.node(nodeId).toNodeTree();
|
||||
const clonedTree = query.parseSerializedNode(freshTree.nodes[freshTree.rootNodeId].data).toNode();
|
||||
|
||||
actions.addNodeTree(freshTree, parentId);
|
||||
const tree = regenerateTreeIds(query.node(nodeId).toNodeTree());
|
||||
actions.addNodeTree(tree, parentId);
|
||||
} catch (e) {
|
||||
console.error('Duplicate failed:', e);
|
||||
}
|
||||
@@ -92,10 +89,25 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
|
||||
const pasteNode = useCallback(() => {
|
||||
const sourceId = clipboardRef.current;
|
||||
if (!sourceId) return;
|
||||
if (!sourceId) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const targetParent = nodeId || 'ROOT';
|
||||
const tree = query.node(sourceId).toNodeTree();
|
||||
if (!query.node(sourceId).get()) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Paste as a SIBLING of the right-clicked node, not as its child --
|
||||
// using the clicked node itself as the parent throws when it's a leaf.
|
||||
let targetParent = 'ROOT';
|
||||
if (nodeId && nodeId !== 'ROOT') {
|
||||
const clickedNode = query.node(nodeId).get();
|
||||
targetParent = clickedNode?.data?.parent || 'ROOT';
|
||||
}
|
||||
|
||||
const tree = regenerateTreeIds(query.node(sourceId).toNodeTree());
|
||||
actions.addNodeTree(tree, targetParent);
|
||||
} catch (e) {
|
||||
console.error('Paste failed:', e);
|
||||
|
||||
Reference in New Issue
Block a user