diff --git a/src/App.css b/src/App.css index b9d355d..54e8654 100644 --- a/src/App.css +++ b/src/App.css @@ -40,3 +40,7 @@ .read-the-docs { color: #888; } + +.ant-tree-list-holder { + border-radius: 0; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 42f850f..9be117a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,42 +1,83 @@ -import React, { createContext, useEffectEvent, useState } from 'react'; -import { Layout, theme, Breadcrumb } from 'antd'; +import React, { createContext, useEffect, useEffectEvent, useState } from 'react'; +import { Layout, theme, Breadcrumb, Button, Space, Tree, type TreeDataNode } from 'antd'; import Graph, { GraphModel } from './components/Graph'; import type { BreadcrumbItemType } from 'antd/es/breadcrumb/Breadcrumb'; import { useKeysdownStore } from './stores/ArrayStore'; +import { + MenuFoldOutlined, + MenuUnfoldOutlined +} from '@ant-design/icons'; +import { useGraphLayersTreeStore } from './stores/TreeStore'; -const { Content } = Layout; +const { Header, Content, Sider } = Layout; const App: React.FC = () => { const { - token: { colorBgContainer, borderRadiusLG }, + token: { colorBgContainer }, } = theme.useToken(); const [graphLevel, setGraphLevel] = useState([]) + const [collapsed, setCollapsed] = useState(true); const addKey = useKeysdownStore(state => state.add); const removeKey = useKeysdownStore(state => state.remove); const onKeydown = useEffectEvent((key: string) => { - addKey(key); + addKey(key); }); const onKeyUp = useEffectEvent((key: string) => { removeKey(key); }) - document.addEventListener('keydown', (ev) => { + document.addEventListener('keydown', (ev) => { onKeydown(ev.key) }); - document.addEventListener('keyup', (ev) => { + document.addEventListener('keyup', (ev) => { onKeyUp(ev.key); - }) + }); + const treeData = useGraphLayersTreeStore(store => store.tree); + + useEffect(() => { + console.info(treeData); + }, [treeData]) return ( + +
+ +
- +
+ +
+
+ +
+
diff --git a/src/components/Graph.tsx b/src/components/Graph.tsx index 4be463e..8478318 100644 --- a/src/components/Graph.tsx +++ b/src/components/Graph.tsx @@ -190,12 +190,7 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch Math.random() - 0.5) diff --git a/src/components/NodeContextMenu.tsx b/src/components/NodeContextMenu.tsx index bbaa1c4..6cdc0ce 100644 --- a/src/components/NodeContextMenu.tsx +++ b/src/components/NodeContextMenu.tsx @@ -3,6 +3,7 @@ import { defaultGraph, graphContext, type EdgeModel, type NodeContext } from "./ import { useContext } from "react"; import { cloneDeep } from "lodash"; import { useGraphsStore } from "../stores/GraphsStore"; +import { useGraphLayersTreeStore } from "../stores/TreeStore"; const items: MenuProps['items'] = [ { @@ -39,6 +40,7 @@ export default function NodeContextMenu({ const graphContextValue = useContext(graphContext)!; const graphsById = useGraphsStore((s) => s.graphsById); + const addTreeNode = useGraphLayersTreeStore(store => store.add); function contextMenuOpenChange(open: boolean) { if (!open) { @@ -64,9 +66,10 @@ export default function NodeContextMenu({ if (!selectedGraph) { selectedGraph = defaultGraph(); graphsById.set(nodeContext.nodeId, selectedGraph); + const parenNodeId = graphContextValue.graphId === 'main' ? undefined : graphContextValue.graphId; + addTreeNode(nodeContext, parenNodeId); } - - console.info(graphsById) + graphContextValue.setGraph(selectedGraph); break; diff --git a/src/stores/TreeStore.tsx b/src/stores/TreeStore.tsx new file mode 100644 index 0000000..5ec8470 --- /dev/null +++ b/src/stores/TreeStore.tsx @@ -0,0 +1,115 @@ +import type { TreeDataNode } from "antd"; +import { cloneDeep } from "lodash"; +import React from "react"; +import { create } from "zustand"; +import type { NodeContext } from "../components/Graph"; + +export interface TreeStore { + nodesFlatById: Map, + parentIdByChildId: Map; + rootNodes: TreeDataNode[]; + tree: TreeDataNode[]; + add: (childNode: NodeContext, parentNodeId: string | undefined) => void; + remove: (nodeId: string) => void; +} + +export const useGraphLayersTreeStore = create()((set, get) => ({ + nodesFlatById: new Map(), + parentIdByChildId: new Map(), + rootNodes: [], + tree: [], + add: (childNodeContext, parentNodeId) => set((state) => { + const childNode = nodeContextToTreeNode(childNodeContext); + if (parentNodeId) { + const parentNode = state.nodesFlatById.get(parentNodeId); + if (parentNode) { + parentNode.children = parentNode.children ? [...parentNode.children, childNode] : [childNode]; + const parentIdByChildId = new Map(state.parentIdByChildId); + parentIdByChildId.set(childNode.key, parentNodeId); + const nodesFlatById = new Map(state.nodesFlatById); + nodesFlatById.set(childNode.key, childNode); + + const newState = { + nodesFlatById: nodesFlatById, + parentIdByChildId: parentIdByChildId, + rootNodes: [...state.rootNodes], + tree: createTree([...state.rootNodes], nodesFlatById) + } + + return newState; + } else { + throw Error(`There is no parent node with id: ${parentNodeId}`) + } + } else { + const nodesFlatById = new Map(state.nodesFlatById); + nodesFlatById.set(childNode.key, childNode); + const newState = { + nodesFlatById: nodesFlatById, + parentIdByChildId: state.parentIdByChildId, + rootNodes: [...state.rootNodes, childNode], + tree: createTree([...state.rootNodes], nodesFlatById) + } + return newState; + } + }), + remove: (nodeId) => set((state) => { + const node = state.nodesFlatById.get(nodeId); + + if (!node) { + return state; + } + + const nodesFlatById = new Map(state.nodesFlatById); + nodesFlatById.delete(nodeId); + const parentNodeId = state.parentIdByChildId.get(nodeId); + + if (parentNodeId) { + const parentNode = state.nodesFlatById.get(parentNodeId); + + if (parentNode) { + parentNode.children = parentNode.children + ? [...parentNode.children?.filter(n => n.key !== nodeId)] + : parentNode.children; + const parentIdByChildId = new Map(state.parentIdByChildId); + parentIdByChildId.delete(nodeId); + + return { + rootNodes: [...state.rootNodes], + nodesFlatById: nodesFlatById, + parentIdByChildId: parentIdByChildId, + tree: createTree([...state.rootNodes], nodesFlatById) + } + } + } + + return { + rootNodes: [...state.rootNodes.filter(n => n.key !== nodeId)], + nodesFlatById: nodesFlatById, + parentIdByChildId: state.parentIdByChildId, + tree: createTree([...state.rootNodes], nodesFlatById) + } + }) +})); + +export function createTree(nodes: TreeDataNode[], nodesFlatById: Map): TreeDataNode[] { + const n = [...nodes]; + const result = []; + + for (const node of n) { + const stateNode = nodesFlatById.get(node.key); + + if (stateNode) { + stateNode.children = createTree(stateNode?.children ?? [], nodesFlatById); + result.push(stateNode); + } + } + + return result; +} + +export function nodeContextToTreeNode(nodeContext: NodeContext): TreeDataNode { + return { + key: nodeContext.nodeId, + title: nodeContext.nodeName, + } as TreeDataNode; +} \ No newline at end of file