Explorar o código

Merge pull request #97 from AykutSarac/toggle-expand

Toggle expand
Aykut Saraç %!s(int64=2) %!d(string=hai) anos
pai
achega
ca208ae6a7

+ 1 - 2
src/components/Button/index.tsx

@@ -9,8 +9,7 @@ enum ButtonType {
   WARNING = "ORANGE",
 }
 
-export interface ButtonProps
-  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
   status?: keyof typeof ButtonType;
   block?: boolean;
 }

+ 65 - 4
src/components/CustomNode/TextNode.tsx

@@ -1,9 +1,33 @@
 import React from "react";
+import { MdCompareArrows } from "react-icons/md";
+import { NodeData } from "reaflow/dist/types";
 import { ConditionalWrapper, CustomNodeProps } from "src/components/CustomNode";
 import useConfig from "src/hooks/store/useConfig";
+import useGraph from "src/hooks/store/useGraph";
+import useStored from "src/hooks/store/useStored";
+import styled from "styled-components";
 import * as Styled from "./styles";
 
-const TextNode: React.FC<CustomNodeProps<string>> = ({
+const StyledExpand = styled.button`
+  pointer-events: all;
+  position: absolute;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  top: 0;
+  right: 0;
+  padding: 0;
+  color: ${({ theme }) => theme.TEXT_NORMAL};
+  background: rgba(0, 0, 0, 0.1);
+  min-height: 0;
+  height: 100%;
+  width: 40px;
+  border-radius: 0;
+  border-left: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
+`;
+
+const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
+  node,
   width,
   height,
   value,
@@ -12,21 +36,58 @@ const TextNode: React.FC<CustomNodeProps<string>> = ({
   y,
 }) => {
   const performanceMode = useConfig((state) => state.performanceMode);
+  const hideCollapse = useStored((state) => state.hideCollapse);
+  const expand = useConfig((state) => state.expand);
+  const expandNodes = useGraph((state) => state.expandNodes);
+  const collapseNodes = useGraph((state) => state.collapseNodes);
+  const [isExpanded, setIsExpanded] = React.useState(!expand);
+
+  const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
+    e.stopPropagation();
+    setIsExpanded(!isExpanded);
+  };
+
+  React.useEffect(() => {
+    setIsExpanded(!expand);
+  }, [expand]);
+
+  React.useEffect(() => {
+    if (isExpanded) collapseNodes(node.id);
+    else expandNodes(node.id);
+  }, [collapseNodes, expand, expandNodes, isExpanded, 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 width={width} height={height}>
+        <Styled.StyledText
+          hideCollapse={hideCollapse}
+          parent={isParent}
+          width={width}
+          height={height}
+        >
           <Styled.StyledKey
             data-x={x}
             data-y={y}
             data-key={JSON.stringify(value)}
             parent={isParent}
           >
-            <Styled.StyledLinkItUrl>{JSON.stringify(value).replaceAll('"', "")}</Styled.StyledLinkItUrl>
+            <Styled.StyledLinkItUrl>
+              {JSON.stringify(value).replaceAll('"', "")}
+            </Styled.StyledLinkItUrl>
           </Styled.StyledKey>
         </Styled.StyledText>
       </ConditionalWrapper>
+      {isParent && !hideCollapse && (
+        <StyledExpand onClick={handleExpand}>
+          <MdCompareArrows size={18} />
+        </StyledExpand>
+      )}
     </Styled.StyledForeignObject>
   );
 };

+ 2 - 1
src/components/CustomNode/index.tsx

@@ -37,7 +37,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
 
   return (
     <Node {...nodeProps} label={<Label style={baseLabelStyle} />}>
-      {({ width, height, x, y }) => {
+      {({ width, height, x, y, node }) => {
         if (properties.text instanceof Object) {
           const entries = Object.entries<string>(properties.text);
 
@@ -54,6 +54,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
 
         return (
           <TextNode
+            node={node}
             isParent={properties.data.isParent}
             value={properties.text}
             width={width}

+ 7 - 1
src/components/CustomNode/styles.tsx

@@ -23,7 +23,12 @@ export const StyledTextWrapper = styled.div`
   cursor: pointer;
 `;
 
-export const StyledText = styled.pre<{ width: number; height: number }>`
+export const StyledText = styled.div<{
+  width: number;
+  height: number;
+  parent?: boolean;
+  hideCollapse?: boolean;
+}>`
   display: flex;
   justify-content: center;
   flex-direction: column;
@@ -31,6 +36,7 @@ export const StyledText = styled.pre<{ width: number; height: number }>`
   height: ${({ height }) => height};
   min-height: 50;
   color: ${({ theme }) => theme.TEXT_NORMAL};
+  padding-right: ${({ parent, hideCollapse }) => parent && !hideCollapse && "20px"};
 `;
 
 export const StyledForeignObject = styled.foreignObject`

+ 49 - 29
src/components/Graph/index.tsx

@@ -4,13 +4,14 @@ import {
   TransformComponent,
   TransformWrapper,
 } from "react-zoom-pan-pinch";
-import { Canvas, EdgeData, ElkRoot, NodeData, NodeProps } 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 useGraph from "src/hooks/store/useGraph";
 
 interface LayoutProps {
   isWidget: boolean;
@@ -39,8 +40,10 @@ const MemoizedGraph = React.memo(function Layout({
   setSelectedNode,
 }: LayoutProps) {
   const json = useConfig((state) => state.json);
-  const [nodes, setNodes] = React.useState<NodeData[]>([]);
-  const [edges, setEdges] = React.useState<EdgeData[]>([]);
+  const nodes = useGraph((state) => state.nodes);
+  const edges = useGraph((state) => state.edges);
+  const setGraphValue = useGraph((state) => state.setGraphValue);
+
   const [size, setSize] = React.useState({
     width: 2000,
     height: 2000,
@@ -52,41 +55,29 @@ const MemoizedGraph = React.memo(function Layout({
     shallow
   );
 
-  React.useEffect(() => {
-    const { nodes, edges } = getEdgeNodes(json, expand);
-
-    setNodes(nodes);
-    setEdges(edges);
-  }, [json, expand]);
+  const handleNodeClick = React.useCallback(
+    (e: React.MouseEvent<SVGElement>, data: NodeData) => {
+      setSelectedNode(data.text);
+      openModal();
+    },
+    [openModal, setSelectedNode]
+  );
 
   const onInit = (ref: ReactZoomPanPinchRef) => {
     setConfig("zoomPanPinch", ref);
   };
 
-  const onCanvasClick = () => {
-    const input = document.querySelector("input:focus") as HTMLInputElement;
-    if (input) input.blur();
-  };
-
   const onLayoutChange = (layout: ElkRoot) => {
     if (layout.width && layout.height)
       setSize({ width: layout.width, height: layout.height });
   };
 
-  const handleNodeClick = React.useCallback(
-    (e: React.MouseEvent<SVGElement>, props: NodeProps) => {
-      setSelectedNode(props.properties.text);
-      openModal();
-    },
-    [openModal, setSelectedNode]
-  );
+  React.useEffect(() => {
+    const { nodes, edges } = getEdgeNodes(json, expand);
 
-  const node = React.useCallback(
-    (props) => (
-      <CustomNode onClick={(e) => handleNodeClick(e, props)} {...props} />
-    ),
-    [handleNodeClick]
-  );
+    setGraphValue("nodes", nodes);
+    setGraphValue("edges", edges);
+  }, [expand, json, setGraphValue]);
 
   return (
     <StyledEditorWrapper isWidget={isWidget}>
@@ -98,6 +89,9 @@ const MemoizedGraph = React.memo(function Layout({
         wheel={{
           step: 0.05,
         }}
+        doubleClick={{
+          disabled: true,
+        }}
       >
         <TransformComponent
           wrapperStyle={{
@@ -114,8 +108,12 @@ const MemoizedGraph = React.memo(function Layout({
             direction={layout}
             key={layout}
             onLayoutChange={onLayoutChange}
-            onCanvasClick={onCanvasClick}
-            node={node}
+            node={(props) => (
+              <CustomNode {...props} onClick={handleNodeClick} />
+            )}
+            edge={(props) => (
+              <Edge {...props} containerClassName={`edge-${props.id}`} />
+            )}
             zoomable={false}
             readonly
           />
@@ -131,6 +129,28 @@ export const Graph = ({ isWidget = false }: { isWidget?: boolean }) => {
 
   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

+ 3 - 25
src/components/Sidebar/index.tsx

@@ -23,11 +23,11 @@ import { useRouter } from "next/router";
 import { ImportModal } from "src/containers/Modals/ImportModal";
 import { ClearModal } from "src/containers/Modals/ClearModal";
 import { ShareModal } from "src/containers/Modals/ShareModal";
-import { IoAlertCircleSharp } from "react-icons/io5";
 import useConfig from "src/hooks/store/useConfig";
 import { getNextLayout } from "src/containers/Editor/LiveEditor/helpers";
 import { HiHeart } from "react-icons/hi";
 import shallow from "zustand/shallow";
+import { IoAlertCircleSharp } from "react-icons/io5";
 
 const StyledSidebar = styled.div`
   display: flex;
@@ -144,8 +144,8 @@ export const Sidebar: React.FC = () => {
   const [shareVisible, setShareVisible] = React.useState(false);
   const { push } = useRouter();
 
-  const [expand, performanceMode, layout] = useConfig(
-    (state) => [state.expand, state.performanceMode, state.layout],
+  const [expand, layout] = useConfig(
+    (state) => [state.expand, state.layout],
     shallow
   );
 
@@ -163,19 +163,6 @@ export const Sidebar: React.FC = () => {
     toast(`${expand ? "Collapsed" : "Expanded"} nodes.`);
   };
 
-  const togglePerformance = () => {
-    const toastMsg = performanceMode
-      ? "Disabled Performance Mode\nSearch Node & Save Image enabled."
-      : "Enabled Performance Mode\nSearch Node & Save Image disabled.";
-
-    toast(toastMsg, {
-      icon: <StyledAlertIcon size={36} />,
-      duration: 3000,
-    });
-
-    setConfig("performanceMode", !performanceMode);
-  };
-
   const toggleLayout = () => {
     const nextLayout = getNextLayout(layout);
     setConfig("layout", nextLayout);
@@ -210,15 +197,6 @@ export const Sidebar: React.FC = () => {
             {expand ? <CgArrowsMergeAltH /> : <CgArrowsShrinkH />}
           </StyledElement>
         </Tooltip>
-        <Tooltip
-          title={`${
-            performanceMode ? "Disable" : "Enable"
-          } Performance Mode (Beta)`}
-        >
-          <StyledElement onClick={togglePerformance} beta>
-            <CgPerformance color={performanceMode ? "#0073FF" : undefined} />
-          </StyledElement>
-        </Tooltip>
         <Tooltip title="Save JSON">
           <StyledElement onClick={handleSave}>
             <AiOutlineSave />

+ 70 - 0
src/components/Toggle/index.tsx

@@ -0,0 +1,70 @@
+import React from "react";
+import styled from "styled-components";
+import { IoIosCheckmarkCircle, IoMdCloseCircle } from "react-icons/io";
+
+interface ToggleProps {
+  checked?: boolean;
+  children?: React.ReactNode;
+  onChange?: (value: boolean) => void;
+}
+
+const StyledToggleWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  user-select: none;
+  width: 100%;
+  gap: 6px;
+`;
+
+const StyledLabel = styled.label`
+  color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
+  font-weight: 500;
+  cursor: pointer;
+`;
+
+const StyledToggle = styled.div<{ active: boolean }>`
+  position: relative;
+  display: flex;
+  justify-content: ${({ active }) => (active ? "right" : "left")};
+  align-items: center;
+  width: 40px;
+  height: 24px;
+  padding: 2px;
+  border-radius: 14px;
+  background: ${({ active }) => (active ? "#3AA55D" : "#72767c")};
+  transition: 0.1s;
+  cursor: pointer;
+
+  input {
+    display: none;
+  }
+`;
+
+const Toggle: React.FC<ToggleProps> = ({
+  children,
+  checked = false,
+  onChange,
+}) => {
+  const [isChecked, setIsChecked] = React.useState(checked);
+
+  const handleClick = () => {
+    setIsChecked(!isChecked);
+    if (onChange) onChange(!isChecked);
+  };
+
+  return (
+    <StyledToggleWrapper>
+      <StyledToggle active={isChecked} onClick={handleClick}>
+        {isChecked ? (
+          <IoIosCheckmarkCircle size={22} color="white" />
+        ) : (
+          <IoMdCloseCircle size={22} color="white" />
+        )}
+        <input type="checkbox" checked={isChecked} onChange={handleClick} />
+      </StyledToggle>
+      <StyledLabel onClick={handleClick}>{children}</StyledLabel>
+    </StyledToggleWrapper>
+  );
+};
+
+export default Toggle;

+ 4 - 0
src/constants/globalStyle.ts

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

+ 1 - 1
src/containers/Editor/LiveEditor/helpers.ts

@@ -32,7 +32,7 @@ export function getEdgeNodes(
         data: {
           isParent: el.parent,
         },
-        width: isExpanded ? 35 + longestLine * (el.parent ? 9 : 8) : 180,
+        width: isExpanded ? 35 + longestLine * 8 + (el.parent && 60) : 180,
         height,
       });
     } else {

+ 66 - 0
src/containers/Editor/Settings.tsx

@@ -0,0 +1,66 @@
+import React from "react";
+import toast from "react-hot-toast";
+import { IoAlertCircleSharp } from "react-icons/io5";
+import { Modal } from "src/components/Modal";
+import Toggle from "src/components/Toggle";
+import useConfig from "src/hooks/store/useConfig";
+import useStored from "src/hooks/store/useStored";
+import styled from "styled-components";
+import shallow from "zustand/shallow";
+
+const StyledToggle = styled(Toggle)`
+  flex-flow: row-reverse;
+  background: black;
+`;
+
+const StyledAlertIcon = styled(IoAlertCircleSharp)`
+  color: ${({ theme }) => theme.ORANGE};
+`;
+
+const StyledModalWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+`;
+
+export const Settings: React.FC<{
+  visible: boolean;
+  setVisible: React.Dispatch<React.SetStateAction<boolean>>;
+}> = ({ visible, setVisible }) => {
+  const performanceMode = useConfig((state) => state.performanceMode);
+  const [toggleHideCollapse, hideCollapse] = useStored(
+    (state) => [state.toggleHideCollapse, state.hideCollapse],
+    shallow
+  );
+  const setConfig = useConfig((state) => state.setConfig);
+
+  const togglePerformance = () => {
+    const toastMsg = performanceMode
+      ? "Disabled Performance Mode\nSearch Node & Save Image enabled."
+      : "Enabled Performance Mode\nSearch Node & Save Image disabled.";
+
+    toast(toastMsg, {
+      icon: <StyledAlertIcon size={36} />,
+      duration: 3000,
+    });
+
+    setConfig("performanceMode", !performanceMode);
+  };
+
+  return (
+    <Modal visible={visible} setVisible={setVisible}>
+      <Modal.Header>Settings</Modal.Header>
+      <Modal.Content>
+        <StyledModalWrapper>
+          <StyledToggle onChange={toggleHideCollapse} checked={hideCollapse}>
+            Hide Collapse/Expand Button
+          </StyledToggle>
+          <StyledToggle onChange={togglePerformance} checked={performanceMode}>
+            Performance Mode (Experimental)
+          </StyledToggle>
+        </StyledModalWrapper>
+      </Modal.Content>
+      <Modal.Controls setVisible={setVisible} />
+    </Modal>
+  );
+};

+ 11 - 0
src/containers/Editor/Tools.tsx

@@ -13,6 +13,8 @@ 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 { TbSettings } from "react-icons/tb";
+import { Settings } from "./Settings";
 
 export const StyledTools = styled.div`
   display: flex;
@@ -42,6 +44,7 @@ const StyledToolElement = styled.button`
 `;
 
 export const Tools: React.FC = () => {
+  const [settingsVisible, setSettingsVisible] = React.useState(false);
   const [isDownloadVisible, setDownloadVisible] = React.useState(false);
   const lightmode = useStored((state) => state.lightmode);
   const setLightTheme = useStored((state) => state.setLightTheme);
@@ -64,10 +67,17 @@ export const Tools: React.FC = () => {
       <StyledToolElement aria-label="fullscreen" onClick={toggleEditor}>
         <AiOutlineFullscreen />
       </StyledToolElement>
+      <StyledToolElement
+        aria-label="settings"
+        onClick={() => setSettingsVisible(true)}
+      >
+        <TbSettings />
+      </StyledToolElement>
       <StyledToolElement aria-label="switch theme" onClick={toggleTheme}>
         {lightmode ? <HiOutlineMoon /> : <HiOutlineSun />}
       </StyledToolElement>
       {!performanceMode && <SearchInput />}
+
       {!performanceMode && (
         <StyledToolElement
           aria-label="save"
@@ -89,6 +99,7 @@ export const Tools: React.FC = () => {
         visible={isDownloadVisible}
         setVisible={setDownloadVisible}
       />
+      <Settings visible={settingsVisible} setVisible={setSettingsVisible} />
     </StyledTools>
   );
 };

+ 0 - 7
src/containers/Modals/ImportModal/index.tsx

@@ -77,13 +77,6 @@ export const ImportModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
     }
   };
 
-  React.useEffect(() => {
-    return () => {
-      setJsonFile(null);
-      setURL("");
-    };
-  }, [visible]);
-
   return (
     <Modal visible={visible} setVisible={setVisible}>
       <Modal.Header>Import JSON</Modal.Header>

+ 0 - 1
src/containers/Modals/NodeModal/index.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import toast from "react-hot-toast";
 import { FiCopy } from "react-icons/fi";
-import { NodeData } from "reaflow";
 import { Button } from "src/components/Button";
 import { Modal } from "src/components/Modal";
 import styled from "styled-components";

+ 77 - 0
src/hooks/store/useGraph.tsx

@@ -0,0 +1,77 @@
+import create from "zustand";
+import { EdgeData, NodeData } from "reaflow/dist/types";
+import { Graph } from "src/components/Graph";
+import { findEdgeChildren } from "src/utils/findEdgeChildren";
+import { findNodeChildren } from "src/utils/findNodeChildren";
+
+export interface Graph {
+  nodes: NodeData[];
+  edges: EdgeData[];
+  collapsedNodes: string[];
+  collapsedEdges: string[];
+}
+
+interface GraphActions {
+  setGraphValue: (key: keyof Graph, value: any) => void;
+  expandNodes: (nodeId: string) => void;
+  collapseNodes: (nodeId: string) => void;
+}
+
+const initialStates: Graph = {
+  nodes: [],
+  edges: [],
+  collapsedNodes: [],
+  collapsedEdges: [],
+};
+
+const useGraph = create<Graph & GraphActions>((set) => ({
+  ...initialStates,
+  setGraphValue: (key, value) =>
+    set({
+      collapsedNodes: [],
+      collapsedEdges: [],
+      [key]: value,
+    }),
+  expandNodes: (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,
+        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,
+        collapsedNodes: state.collapsedNodes.concat(nodeIds),
+        collapsedEdges: state.collapsedEdges.concat(edgeIds),
+      };
+    }),
+}));
+
+export default useGraph;

+ 8 - 8
src/hooks/store/useStored.tsx

@@ -17,37 +17,37 @@ function getTomorrow() {
 
 export interface Config {
   lightmode: boolean;
+  hideCollapse: boolean;
   sponsors: {
     users: Sponsor[];
     nextDate: number;
   };
   setSponsors: (sponsors: Sponsor[]) => void;
   setLightTheme: (theme: boolean) => void;
+  toggleHideCollapse: (value: boolean) => void;
 }
 
 const useStored = create(
   persist<Config>(
     (set) => ({
       lightmode: false,
+      hideCollapse: false,
       sponsors: {
         users: [],
         nextDate: Date.now(),
       },
       setLightTheme: (enabled: boolean) =>
-        set((state) => {
-          return {
-            ...state,
-            lightmode: enabled,
-          };
+        set({
+          lightmode: enabled,
         }),
       setSponsors: (users) =>
-        set((state) => ({
-          ...state,
+        set({
           sponsors: {
             users,
             nextDate: getTomorrow(),
           },
-        })),
+        }),
+      toggleHideCollapse: (value: boolean) => set({ hideCollapse: value }),
     }),
     {
       name: "config",

+ 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);
         });
     }
 

+ 0 - 7
src/typings/styled.d.ts

@@ -1,12 +1,5 @@
 import "styled-components";
 
-interface ThemeTones {
-  readonly LOWLIGHT?: string;
-  readonly BASE: string;
-  readonly HIGHLIGHT?: string;
-  readonly TINT?: string;
-}
-
 declare module "styled-components" {
   export interface DefaultTheme {
     BLURPLE: string;

+ 17 - 0
src/utils/findEdgeChildren.ts

@@ -0,0 +1,17 @@
+import { NodeData, EdgeData } from "reaflow/dist/types";
+
+export const findEdgeChildren = (
+  selectedNode: string,
+  connections: NodeData[],
+  edges: EdgeData[]
+) => {
+  const nodeIds = connections.map((n) => n.id);
+
+  nodeIds.push(selectedNode);
+  const newEdges = edges.filter(
+    (e) =>
+      nodeIds.includes(e.from as string) && nodeIds.includes(e.to as string)
+  );
+
+  return newEdges;
+};

+ 35 - 0
src/utils/findNodeChildren.ts

@@ -0,0 +1,35 @@
+import { NodeData, EdgeData } from "reaflow/dist/types";
+
+export const findNodeChildren = (
+  selectedNode: string,
+  nodes: NodeData[],
+  edges: EdgeData[]
+) => {
+  const toByFrom = {};
+  for (const edge of edges) {
+    if (edge.from) {
+      toByFrom[edge.from] ??= [];
+      toByFrom[edge.from].push(edge.to);
+    }
+  }
+
+  const getNodes = (parent, allNodesIds: string[] = []) => {
+    const tos = toByFrom[parent];
+    if (tos) {
+      allNodesIds.push(...tos);
+      for (const to of tos) {
+        getNodes(to, allNodesIds);
+      }
+    }
+    return allNodesIds;
+  };
+
+  const myNodes = getNodes(selectedNode);
+
+  const findNodes = myNodes.map((id) => {
+    const node = nodes.find((n) => n.id === id);
+    return node as NodeData<any>;
+  });
+
+  return findNodes;
+};

+ 1 - 1
src/utils/json-editor-parser.ts

@@ -3,7 +3,7 @@
  */
 import toast from "react-hot-toast";
 
-const filterChild = ([k, v]) => {
+const filterChild = ([_, v]) => {
   const isNull = v === null;
   const isArray = Array.isArray(v) && v.length;
   const isObject = v instanceof Object;