Bladeren bron

implement toggle/expand

AykutSarac 2 jaren geleden
bovenliggende
commit
d6eb3c75e6

+ 15 - 15
src/components/CustomNode/TextNode.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { BiChevronLeft, BiChevronRight } from "react-icons/bi";
+import { BiHide } from "react-icons/bi";
 import { NodeData, EdgeData } from "reaflow";
 import { ConditionalWrapper, CustomNodeProps } from "src/components/CustomNode";
 import useConfig from "src/hooks/store/useConfig";
@@ -35,22 +35,26 @@ const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
   y,
 }) => {
   const performanceMode = useConfig((state) => state.performanceMode);
-  const expand = useConfig((state) => state.expand);
-  const [isExpanded, setIsExpanded] = React.useState(!expand);
-  const toggleNode = useGraph((state) => state.toggleNode);
-
-  React.useEffect(() => {
-    setIsExpanded(expand);
-  }, [expand]);
+  const expandNodes = useGraph((state) => state.expandNodes);
+  const collapseNodes = useGraph((state) => state.collapseNodes);
+  const [isExpanded, setIsExpanded] = React.useState(true);
 
   const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
     e.stopPropagation();
-    toggleNode(node, isExpanded);
     setIsExpanded(!isExpanded);
+
+    if (isExpanded) collapseNodes(node.id);
+    else expandNodes(node.id);
   };
 
   return (
-    <Styled.StyledForeignObject width={width} height={height} x={0} y={0}>
+    <Styled.StyledForeignObject
+      width={width}
+      height={height}
+      x={0}
+      y={0}
+      data-nodeid={node.id}
+    >
       <ConditionalWrapper condition={performanceMode}>
         <Styled.StyledText parent={isParent} width={width} height={height}>
           <Styled.StyledKey
@@ -67,11 +71,7 @@ const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
       </ConditionalWrapper>
       {isParent && (
         <StyledExpand onClick={handleExpand}>
-          {isExpanded ? (
-            <BiChevronRight size={20} />
-          ) : (
-            <BiChevronLeft size={20} />
-          )}
+          <BiHide size={18} />
         </StyledExpand>
       )}
     </Styled.StyledForeignObject>

+ 68 - 22
src/components/Graph/index.tsx

@@ -4,18 +4,24 @@ import {
   TransformComponent,
   TransformWrapper,
 } from "react-zoom-pan-pinch";
-import { Canvas, Edge, ElkRoot, NodeData, useSelection } from "reaflow";
+import {
+  Canvas,
+  Edge,
+  ElkRoot,
+  NodeData,
+} from "reaflow";
 import { CustomNode } from "src/components/CustomNode";
 import { NodeModal } from "src/containers/Modals/NodeModal";
 import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
 import useConfig from "src/hooks/store/useConfig";
 import styled from "styled-components";
 import shallow from "zustand/shallow";
-import useNodeTools from "src/hooks/store/useNodeTools";
 import useGraph from "src/hooks/store/useGraph";
 
 interface LayoutProps {
   isWidget: boolean;
+  openModal: () => void;
+  setSelectedNode: (node: object) => void;
 }
 
 const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
@@ -33,7 +39,11 @@ const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
   }
 `;
 
-const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
+const MemoizedGraph = React.memo(function Layout({
+  isWidget,
+  openModal,
+  setSelectedNode,
+}: LayoutProps) {
   const json = useConfig((state) => state.json);
   const nodes = useGraph((state) => state.nodes);
   const edges = useGraph((state) => state.edges);
@@ -50,14 +60,13 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
     shallow
   );
 
-  React.useEffect(() => {
-    const { nodes, edges } = getEdgeNodes(json, expand);
-
-    setGraphValue("nodes", nodes);
-    setGraphValue("edges", edges);
-    setGraphValue("initialNodes", nodes);
-    setGraphValue("initialEdges", edges);
-  }, [expand, json, setGraphValue]);
+  const handleNodeClick = React.useCallback(
+    (e: React.MouseEvent<SVGElement>, data: NodeData) => {
+      setSelectedNode(data.text);
+      openModal();
+    },
+    [openModal, setSelectedNode]
+  );
 
   const onInit = (ref: ReactZoomPanPinchRef) => {
     setConfig("zoomPanPinch", ref);
@@ -68,6 +77,13 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
       setSize({ width: layout.width, height: layout.height });
   };
 
+  React.useEffect(() => {
+    const { nodes, edges } = getEdgeNodes(json, expand);
+
+    setGraphValue("nodes", nodes);
+    setGraphValue("edges", edges);
+  }, [expand, json, setGraphValue]);
+
   return (
     <StyledEditorWrapper isWidget={isWidget}>
       <TransformWrapper
@@ -97,7 +113,12 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
             direction={layout}
             key={layout}
             onLayoutChange={onLayoutChange}
-            node={(props) => <CustomNode {...props} />}
+            node={(props) => (
+              <CustomNode {...props} onClick={handleNodeClick} />
+            )}
+            edge={(props) => (
+              <Edge {...props} containerClassName={`edge-${props.id}`} />
+            )}
             zoomable={false}
             readonly
           />
@@ -108,20 +129,45 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
 });
 
 export const Graph = ({ isWidget = false }: { isWidget?: boolean }) => {
-  const [newNodes, selectedNode, copySelectedNode] = useNodeTools(
-    (state) => [state.nodes, state.selectedNode, state.copySelectedNode],
-    shallow
-  );
-  const setNodeTools = useNodeTools((state) => state.setNodeTools);
-  const selectedNodeObject = newNodes.find((n) => n.id === selectedNode);
+  const [isModalVisible, setModalVisible] = React.useState(false);
+  const [selectedNode, setSelectedNode] = React.useState<object>({});
+
+  const openModal = React.useCallback(() => setModalVisible(true), []);
+
+  const collapsedNodes = useGraph((state) => state.collapsedNodes);
+  const collapsedEdges = useGraph((state) => state.collapsedEdges);
+
+  React.useEffect(() => {
+    const nodeList = collapsedNodes.map((id) => `[id*="node-${id}"]`);
+    const edgeList = collapsedEdges.map((id) => `[class*="edge-${id}"]`);
+
+    const nodes = document.querySelectorAll('[id*="node-"]');
+    const edges = document.querySelectorAll('[class*="edge-"]');
+
+    nodes.forEach((node) => node.classList.remove("hide"));
+    edges.forEach((edges) => edges.classList.remove("hide"));
+
+    if (nodeList.length) {
+      const selectedNodes = document.querySelectorAll(nodeList.join(","));
+      const selectedEdges = document.querySelectorAll(edgeList.join(","));
+
+      selectedNodes.forEach((node) => node.classList.add("hide"));
+      selectedEdges.forEach((edge) => edge.classList.add("hide"));
+    }
+  }, [collapsedNodes, collapsedEdges]);
+
   return (
     <>
-      <MemoizedGraph isWidget={isWidget} />
+      <MemoizedGraph
+        openModal={openModal}
+        setSelectedNode={setSelectedNode}
+        isWidget={isWidget}
+      />
       {!isWidget && (
         <NodeModal
-          selectedNode={selectedNodeObject?.text}
-          visible={copySelectedNode}
-          closeModal={() => setNodeTools("copySelectedNode", false)}
+          selectedNode={selectedNode}
+          visible={isModalVisible}
+          closeModal={() => setModalVisible(false)}
         />
       )}
     </>

+ 4 - 0
src/constants/globalStyle.ts

@@ -23,6 +23,10 @@ const GlobalStyle = createGlobalStyle`
       background-size: 15px 15px;
     }
   }
+
+  .hide {
+    display: none;
+  }
   
   a {
     text-decoration: none;

+ 2 - 96
src/containers/Editor/Tools.tsx

@@ -1,26 +1,18 @@
-import React, { useEffect } from "react";
+import React from "react";
 import {
   AiOutlineFullscreen,
   AiOutlineMinus,
   AiOutlinePlus,
-  AiOutlineNodeCollapse,
-  AiOutlineNodeExpand,
 } from "react-icons/ai";
 import { FiDownload } from "react-icons/fi";
 import { HiOutlineSun, HiOutlineMoon } from "react-icons/hi";
-import {
-  MdCenterFocusWeak,
-  MdOutlineCopyAll,
-  MdOutlineRestartAlt,
-} from "react-icons/md";
+import { MdCenterFocusWeak } from "react-icons/md";
 import { SearchInput } from "src/components/SearchInput";
 import styled from "styled-components";
 import useConfig from "src/hooks/store/useConfig";
 import shallow from "zustand/shallow";
 import { DownloadModal } from "../Modals/DownloadModal";
 import useStored from "src/hooks/store/useStored";
-import useNodeTools from "src/hooks/store/useNodeTools";
-import { collapseNodes, expandNodes, restartNodes } from "./NodeTools";
 
 export const StyledTools = styled.div`
   display: flex;
@@ -63,43 +55,6 @@ export const Tools: React.FC = () => {
 
   const setConfig = useConfig((state) => state.setConfig);
 
-  const [
-    selectedNode,
-    nodes,
-    edges,
-    newNodes,
-    newEdges,
-    collapsedNodes,
-    collapsedEdges,
-  ] = useNodeTools(
-    (state) => [
-      state.selectedNode,
-      state.nodes,
-      state.edges,
-      state.newNodes,
-      state.newEdges,
-      state.collapsedNodes,
-      state.collapsedEdges,
-    ],
-    shallow
-  );
-
-  const setNodeTools = useNodeTools((state) => state.setNodeTools);
-  useEffect(() => {
-    if (selectedNode) {
-      const haveChildren = newEdges.find((edge) => edge.from === selectedNode);
-      if (!collapsedNodes[selectedNode] && !haveChildren) {
-        setIsLastNode(true);
-      } else if (haveChildren) {
-        setExpand(true);
-        setIsLastNode(false);
-      } else if (collapsedNodes[selectedNode] && !haveChildren) {
-        setExpand(false);
-        setIsLastNode(false);
-      }
-    }
-  }, [collapsedNodes, newEdges, selectedNode]);
-
   const zoomIn = useConfig((state) => state.zoomIn);
   const zoomOut = useConfig((state) => state.zoomOut);
   const centerView = useConfig((state) => state.centerView);
@@ -116,55 +71,6 @@ export const Tools: React.FC = () => {
         {lightmode ? <HiOutlineMoon /> : <HiOutlineSun />}
       </StyledToolElement>
       {!performanceMode && <SearchInput />}
-      {selectedNode && (
-        <>
-          <StyledToolElement
-            aria-label="copy"
-            onClick={() => setNodeTools("copySelectedNode", true)}
-          >
-            <MdOutlineCopyAll color={selectedNodeColor} />
-          </StyledToolElement>
-          <StyledToolElement
-            aria-label="restart"
-            onClick={() => restartNodes(nodes, edges, newNodes, setNodeTools)}
-          >
-            <MdOutlineRestartAlt color={selectedNodeColor} />
-          </StyledToolElement>
-        </>
-      )}
-      {isLastNode ? null : expand ? (
-        <StyledToolElement
-          aria-label="collapse"
-          onClick={() =>
-            collapseNodes(
-              selectedNode,
-              newNodes,
-              newEdges,
-              collapsedNodes,
-              collapsedEdges,
-              setNodeTools
-            )
-          }
-        >
-          <AiOutlineNodeCollapse color={selectedNodeColor} />
-        </StyledToolElement>
-      ) : (
-        <StyledToolElement
-          aria-label="expand"
-          onClick={() =>
-            expandNodes(
-              selectedNode,
-              newNodes,
-              newEdges,
-              collapsedNodes,
-              collapsedEdges,
-              setNodeTools
-            )
-          }
-        >
-          <AiOutlineNodeExpand color={selectedNodeColor} />
-        </StyledToolElement>
-      )}
 
       {!performance && (
         <StyledToolElement

+ 35 - 98
src/hooks/store/useGraph.tsx

@@ -5,133 +5,70 @@ import { findEdgeChildren } from "src/utils/findEdgeChildren";
 import { findNodeChildren } from "src/utils/findNodeChildren";
 
 export interface Graph {
-  initialNodes: NodeData[];
-  initialEdges: EdgeData[];
   nodes: NodeData[];
   edges: EdgeData[];
-  collapsedNodes: { [key: string]: NodeData[] };
-  collapsedEdges: { [key: string]: EdgeData[] };
+  collapsedNodes: string[];
+  collapsedEdges: string[];
 }
 
 interface GraphActions {
   setGraphValue: (key: keyof Graph, value: any) => void;
-  toggleNode: (node: NodeData, isExpanded: boolean) => void;
+  expandNodes: (nodeId: string) => void;
+  collapseNodes: (nodeId: string) => void;
 }
 
-function isNode(element: NodeData | EdgeData) {
-  if ("text" in element) return true;
-  return false;
-}
-
-export const getIncomers = (
-  node: NodeData,
-  nodes: NodeData[],
-  edges: EdgeData[]
-): NodeData[] => {
-  if (!isNode(node)) {
-    return [];
-  }
-
-  const incomersIds = edges.filter((e) => e.to === node.id).map((e) => e.from);
-  return nodes.filter((n) => incomersIds.includes(n.id));
-};
-
-export const getOutgoingEdges = (
-  node: NodeData,
-  edges: EdgeData[]
-): EdgeData[] => {
-  if (!isNode(node)) {
-    return [];
-  }
-
-  return edges.filter((edge) => edge.from === node.id);
-};
-
-export const getIncomingEdges = (
-  node: NodeData,
-  edges: EdgeData[]
-): EdgeData[] => {
-  if (!isNode(node)) {
-    return [];
-  }
-
-  return edges.filter((edge) => edge.to === node.id);
-};
-
-export const getOutgoers = (
-  node: NodeData,
-  nodes: NodeData[],
-  edges: EdgeData[]
-) => {
-  const allOutgoers: NodeData[] = [];
-
-  if (!isNode(node)) {
-    return [];
-  }
-
-  const runner = (currentNode: NodeData) => {
-    const outgoerIds = edges
-      .filter((e) => e.from === currentNode.id)
-      .map((e) => e.to);
-    const nodeList = nodes.filter((n) => outgoerIds.includes(n.id));
-    allOutgoers.push(...nodeList);
-    nodeList.forEach((node) => runner(node));
-  };
-
-  return allOutgoers;
-};
-
 const initialStates: Graph = {
-  initialNodes: [],
-  initialEdges: [],
   nodes: [],
   edges: [],
-  collapsedNodes: {},
-  collapsedEdges: {},
+  collapsedNodes: [],
+  collapsedEdges: [],
 };
 
-const useGraph = create<Graph & GraphActions>((set, get) => ({
+const useGraph = create<Graph & GraphActions>((set) => ({
   ...initialStates,
   setGraphValue: (key, value) =>
     set((state) => ({
       ...state,
       [key]: value,
     })),
-  toggleNode: (node, isExpanded) =>
+  expandNodes: (nodeId) =>
     set((state) => {
-      const childrenNodes = findNodeChildren(
-        node.id,
-        state.initialNodes,
-        state.initialEdges
-      );
+      const childrenNodes = findNodeChildren(nodeId, state.nodes, state.edges);
       const childrenEdges = findEdgeChildren(
-        node.id,
+        nodeId,
         childrenNodes,
-        state.initialEdges
+        state.edges
       );
 
-      const expand = () => {
-        if (isExpanded) {
-          return {
-            nodes: state.nodes.filter(
-              (sNode) => !childrenNodes.map((n) => n.id).includes(sNode.id)
-            ),
-            edges: state.edges.filter(
-              (sEdge) => !childrenEdges.map((n) => n.id).includes(sEdge.id)
-            ),
-          };
-        }
+      const nodeIds = childrenNodes.map((node) => node.id);
+      const edgeIds = childrenEdges.map((edge) => edge.id);
 
-        return {
-          nodes: state.nodes.concat(childrenNodes),
-          edges: state.edges.concat(childrenEdges),
-        };
+      return {
+        ...state,
+        collapsedNodes: state.collapsedNodes.filter(
+          (nodeId) => !nodeIds.includes(nodeId)
+        ),
+        collapsedEdges: state.collapsedEdges.filter(
+          (edgeId) => !edgeIds.includes(edgeId)
+        ),
       };
+    }),
+  collapseNodes: (nodeId) =>
+    set((state) => {
+      const childrenNodes = findNodeChildren(nodeId, state.nodes, state.edges);
+      const childrenEdges = findEdgeChildren(
+        nodeId,
+        childrenNodes,
+        state.edges
+      );
+
+      const nodeIds = childrenNodes.map((node) => node.id);
+      const edgeIds = childrenEdges.map((edge) => edge.id);
 
       return {
         ...state,
-        nodes: expand().nodes,
-        edges: expand().edges,
+        collapsedNodes: state.collapsedNodes.concat(nodeIds),
+        collapsedEdges: state.collapsedEdges.concat(edgeIds),
       };
     }),
 }));

+ 0 - 39
src/hooks/store/useNodeTools.tsx

@@ -1,39 +0,0 @@
-import create from "zustand";
-import { EdgeData, NodeData } from "reaflow";
-
-export interface NodeTools {
-  selectedNode: string;
-  copySelectedNode: boolean;
-  nodes: NodeData[];
-  edges: EdgeData[];
-  newNodes: NodeData[];
-  newEdges: EdgeData[];
-  collapsedNodes: { [key: string]: NodeData[] };
-  collapsedEdges: { [key: string]: EdgeData[] };
-}
-
-export interface SettingsNodeTools {
-  setNodeTools: (key: keyof NodeTools, value: unknown) => void;
-}
-
-const initialStates: NodeTools = {
-  selectedNode: "",
-  copySelectedNode: false,
-  nodes: [],
-  edges: [],
-  newNodes: [],
-  newEdges: [],
-  collapsedNodes: {},
-  collapsedEdges: {},
-};
-
-const useNodeTools = create<NodeTools & SettingsNodeTools>((set) => ({
-  ...initialStates,
-  setNodeTools: (nodeTool: keyof NodeTools, value: unknown) =>
-    set((state) => ({
-      ...state,
-      [nodeTool]: value,
-    })),
-}));
-
-export default useNodeTools;

+ 1 - 1
src/pages/_app.tsx

@@ -49,7 +49,7 @@ function JsonVisio({ Component, pageProps }: AppProps) {
           }
         })
         .catch(function (err) {
-          console.log("Service Worker registration failed: ", err);
+          console.error("Service Worker registration failed: ", err);
         });
     }