Parcourir la source

Merge pull request #304 from hanbin9775/feature/dot-notation-on-edge

#257 Getting path of JSON on clicking the edges
Aykut Saraç il y a 2 ans
Parent
commit
9216180019

+ 54 - 8
src/components/Graph/index.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
-import { Canvas, Edge, ElkRoot } from "reaflow";
+import { Canvas, Edge, ElkRoot, getParentsForNodeId } from "reaflow";
 import { CustomNode } from "src/components/CustomNode";
 import useGraph from "src/store/useGraph";
 import useUser from "src/store/useUser";
@@ -11,8 +11,7 @@ import { PremiumView } from "./PremiumView";
 
 interface GraphProps {
   isWidget?: boolean;
-  openModal: () => void;
-  setSelectedNode: (node: [string, string][]) => void;
+  openNodeModal: () => void;
 }
 
 const StyledEditorWrapper = styled.div<{ widget: boolean }>`
@@ -48,11 +47,12 @@ const StyledEditorWrapper = styled.div<{ widget: boolean }>`
   }
 `;
 
-const GraphComponent = ({ isWidget = false, openModal, setSelectedNode }: GraphProps) => {
+const GraphComponent = ({ isWidget = false, openNodeModal }: GraphProps) => {
   const isPremium = useUser(state => state.isPremium);
   const setLoading = useGraph(state => state.setLoading);
   const setZoomPanPinch = useGraph(state => state.setZoomPanPinch);
   const centerView = useGraph(state => state.centerView);
+  const setSelectedNode = useGraph(state => state.setSelectedNode);
 
   const loading = useGraph(state => state.loading);
   const direction = useGraph(state => state.direction);
@@ -65,11 +65,57 @@ const GraphComponent = ({ isWidget = false, openModal, setSelectedNode }: GraphP
   });
 
   const handleNodeClick = React.useCallback(
-    (e: React.MouseEvent<SVGElement>, data: NodeData) => {
-      if (setSelectedNode) setSelectedNode(data.text);
-      if (openModal) openModal();
+    (_: React.MouseEvent<SVGElement>, data: NodeData) => {
+      let resolvedPath = "";
+      const parentIds = getParentsForNodeId(nodes, edges, data.id).map(n => n.id);
+      const path = parentIds.reverse().concat(data.id);
+      const rootArrayElementIds = ["1"];
+      const edgesMap = new Map();
+
+      for (const edge of edges) {
+        if (!edgesMap.has(edge.from!)) {
+          edgesMap.set(edge.from!, []);
+        }
+        edgesMap.get(edge.from!).push(edge.to);
+      }
+
+      for (let i = 1; i < edges.length; i++) {
+        const curNodeId = edges[i].from!;
+        if (rootArrayElementIds.includes(curNodeId)) continue;
+        if (!edgesMap.has(curNodeId)) {
+          rootArrayElementIds.push(curNodeId);
+        }
+      }
+
+      if (rootArrayElementIds.length > 1) {
+        resolvedPath += `Root[${rootArrayElementIds.findIndex(id => id === path[0])}]`;
+      } else {
+        resolvedPath += "{Root}";
+      }
+
+      for (let i = 1; i < path.length; i++) {
+        const curId = path[i];
+        const curNode = nodes[+curId - 1];
+
+        if (!curNode) break;
+        if (curNode.data.parent === "array") {
+          resolvedPath += `.${curNode.text}`;
+          if (i !== path.length - 1) {
+            const toNodeId = path[i + 1];
+            const idx = edgesMap.get(curId).indexOf(toNodeId);
+            resolvedPath += `[${idx}]`;
+          }
+        }
+
+        if (curNode.data.parent === "object") {
+          resolvedPath += `.${curNode.text}`;
+        }
+      }
+
+      if (setSelectedNode) setSelectedNode({ node: data.text, path: resolvedPath });
+      if (openNodeModal) openNodeModal();
     },
-    [openModal, setSelectedNode]
+    [edges, nodes, openNodeModal, setSelectedNode]
   );
 
   const onInit = React.useCallback(

+ 4 - 9
src/containers/Editor/LiveEditor/GraphCanvas.tsx

@@ -4,13 +4,12 @@ import { NodeModal } from "src/containers/Modals/NodeModal";
 import useGraph from "src/store/useGraph";
 
 export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
-  const [isModalVisible, setModalVisible] = React.useState(false);
-  const [selectedNode, setSelectedNode] = React.useState<[string, string][]>([]);
+  const [isNodeModalVisible, setNodeModalVisible] = React.useState(false);
 
   const collapsedNodes = useGraph(state => state.collapsedNodes);
   const collapsedEdges = useGraph(state => state.collapsedEdges);
 
-  const openModal = React.useCallback(() => setModalVisible(true), []);
+  const openNodeModal = React.useCallback(() => setNodeModalVisible(true), []);
 
   React.useEffect(() => {
     const nodeList = collapsedNodes.map(id => `[id$="node-${id}"]`);
@@ -32,12 +31,8 @@ export const GraphCanvas = ({ isWidget = false }: { isWidget?: boolean }) => {
 
   return (
     <>
-      <Graph openModal={openModal} setSelectedNode={setSelectedNode} isWidget={isWidget} />
-      <NodeModal
-        selectedNode={selectedNode}
-        visible={isModalVisible}
-        closeModal={() => setModalVisible(false)}
-      />
+      <Graph openNodeModal={openNodeModal} isWidget={isWidget} />
+      <NodeModal visible={isNodeModalVisible} closeModal={() => setNodeModalVisible(false)} />
     </>
   );
 };

+ 43 - 8
src/containers/Modals/NodeModal/index.tsx

@@ -6,32 +6,52 @@ import vs from "react-syntax-highlighter/dist/cjs/styles/prism/vs";
 import vscDarkPlus from "react-syntax-highlighter/dist/cjs/styles/prism/vsc-dark-plus";
 import { Button } from "src/components/Button";
 import { Modal } from "src/components/Modal";
+import useGraph from "src/store/useGraph";
 import useStored from "src/store/useStored";
+import styled from "styled-components";
 
 const SyntaxHighlighter = dynamic(() => import("react-syntax-highlighter/dist/cjs/prism-async"), {
   ssr: false,
 });
 
 interface NodeModalProps {
-  selectedNode: object;
   visible: boolean;
   closeModal: () => void;
 }
 
-export const NodeModal = ({ selectedNode, visible, closeModal }: NodeModalProps) => {
+const StyledTitle = styled.div`
+  line-height: 16px;
+  font-size: 12px;
+  font-weight: 600;
+  padding: 16px 0;
+
+  &:first-of-type {
+    padding-top: 0;
+  }
+`;
+
+export const NodeModal = ({ visible, closeModal }: NodeModalProps) => {
   const lightmode = useStored(state => state.lightmode);
-  const nodeData = Array.isArray(selectedNode) ? Object.fromEntries(selectedNode) : selectedNode;
+  const path = useGraph(state => state.path);
+  const nodeData = useGraph(state =>
+    Array.isArray(state.selectedNode) ? Object.fromEntries(state.selectedNode) : state.selectedNode
+  );
 
-  const handleClipboard = () => {
-    toast.success("Content copied to clipboard!");
-    navigator.clipboard?.writeText(JSON.stringify(nodeData));
-    closeModal();
+  const handleClipboard = (content: string) => {
+    try {
+      navigator.clipboard?.writeText(content);
+      toast.success("Content copied to clipboard!");
+      closeModal();
+    } catch (error) {
+      toast.error("Failed to save to clipboard.");
+    }
   };
 
   return (
     <Modal visible={visible} setVisible={closeModal} size="lg">
       <Modal.Header>Node Content</Modal.Header>
       <Modal.Content>
+        <StyledTitle>Content</StyledTitle>
         <SyntaxHighlighter
           style={lightmode ? vs : vscDarkPlus}
           customStyle={{
@@ -51,11 +71,26 @@ export const NodeModal = ({ selectedNode, visible, closeModal }: NodeModalProps)
             2
           )}
         </SyntaxHighlighter>
+        <StyledTitle>Node Path</StyledTitle>
+        <SyntaxHighlighter
+          style={lightmode ? vs : vscDarkPlus}
+          customStyle={{
+            borderRadius: "4px",
+            fontSize: "14px",
+            margin: "0",
+          }}
+          language="javascript"
+        >
+          {path}
+        </SyntaxHighlighter>
       </Modal.Content>
       <Modal.Controls setVisible={closeModal}>
-        <Button status="SECONDARY" onClick={handleClipboard}>
+        <Button status="SECONDARY" onClick={() => handleClipboard(JSON.stringify(nodeData))}>
           <FiCopy size={18} /> Clipboard
         </Button>
+        <Button status="SECONDARY" onClick={() => handleClipboard(path)}>
+          <FiCopy size={18} /> Copy Path
+        </Button>
       </Modal.Controls>
     </Modal>
   );

+ 4 - 0
src/store/useGraph.tsx

@@ -18,6 +18,8 @@ const initialStates = {
   collapsedNodes: [] as string[],
   collapsedEdges: [] as string[],
   collapsedParents: [] as string[],
+  selectedNode: [] as string | object,
+  path: "",
 };
 
 export type Graph = typeof initialStates;
@@ -36,10 +38,12 @@ interface GraphActions {
   zoomIn: () => void;
   zoomOut: () => void;
   centerView: () => void;
+  setSelectedNode: ({ node, path }: { node?: string | string[]; path?: string }) => void;
 }
 
 const useGraph = create<Graph & GraphActions>((set, get) => ({
   ...initialStates,
+  setSelectedNode: ({ node, path }) => set({ selectedNode: node, path }),
   setGraph: (data, options) => {
     const { nodes, edges } = parser(data ?? useJson.getState().json);