mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
fix(ui): keep the whole graph on screen while the first scope picture is empty
Selecting a symbol outside the loaded layout first arranges an empty preview. The scene took that empty arrangement as its placeholder and drew a blank canvas for about 250 ms before the scope appeared. An arrangement without nodes is no picture: the whole graph stays visible until the scope is laid out. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
@@ -143,7 +143,7 @@ import { galaxyPathText, galaxyToolbarText } from './galaxy-strings';
|
||||
import { useOrganicLayout } from './use-organic-layout';
|
||||
import RenderProgress from './RenderProgress';
|
||||
import { useGraphScope } from './use-graph-scope';
|
||||
import { limitGraphRender, scopedHierarchy } from './graph-scope';
|
||||
import { limitGraphRender, scenePictureFor, scopedHierarchy } from './graph-scope';
|
||||
import './graph-exploration.css';
|
||||
import { layoutNodeForSelection } from './selected-node';
|
||||
import { galaxyScopeEvidence, useSelectionEvidence, type SelectionEvidenceListener } from './selection-evidence';
|
||||
@@ -790,7 +790,7 @@ export default function GalaxyPanel(props: GalaxyPanelProps): JSX.Element {
|
||||
* der ganze Graph, der gerade noch zu sehen war. `data` bleibt dabei das
|
||||
* AKTUELLE Bild: Einpassung, Legende und Zaehler rechnen nie mit dem alten.
|
||||
*/
|
||||
const sceneData = scope.scope ? (organic ?? organicTask.stale)?.data ?? layout : layout;
|
||||
const sceneData = scope.scope ? scenePictureFor(organic?.data, organicTask.stale?.data, layout) : layout;
|
||||
const sceneScoped = Boolean(scope.scope) && sceneData !== layout;
|
||||
const scopedRoot = scope.result?.roots.size === 1
|
||||
? scope.result.data.nodes.find(node => scope.result!.roots.has(node.id)) : undefined;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { arrangeScopedGraph, graphEdgeTypesKey, limitGraphRender, loadGraphScope, readGraphPages, scopedHierarchy, type GraphQueryClient } from './graph-scope';
|
||||
import { arrangeScopedGraph, graphEdgeTypesKey, limitGraphRender, loadGraphScope, readGraphPages, scenePictureFor, scopedHierarchy, type GraphQueryClient } from './graph-scope';
|
||||
import type { QueryGraphResult } from '../provider/rpc-schemas';
|
||||
import type { GraphNode } from './types';
|
||||
import type { GraphData, GraphNode } from './types';
|
||||
|
||||
const node = (id: number, file = 'src/a.ts', start = 1, end = 10): GraphNode => ({ id, name: `n${id}`, qualified_name: `p.n${id}`, file_path: file,
|
||||
label: 'Function', start_line: start, end_line: end, x: id, y: 0, z: 0, color: '#abcabc', size: 3 });
|
||||
@@ -228,3 +228,14 @@ it('retains simple columns for small hierarchy levels', () => {
|
||||
const result = scopedHierarchy({ data: { nodes, edges: [], total_nodes: 3 }, roots: new Set([1]), depth: 1, exhausted: false }, 'small');
|
||||
expect(result.data.nodes.map(node => [node.x, node.y])).toEqual([[0, 0], [160, 16], [160, -16]]);
|
||||
});
|
||||
|
||||
it('keeps the whole graph on screen while the first scope picture is still empty', () => {
|
||||
const graph = (count: number): GraphData => ({ nodes: Array.from({ length: count }, (_, id) => ({ id, name: `n${id}`, label: 'Function', x: 0, y: 0, z: 0, size: 1, color: '#999999' })), edges: [], total_nodes: count });
|
||||
const layout = graph(5), stale = graph(2), current = graph(3), empty = graph(0);
|
||||
// An empty preview of a symbol outside the loaded layout is no picture.
|
||||
expect(scenePictureFor(empty, undefined, layout)).toBe(layout);
|
||||
expect(scenePictureFor(undefined, empty, layout)).toBe(layout);
|
||||
expect(scenePictureFor(empty, stale, layout)).toBe(stale);
|
||||
expect(scenePictureFor(current, stale, layout)).toBe(current);
|
||||
expect(scenePictureFor(undefined, stale, layout)).toBe(stale);
|
||||
});
|
||||
|
||||
@@ -198,6 +198,17 @@ export async function loadGraphScope(project: string, scope: GraphScope, depth:
|
||||
return { data: arrangeScopedGraph([...nodes.values()], [...edges.values()], roots, levels), roots, depth: reachedDepth, exhausted: edgeTypes?.length === 0 || (depth > 0 && frontier.length === 0), frontier, levels, traversalKey };
|
||||
}
|
||||
|
||||
/** Was die Galaxie zeigt, waehrend ein Scope angeordnet wird: das aktuelle
|
||||
* Bild, sonst die letzte Anordnung, sonst den ganzen Graphen. Eine Anordnung
|
||||
* ohne Knoten ist kein Bild. Liegt das gewaehlte Symbol ausserhalb des
|
||||
* geladenen Layouts, ist die erste Vorschau leer, und als Platzhalter stand
|
||||
* dann eine leere Flaeche statt des Graphen, der gerade noch zu sehen war. */
|
||||
export function scenePictureFor(current: GraphData | undefined, stale: GraphData | undefined, layout: GraphData | undefined): GraphData | undefined {
|
||||
if (current && current.nodes.length > 0) return current;
|
||||
if (stale && stale.nodes.length > 0) return stale;
|
||||
return layout;
|
||||
}
|
||||
|
||||
export function limitGraphRender(data: GraphData, nodeLimit: number, edgeLimit: number, required: ReadonlySet<number> = new Set()): GraphData {
|
||||
const nodes = [...data.nodes.filter(node => required.has(node.id)), ...data.nodes.filter(node => !required.has(node.id))]
|
||||
.slice(0, Math.max(0, Math.floor(nodeLimit)));
|
||||
|
||||
Reference in New Issue
Block a user