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

How do I save a node as a template that I can drag into the editor #316

Closed
linxianxi opened this issue Sep 22, 2021 · 24 comments
Closed

How do I save a node as a template that I can drag into the editor #316

linxianxi opened this issue Sep 22, 2021 · 24 comments

Comments

@linxianxi
Copy link
Contributor

linxianxi commented Sep 22, 2021

Describe the solution you'd like
save a node as a template that I can drag into the editor

Something like #209 needs to regenerate the ID of each node. But how do I save it in the sidebar and drag it?

The second parameter can now only be ReactElement, and it should be possible to customize the NodeTree type

connectors.create(
    ref,
    <Element />
  )

should like this

connectors.create(
    ref,
    // executed at the start of each drag to generate a new id for each node
   () => {
        const nodeTree = ...;
        const newNodeId = getRandomId();
        ......
        return nodeTree;
    }
  )
@hugominas
Copy link

@linxianxi I have an open issue with this #311 I got stuck on the node serializations which I can't access from the available actions. Its only possible to serialize the state. prob @prevwong could point us in the correct direction.

@linxianxi
Copy link
Contributor Author

linxianxi commented Sep 28, 2021

@linxianxi I have an open issue with this #311 I got stuck on the node serializations which I can't access from the available actions. Its only possible to serialize the state. prob @prevwong could point us in the correct direction.

Maybe we can do it ourselves with these two methods in the source code?
image

This way it might work, I have other things to do now and I'll try it when I have time

const tree = query.node(id).toNodeTree();
const nodePairs = Object.keys(tree.nodes).map((id) => [
  id,
  query.node(id).toSerializedNode(),
]);
console.log(JSON.stingify(fromEntries(nodePairs)));

@hugominas
Copy link

Yes, that's correct path. You need to clone the tree, so that you have new IDs, I did try serializing but it didn't work for me I guess it needs to be recursive to take care of all the child nodes and child of does child nodes.

It would be great if we could reuse the internal method

export const serializeComp = (
I will try rewriting the method for testing as soon as I have time

@linxianxi
Copy link
Contributor Author

@hugominas Of course, you need to traverse all nodes, childrens, and parents. If have the same ID, you need to generate a new ID to replace them

@hugominas
Copy link

Yap its shame, if the ids could be the same the method would be 2 lines

  const copyNodeTree = (id) => {
    if (!id) return null;
    const json = query.node(id).toSerializedNode();
    copy(lz.encodeBase64(lz.compress(JSON.stringify(json))));
  };

@hugominas
Copy link

hugominas commented Sep 28, 2021

Almost got it working, #311 (comment) , but for your use case you need to clone the tree on paste and not on copy as you will have to generate new ids everytime

@linxianxi
Copy link
Contributor Author

linxianxi commented Sep 29, 2021

@hugominas This example can save the template anywhere you want, but you can't drag it from the sidebar to the editor yet, maybe when my PR passes.

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

const fromEntries = (pairs) => {
  if (Object.fromEntries) {
    return Object.fromEntries(pairs);
  }
  return pairs.reduce(
    (accum, [id, value]) => ({
      ...accum,
      [id]: value,
    }),
    {}
  );
};

const getCloneTree = useCallback(
  (tree: NodeTree) => {
    const newNodes = {};
    const changeNodeId = (node: Node, newParentId?: string) => {
      const newNodeId = getRandomId();
      const childNodes = node.data.nodes.map((childId) =>
        changeNodeId(tree.nodes[childId], newNodeId)
      );
      const linkedNodes = Object.keys(node.data.linkedNodes).reduce(
        (acc, id) => {
          const newLinkedNodeId = changeNodeId(
            tree.nodes[node.data.linkedNodes[id]],
            newNodeId
          );
          return {
            ...acc,
            [id]: newLinkedNodeId,
          };
        },
        {}
      );

      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,
    };
  },
  [query]
);

// to save as a template
const handleSaveTemplate = useCallback(() => {
  const tree = query.node(id).toNodeTree();
  const nodePairs = Object.keys(tree.nodes).map((id) => [
    id,
    query.node(id).toSerializedNode(),
  ]);
  const serializedNodesJSON = JSON.stringify(fromEntries(nodePairs));
  const saveData = {
    rootNodeId: tree.rootNodeId,
    nodes: serializedNodesJSON,
  };
  // save to your database
  localStorage.setItem("template", JSON.stringify(saveData));
}, [id, query]);

// add templates where you want
const handleAdd = useCallback(() => {
 // get the template from your database
  const data = JSON.parse(localStorage.getItem("template"));
  const newNodes = JSON.parse(data.nodes);
  const nodePairs = Object.keys(newNodes).map((id) => {
    let nodeId = id;

    return [
      nodeId,
      query
        .parseSerializedNode(newNodes[id])
        .toNode((node) => (node.id = nodeId)),
    ];
  });
  const tree = { rootNodeId: data.rootNodeId, nodes: fromEntries(nodePairs) };
  const newTree = getCloneTree(tree);

  // add templates where you want
  actions.addNodeTree(newTree, ROOT_NODE, 0);
  actions.selectNode(newTree.rootNodeId);
}, [actions, getCloneTree, query]);

@hugominas
Copy link

Awesome, I will test it! thanks for your help

@hugominas
Copy link

Just tested and its working :D its a better solution to save the template in localstorage then copy and pasting the hash!

I added to icons to the RenderNode component to be able to copy and paste.

I had to add a timeout to refresh the sate

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

@linxianxi
Copy link
Contributor Author

@hugominas I don't know why I don't need to set setTimeout to select a node

@hugominas
Copy link

I will test it again, but the state was not updating after copying into a node Id

@hugominas
Copy link

Sorry my bad, it works correctly without the setTimeout. Glad you opened a PR this should should be present everywhere in craft, layers setting.

Thanks for your help

@Spenc84
Copy link

Spenc84 commented Dec 16, 2021

We've also found ourselves in the same position where we need a way to replicate a node tree using the drag and drop functionality. It would be really helpful if the second argument of the create method could be a callback function that returns the desired node or node tree that should get added to the layout like you suggested in your original post.

The code you listed just above seems like it would be an adequate solution if the id of the desired rootNode and desired index are already known, but if the goal is to allow a user to drag something from a sidebar to where they want it to be on the layout then I'm at a loss. Unless I've missed something, Craft doesn't seem to provide any way to identify what the parent node and index should be based off of the mouse coordinates?

@hugominas
Copy link

We have used this solution to allow for copy and paste node trees into other node trees. So a simple local storage solution works fine. If you want to create a component out of that tree you would need to store the value in a db and then present it as a user componente, so that you can drag it in.

Hope this helps, it would be a great way to have custom nodes create on the fly so let me know if you where able to do it :)

@Spenc84
Copy link

Spenc84 commented Dec 30, 2021

Although it's a bit janky, we've decided to implement a draggable button that uses craft's create method to add a simple empty div to the drop location and then uses the create method's optional callback to determine what the newly added div's index and parent id will be. We then delete that node, replicate the node tree of whatever node is actively selected in state.events.selected, update the id's and use the addNodeTree method to add the replicated tree to the parent id and index of div node we just deleted. The client won't ever see the temporary div and we're using the history throttle method to ensure that it's all done as a single step in the history log.

We would certainly appreciate a more flexible create method and are hoping that linxianxi's PR will go through, but in the meantime this has been working out pretty well for us so far.

@prevwong
Copy link
Owner

#317 Released in 0.2.0-beta.2 🎉

@xs-nayeem
Copy link

xs-nayeem commented Jan 29, 2024

@linxianxi @Spenc84 did you implement drag feature on serialized JSON? Is there any working example of do I implement this?

@linxianxi
Copy link
Contributor Author

@linxianxi @Spenc84 did you implement drag feature on serialized JSON? Is there any working example of do I implement this?

sorry, I don't remember, I haven't used the library in over two years, try this pr #317

@xs-nayeem
Copy link

@linxianxi @Spenc84 did you implement drag feature on serialized JSON? Is there any working example of do I implement this?

sorry, I don't remember, I haven't used the library in over two years, try this pr #317

I have checked this PR. Couldn't really figure out how to implement this?
image
Any kind of insight will be really helpful.

@linxianxi
Copy link
Contributor Author

linxianxi commented Jan 30, 2024

@linxianxi @Spenc84 did you implement drag feature on serialized JSON? Is there any working example of do I implement this?

sorry, I don't remember, I haven't used the library in over two years, try this pr #317

I have checked this PR. Couldn't really figure out how to implement this? image Any kind of insight will be really helpful.

run this repo and view code, this is code from two years ago https://github.com/linxianxi/page-builder

@MimMostakim
Copy link

Thanks for this repo. It works fine for single node. I can drag the single node from sidebar. But when I loop through multiple nodes, it does not work. Looking for some suggestion to drag multiple node at once.

@xs-nayeem
Copy link

xs-nayeem commented Jan 31, 2024

@linxianxi @Spenc84 did you implement drag feature on serialized JSON? Is there any working example of do I implement this?

sorry, I don't remember, I haven't used the library in over two years, try this pr #317

I have checked this PR. Couldn't really figure out how to implement this? image Any kind of insight will be really helpful.

run this repo and view code, this is code from two years ago https://github.com/linxianxi/page-builder

@linxianxi Tried your code. It works fine for a single node. I have also checked the source. It seems that it does not support for multiple nodes. Do craft js supports multiple node drag at once? How can i achieve multiple node drag at once?

@hugominas
Copy link

hugominas commented Mar 13, 2024

I am sharing an util function that takes a saved serialized node at the moment is from localstorage but you can enhance it to read it from db.

The serialized node looks something like this:

{
  rootNodeID: tree.rootNodeId,
  nodes: serializedNodesJSON,
};

How to use it

import { copyNodeTree, pasteNodeTree } from "./util";
....
<Copy
  onClick={(e) => {
    e.preventDefault();
    copyNodeTree(id, query);
  }}
/>
...
<Paste
    onClick={(e) => {
      e.preventDefault();
      pasteNodeTree(id, query, actions, node); //show pass the template when connected to a db
    }}
  />

The code in util.js

import { getRandomId } from "@craftjs/utils";
/**
 * Handle copy and past nodes
 */
const fromEntries = (pairs) => {
  if (Object.fromEntries) {
    return Object.fromEntries(pairs);
  }
  return pairs.reduce(
    (accum, [id, value]) => ({
      ...accum,
      [id]: value,
    }),
    {}
  );
};
// to copy a node
const getCloneTree = (node, query, tree) => {
  const newNodes = {};
  const changeNodeId = (node, newParentId) => {
    const newNodeId = getRandomId();
    const childNodes = node.data.nodes.map((childId) =>
      changeNodeId(tree.nodes[childId], newNodeId)
    );
    const linkedNodes = Object.keys(node.data.linkedNodes).reduce((acc, id) => {
      const newLinkedNodeId = changeNodeId(
        tree.nodes[node.data.linkedNodes[id]],
        newNodeId
      );
      return {
        ...acc,
        [id]: newLinkedNodeId,
      };
    }, {});

    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,
  };
};

// to save as a template
export const copyNodeTree = (id, query) => {
  const tree = query.node(id).toNodeTree();
  const nodePairs = Object.keys(tree.nodes).map((id) => [
    id,
    query.node(id).toSerializedNode(),
  ]);
  const serializedNodesJSON = JSON.stringify(fromEntries(nodePairs));
  const saveData = {
    rootNodeID: tree.rootNodeId,
    nodes: serializedNodesJSON,
  };
  // save to your database
  localStorage.setItem("template", JSON.stringify(saveData));
};

// add templates where you want
export const pasteNodeTree = (id, query, actions, node) => {
  // get the template from your database
  const data = JSON.parse(localStorage.getItem("template"));
  console.log("data", data);
  const newNodes = JSON.parse(data.nodes);
  const nodePairs = Object.keys(newNodes).map((id) => {
    let nodeId = id;

    return [
      nodeId,
      query
        .parseSerializedNode(newNodes[id])
        .toNode((node) => (node.id = nodeId)),
    ];
  });
  const tree = {
    rootNodeId: data.rootNodeID,
    nodes: fromEntries(nodePairs),
  };
  const newTree = getCloneTree(node, query, tree);
  // add templates where you want
  actions.addNodeTree(newTree, id, 0);
  // actions.selectNode(newTree.rootNodeId);

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

@thomassmartinez
Copy link

I'd like to know if mutating JSON is advised. I ask because I've implemented a feature to copy and paste fields from other JSON objects. However, I always generate new ids and assign them to both the parents and nodes. However, when I'm dragging a field from within a pasted group, it duplicates the node. Essentially, it doesn't remove the node from the nodes.

before

{
   "ROOT": {
      "type": {
         "resolvedName": "Container"
      },
      "isCanvas": true,
      "props": {},
      "displayName": "Container",
      "custom": {},
      "hidden": false,
      "nodes": [
         "KRI1PuP_ff"
      ],
      "linkedNodes": {}
   },
   "xkui96ghtpc": {
      "type": {
         "resolvedName": "Group"
      },
      "isCanvas": true,
      "props": {
         "name": "Agrupamento",
         "label": "parent group",
         "visible": true
      },
      "displayName": "Group",
      "custom": {},
      "parent": "KRI1PuP_ff",
      "hidden": false,
      "nodes": [
         "48pgpnx7q88"
      ],
      "linkedNodes": {}
   },
   "48pgpnx7q88": {
      "type": {
         "resolvedName": "TextField"
      },
      "isCanvas": false,
      "props": {
         "label": "text parent",
         "placeholder": "Digite aqui...",
         "type": "text",
         "name": "Texto curto",
         "visible": true
      },
      "displayName": "TextField",
      "custom": {},
      "parent": "KRI1PuP_ff",
      "hidden": false,
      "nodes": [],
      "linkedNodes": {}
   },
   "KRI1PuP_ff": {
      "type": {
         "resolvedName": "Group"
      },
      "isCanvas": true,
      "props": {
         "name": "Agrupamento",
         "label": "anyway",
         "visible": true
      },
      "displayName": "Group",
      "custom": {},
      "parent": "ROOT",
      "hidden": false,
      "nodes": [
         "xkui96ghtpc"
      ],
      "linkedNodes": {}
   }
}

After dragging the field.

{
   "ROOT": {
      "type": {
         "resolvedName": "Container"
      },
      "isCanvas": true,
      "props": {},
      "displayName": "Container",
      "custom": {},
      "hidden": false,
      "nodes": [
         "KRI1PuP_ff"
      ],
      "linkedNodes": {}
   },
   "xkui96ghtpc": {
      "type": {
         "resolvedName": "Group"
      },
      "isCanvas": true,
      "props": {
         "name": "Agrupamento",
         "label": "parent group",
         "visible": true
      },
      "displayName": "Group",
      "custom": {},
      "parent": "KRI1PuP_ff",
      "hidden": false,
      "nodes": [
         "48pgpnx7q88"
      ],
      "linkedNodes": {}
   },
   "48pgpnx7q88": {
      "type": {
         "resolvedName": "TextField"
      },
      "isCanvas": false,
      "props": {
         "label": "text parent",
         "placeholder": "Digite aqui...",
         "type": "text",
         "name": "Texto curto",
         "visible": true
      },
      "displayName": "TextField",
      "custom": {},
      "parent": "KRI1PuP_ff",
      "hidden": false,
      "nodes": [],
      "linkedNodes": {}
   },
   "KRI1PuP_ff": {
      "type": {
         "resolvedName": "Group"
      },
      "isCanvas": true,
      "props": {
         "name": "Agrupamento",
         "label": "anyway",
         "visible": true
      },
      "displayName": "Group",
      "custom": {},
      "parent": "ROOT",
      "hidden": false,
      "nodes": [
         "48pgpnx7q88",
         "xkui96ghtpc" <--- the issue is here.
      ],
      "linkedNodes": {}
   }
}

Notice that it didn't remove from the node after dragging.

image

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