Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clone element work only after serialize and deserialize #209

Open
nicosh opened this issue Mar 3, 2021 · 8 comments
Open

Clone element work only after serialize and deserialize #209

nicosh opened this issue Mar 3, 2021 · 8 comments

Comments

@nicosh
Copy link

nicosh commented Mar 3, 2021

Describe the bug
A little bit of context, in #144 the first solution for cloning elements causes linked cloned nodes to change props to the original ones, #144 (comment) seems to have the same problem, but actually the nodes ids are changed and if we deserialize a serialized state editor inside a setTimeout function it works.
To Reproduce
Steps to reproduce the behavior:

  1. Go to https://codesandbox.io/s/epic-colden-giwsw
  2. Select a button, on the settings panel under delete click clone
  3. Try to changes some props to the cloned element
  4. You can see that props are changed to the original one.
  5. Now uncomment line 87 on settings panel https://codesandbox.io/s/epic-colden-giwsw?file=/components/SettingsPanel.js:2258-2268
  6. Now the clone function works

Expected behavior
Cloning an element and run actions.addNodeTree should load the correct data.

@PurviJha
Copy link

PurviJha commented Mar 7, 2021

This is awsome solution to our problem but there is a bug i face .After deserializing the json, drag and drop stopped working It has unexpected behaviour .while checking the official example(basic one) when click on lode state it deserialize the json and drag and drop stopped working while switch on and off enable button drag and drop started working .May be this is not the solution but its a quick fix

@nicosh
Copy link
Author

nicosh commented Mar 7, 2021

@Purvicapternal
Instead clicking the button you can use actions.setHidden, i use this function :

   const handleClone = (e)=>{
    e.preventDefault()
    const parentNode = query.node(parent).get();
    const indexToAdd = parentNode.data.nodes.indexOf(id)
    const tree = getCloneTree(id,parentNode.id);
    actions.setHidden("ROOT",true)
    actions.addNodeTree(tree, parentNode.id ,indexToAdd+1);  
    setTimeout(
      function() {
        actions.deserialize(query.serialize())
        actions.setHidden("ROOT",false)
        actions.selectNode(tree.rootNodeId);
        let cnode =  query.node(tree.rootNodeId).get();
        cnode.dom.scrollIntoView()
      }, 100);
  }

@nicosh
Copy link
Author

nicosh commented Mar 10, 2021

Here is a working function for cloning elements (sandbox) :

  const getCloneTree = useCallback((idToClone) => {
   const tree = query.node(idToClone).toNodeTree();
   const newNodes = {};

   const changeNodeId = (node, newParentId) => {
     const newNodeId = shortid();
     const childNodes = node.data.nodes.map((childId) => changeNodeId(tree.nodes[childId], newNodeId));
     const linkedNodes = Object.keys(node.data.linkedNodes).reduce(
       (accum, id) => {
         const newNodeId = changeNodeId(
           tree.nodes[node.data.linkedNodes[id]],
           newNodeId,
         );
         return {
           ...accum,
           [id]: newNodeId,
         };
       },
       {},
     );

     let tmpNode = {
       ...node,
       id: newNodeId,
       data: {
         ...node.data,
         parent: newParentId || node.data.parent,
         nodes: childNodes,
         linkedNodes,
       },
     };
     let freshnode = query.parseFreshNode(tmpNode).toNode()
     newNodes[newNodeId] = freshnode
     return newNodeId;
   };

   const rootNodeId = changeNodeId(tree.nodes[tree.rootNodeId]);
   return {
     rootNodeId,
     nodes: newNodes,
   };
 }, []);

The problem was this piece of code that i guess did not update the internal state :

      newNodes[newNodeId] = {
        ...node,
        id: newNodeId,
        data: {
          ...node.data,
          parent: newParentId || node.data.parent,
          nodes: childNodes,
          linkedNodes
        }
      };

While using query.parseFreshNode seems to work fine :

      let tmpNode = {
        ...node,
        id: newNodeId,
        data: {
          ...node.data,
          parent: newParentId || node.data.parent,
          nodes: childNodes,
          linkedNodes,
        },
      };
      let freshnode = query.parseFreshNode(tmpNode).toNode()

@hugominas
Copy link

hugominas commented Mar 10, 2021

Thanks @nicosh for the heads up and for sharing your solution! Just tested on two staging websites and everything works flawlessly. After node clone and after state save and load. Top!

@linxianxi
Copy link
Contributor

Now, do not use shortid, should use nanoid, like this.

import { getRandomId } from "@craftjs/utils";

const newNodeId =  getRandomId();

@luc-hn
Copy link

luc-hn commented Nov 19, 2021

const copyNode = (node: Node, newId: string) => {
    const newNode: Node = {
      ...node,
      id: newId,
      events: {
        dragged: false,
        hovered: false,
        selected: false,
      },
    };
    return query.parseFreshNode(newNode).toNode();
  };

  const duplicateNode = async (
    node: Node,
    parentId: string,
    index?: number,
  ) => {
    if (!node || !parentId) return;
    const newId = generateRandomId(10);
    const newNode = copyNode(node, newId);
    newNode.data.nodes = [];
    // Need to create parent node before create child node
    await actions.history.throttle().add(newNode, parentId, index);
    // add child nodes to the copy node
    node.data.nodes.forEach((childNodeId) => {
      const childNode = query.node(childNodeId).get();
      const newChildNode = copyNode(childNode, generateRandomId(10));
      duplicateNode(newChildNode, newId);
    });
  };

@Govind2210
Copy link

This is the Update Version where I have used uuidv4 to generate the unique ID:-

    const tree = query.node(idToClone).toNodeTree();
    const newNodes = {};

    const changeNodeId = (node, newParentId) => {
      const newNodeId = uuidv4();

      const childNodes = node.data.nodes.map((childId: any) =>
        changeNodeId(tree.nodes[childId], newNodeId)
      );

      const linkedNodes = Object.keys(node.data.linkedNodes).reduce(
        (accum, id) => {
          const linkedNodeId = changeNodeId(
            tree.nodes[node.data.linkedNodes[id]],
            newNodeId
          );
          return {
            ...accum,
            [id]: linkedNodeId
          };
        },
        {}
      );

      newNodes[newNodeId] = {
        ...node,
        id: newNodeId,
        data: {
          ...node.data,
          parent: newParentId || node.data.parent,
          nodes: childNodes,
          linkedNodes
        }
      };
      return newNodeId;
    };

    const rootNodeId = changeNodeId(tree.nodes[tree.rootNodeId], "");
    return {
      rootNodeId,
      nodes: newNodes
    };
  }, []);

  const handleClone = (e: any, id: any) => {
    e.preventDefault();
    const theNode = query.node(id).get();
    const parentNode = query.node(theNode.data.parent).get();
    const indexToAdd = parentNode.data.nodes.indexOf(id);
    const tree = getCloneTree(id);
    actions.addNodeTree(tree, parentNode.id, indexToAdd + 1);

    setTimeout(function () {
      actions.deserialize(query.serialize());
      actions.selectNode(tree.rootNodeId);
    }, 100);
  };```

@solidsnail
Copy link

Here is a simple version without recursion:

import { Node, NodeTree, useEditor } from "@craftjs/core";

export const cloneNodeTree = (
  node: Node,
  query: ReturnType<typeof useEditor>["query"],
  actions: ReturnType<typeof useEditor>["actions"]
) => {
  const parentId = node.data.parent;
  if (!parentId) {
    throw "No parent id found !";
  }
  const nodeIndex = query.node(parentId).get().data.nodes.indexOf(node.id);
  const oldTree = query.node(node.id).toNodeTree();
  const treeIds = Object.keys(oldTree.nodes).map((oldId) => ({
    oldId,
    newId: "" + Math.round(Math.random() * 100000),
  }));

  let newTreeJson = JSON.stringify(oldTree, (key, value) => {
    if (["dom", "rules"].includes(key)) {
      return undefined;
    }
    return value;
  });

  for (const treeId of treeIds) {
    newTreeJson = newTreeJson.replaceAll(treeId.oldId, treeId.newId);
  }
  const newTree = JSON.parse(newTreeJson) as NodeTree;
  for (const treeId of treeIds) {
    newTree.nodes[treeId.newId].rules = oldTree.nodes[treeId.oldId].rules;
    newTree.nodes[treeId.newId].data.type =
      oldTree.nodes[treeId.oldId].data.type;
    newTree.nodes[treeId.newId].events = {
      dragged: false,
      hovered: false,
      selected: false,
    };
  }
  actions.addNodeTree(newTree, parentId, nodeIndex + 1);
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants