layers tree I

This commit is contained in:
2025-11-11 23:31:22 +01:00
parent 4092b12aef
commit ae02080563
5 changed files with 176 additions and 18 deletions

View File

@@ -40,3 +40,7 @@
.read-the-docs { .read-the-docs {
color: #888; color: #888;
} }
.ant-tree-list-holder {
border-radius: 0;
}

View File

@@ -1,16 +1,22 @@
import React, { createContext, useEffectEvent, useState } from 'react'; import React, { createContext, useEffect, useEffectEvent, useState } from 'react';
import { Layout, theme, Breadcrumb } from 'antd'; import { Layout, theme, Breadcrumb, Button, Space, Tree, type TreeDataNode } from 'antd';
import Graph, { GraphModel } from './components/Graph'; import Graph, { GraphModel } from './components/Graph';
import type { BreadcrumbItemType } from 'antd/es/breadcrumb/Breadcrumb'; import type { BreadcrumbItemType } from 'antd/es/breadcrumb/Breadcrumb';
import { useKeysdownStore } from './stores/ArrayStore'; 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 App: React.FC = () => {
const { const {
token: { colorBgContainer, borderRadiusLG }, token: { colorBgContainer },
} = theme.useToken(); } = theme.useToken();
const [graphLevel, setGraphLevel] = useState<BreadcrumbItemType[]>([]) const [graphLevel, setGraphLevel] = useState<BreadcrumbItemType[]>([])
const [collapsed, setCollapsed] = useState(true);
const addKey = useKeysdownStore(state => state.add); const addKey = useKeysdownStore(state => state.add);
const removeKey = useKeysdownStore(state => state.remove); const removeKey = useKeysdownStore(state => state.remove);
const onKeydown = useEffectEvent((key: string) => { const onKeydown = useEffectEvent((key: string) => {
@@ -22,21 +28,56 @@ const App: React.FC = () => {
document.addEventListener('keydown', (ev) => { document.addEventListener('keydown', (ev) => {
onKeydown(ev.key) onKeydown(ev.key)
}); });
document.addEventListener('keyup', (ev) => { document.addEventListener('keyup', (ev) => {
onKeyUp(ev.key); onKeyUp(ev.key);
}) });
const treeData = useGraphLayersTreeStore(store => store.tree);
useEffect(() => {
console.info(treeData);
}, [treeData])
return ( return (
<Layout> <Layout>
<Sider trigger={null} collapsible collapsed={collapsed} collapsedWidth={0} style={{
background: colorBgContainer,
}}>
<Header style={{ padding: 0 }}></Header>
<Tree
checkable
treeData={treeData}
defaultExpandAll={true}
style={{
borderRadius: 0
}}
/>
</Sider>
<Layout> <Layout>
<Breadcrumb style={{ margin: '8px 0 0 16px' }} items={graphLevel} /> <Header style={{ padding: 0, background: colorBgContainer }}>
<Space>
<div style={{ background: '#001529' }}>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
width: 32,
height: 32,
color: colorBgContainer,
}}
/>
</div>
<Breadcrumb items={graphLevel} />
</Space>
</Header>
<Content <Content
style={{ style={{
margin: '8px 16px', margin: '8px 16px',
padding: 24, padding: 24,
minHeight: 280, minHeight: 280,
background: colorBgContainer, background: colorBgContainer,
borderRadius: borderRadiusLG, borderRadius: '6px'
}} }}
> >
<Graph setGraphPath={setGraphLevel} /> <Graph setGraphPath={setGraphLevel} />

View File

@@ -190,12 +190,7 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
} }
function getRandomWords(count: number) { function getRandomWords(count: number) {
const wordList = [ const wordList = ['Apple', 'Sun', 'Flame', 'Earth', 'Forest', 'Dream', 'Sky', 'Shadow', 'Flower', 'Ocean', 'River', 'Path', 'Sand', 'Night', 'Star', 'Rain', 'Light', 'Tree', 'Wave', 'Storm', 'Stone', 'Snow', 'Cloud', 'Heart', 'Mountain', 'Leaf', 'Bird', 'Wind', 'Fire', 'Wolf'];
"apple", "river", "mountain", "sky", "storm", "ocean", "forest", "dream",
"stone", "flame", "shadow", "cloud", "leaf", "wind", "fire", "earth",
"flower", "bird", "light", "night", "sun", "rain", "snow", "tree",
"wolf", "star", "sand", "wave", "heart", "path"
];
return wordList return wordList
.sort(() => Math.random() - 0.5) .sort(() => Math.random() - 0.5)

View File

@@ -3,6 +3,7 @@ import { defaultGraph, graphContext, type EdgeModel, type NodeContext } from "./
import { useContext } from "react"; import { useContext } from "react";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { useGraphsStore } from "../stores/GraphsStore"; import { useGraphsStore } from "../stores/GraphsStore";
import { useGraphLayersTreeStore } from "../stores/TreeStore";
const items: MenuProps['items'] = [ const items: MenuProps['items'] = [
{ {
@@ -39,6 +40,7 @@ export default function NodeContextMenu({
const graphContextValue = useContext(graphContext)!; const graphContextValue = useContext(graphContext)!;
const graphsById = useGraphsStore((s) => s.graphsById); const graphsById = useGraphsStore((s) => s.graphsById);
const addTreeNode = useGraphLayersTreeStore(store => store.add);
function contextMenuOpenChange(open: boolean) { function contextMenuOpenChange(open: boolean) {
if (!open) { if (!open) {
@@ -64,9 +66,10 @@ export default function NodeContextMenu({
if (!selectedGraph) { if (!selectedGraph) {
selectedGraph = defaultGraph(); selectedGraph = defaultGraph();
graphsById.set(nodeContext.nodeId, selectedGraph); graphsById.set(nodeContext.nodeId, selectedGraph);
const parenNodeId = graphContextValue.graphId === 'main' ? undefined : graphContextValue.graphId;
addTreeNode(nodeContext, parenNodeId);
} }
console.info(graphsById)
graphContextValue.setGraph(selectedGraph); graphContextValue.setGraph(selectedGraph);
break; break;

115
src/stores/TreeStore.tsx Normal file
View File

@@ -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<React.Key, TreeDataNode>,
parentIdByChildId: Map<React.Key, string>;
rootNodes: TreeDataNode[];
tree: TreeDataNode[];
add: (childNode: NodeContext, parentNodeId: string | undefined) => void;
remove: (nodeId: string) => void;
}
export const useGraphLayersTreeStore = create<TreeStore>()((set, get) => ({
nodesFlatById: new Map<React.Key, TreeDataNode>(),
parentIdByChildId: new Map<React.Key, string>(),
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<React.Key, TreeDataNode>): 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;
}