feature: tree navigation

This commit is contained in:
2026-03-16 14:48:01 +01:00
parent 2bf8a20f24
commit 2495041a2b
2 changed files with 38 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useEffectEvent, useState } from 'react'; import React, { useEffectEvent, useRef, useState } from 'react';
import { Layout, theme, Breadcrumb, Button, Space, Tree } from 'antd'; import { Layout, theme, Breadcrumb, Button, Space, Tree } from 'antd';
import Graph from './components/Graph'; import Graph, { type GraphHandle } 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 { import {
@@ -36,10 +36,20 @@ const App: React.FC = () => {
onKeyUp(ev.key); onKeyUp(ev.key);
}); });
const treeData = useGraphLayersTreeStore(store => store.tree); const treeData = useGraphLayersTreeStore(store => store.tree);
const nodesFlatById = useGraphLayersTreeStore(store => store.nodesFlatById);
const parentIdByChildId = useGraphLayersTreeStore(store => store.parentIdByChildId);
const graphRef = useRef<GraphHandle>(null);
useEffect(() => { function buildPathToNode(nodeId: string) {
console.info(treeData); const path: Array<{ id: string; name: string | undefined }> = [];
}, [treeData]) let current: string | undefined = nodeId;
while (current && nodesFlatById.has(current)) {
const node = nodesFlatById.get(current)!;
path.unshift({ id: current, name: node.title as string | undefined });
current = parentIdByChildId.get(current);
}
return [{ id: 'main', name: 'Main' }, ...path];
}
return ( return (
<Layout> <Layout>
@@ -54,6 +64,11 @@ const App: React.FC = () => {
style={{ style={{
borderRadius: 0 borderRadius: 0
}} }}
onSelect={(keys) => {
const nodeId = keys[0] as string;
if (!nodeId) return;
graphRef.current?.navigateTo(nodeId, buildPathToNode(nodeId));
}}
/> />
</Sider> </Sider>
<Layout> <Layout>
@@ -110,7 +125,7 @@ const App: React.FC = () => {
borderRadius: '6px' borderRadius: '6px'
}} }}
> >
<Graph setGraphPath={setGraphLevel} /> <Graph ref={graphRef} setGraphPath={setGraphLevel} />
</Content> </Content>
</Layout> </Layout>
</Layout> </Layout>

View File

@@ -1,4 +1,4 @@
import { createContext, useEffect, useRef, useState } from "react"; import { createContext, forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
import Viz from 'viz.js'; import Viz from 'viz.js';
import { Module, render } from 'viz.js/full.render.js'; import { Module, render } from 'viz.js/full.render.js';
import * as d3 from 'd3'; import * as d3 from 'd3';
@@ -63,7 +63,11 @@ export interface OpenNodeContext {
const viz = new Viz({ Module, render }); const viz = new Viz({ Module, render });
export const graphContext = createContext<GraphContext | null>(null); export const graphContext = createContext<GraphContext | null>(null);
export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<React.SetStateAction<BreadcrumbItemType[]>> }) { export interface GraphHandle {
navigateTo: (nodeId: string, path: Array<{ id: string; name: string | undefined }>) => void;
}
const Graph = forwardRef<GraphHandle, { setGraphPath: React.Dispatch<React.SetStateAction<BreadcrumbItemType[]>> }>(function Graph({ setGraphPath }, ref) {
const containerRef = useRef(null); const containerRef = useRef(null);
const [graph, setGraph] = useState(defaultGraph()); const [graph, setGraph] = useState(defaultGraph());
const [contextMenuOpened, openContextMenu] = useState(false); const [contextMenuOpened, openContextMenu] = useState(false);
@@ -242,6 +246,14 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
} as BreadcrumbItemType; } as BreadcrumbItemType;
} }
useImperativeHandle(ref, () => ({
navigateTo(nodeId, path) {
const newPath = path.map(p => createPathSegment(p.id, p.name));
setGraphsPath(newPath);
selectGraphId(nodeId);
}
}));
return ( return (
<div className="flex-1 p-4"> <div className="flex-1 p-4">
<div ref={containerRef} className="w-full h-full bg-white rounded shadow" style={{ minHeight: '600px', overflow: 'auto' }}> <div ref={containerRef} className="w-full h-full bg-white rounded shadow" style={{ minHeight: '600px', overflow: 'auto' }}>
@@ -262,7 +274,9 @@ export default function Graph({ setGraphPath }: { setGraphPath: React.Dispatch<R
</graphContext.Provider> </graphContext.Provider>
</div> </div>
) )
} });
export default Graph;
export function defaultGraph(): GraphModel { export function defaultGraph(): GraphModel {
const start = crypto.randomUUID(); const start = crypto.randomUUID();