mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
refactor(dashboard): remove swim-lane flow view
The swim-lane layout needs more design work before shipping. Remove the flow view toggle, useFlowViewGraph hook, applySwimLaneLayout, and related store/breadcrumb code. Hierarchical lens navigation is retained.
This commit is contained in:
@@ -1,29 +1,22 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useDashboardStore } from "../store";
|
||||
import type { ViewMode } from "../store";
|
||||
|
||||
export default function Breadcrumb() {
|
||||
const navigationLevel = useDashboardStore((s) => s.navigationLevel);
|
||||
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
|
||||
const viewMode = useDashboardStore((s) => s.viewMode);
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const navigateToOverview = useDashboardStore((s) => s.navigateToOverview);
|
||||
const setViewMode = useDashboardStore((s) => s.setViewMode);
|
||||
|
||||
const activeLayer = graph?.layers.find((l) => l.id === activeLayerId);
|
||||
|
||||
// Escape key to go back to overview (only in graph mode)
|
||||
// Escape key to go back to overview
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (
|
||||
e.key === "Escape" &&
|
||||
viewMode === "graph" &&
|
||||
navigationLevel === "layer-detail"
|
||||
) {
|
||||
if (e.key === "Escape" && navigationLevel === "layer-detail") {
|
||||
navigateToOverview();
|
||||
}
|
||||
},
|
||||
[viewMode, navigationLevel, navigateToOverview],
|
||||
[navigationLevel, navigateToOverview],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,48 +24,15 @@ export default function Breadcrumb() {
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
const handleViewToggle = useCallback(
|
||||
(mode: ViewMode) => {
|
||||
setViewMode(mode);
|
||||
},
|
||||
[setViewMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 left-4 z-10 flex items-center gap-2">
|
||||
{/* View mode toggle */}
|
||||
<div className="flex items-center rounded-full bg-elevated border border-border-subtle shadow-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => handleViewToggle("graph")}
|
||||
className={`px-3 py-2 text-[10px] font-semibold uppercase tracking-wider transition-colors ${
|
||||
viewMode === "graph"
|
||||
? "bg-gold/20 text-gold"
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Graph
|
||||
</button>
|
||||
<div className="w-px h-4 bg-border-subtle" />
|
||||
<button
|
||||
onClick={() => handleViewToggle("flow")}
|
||||
className={`px-3 py-2 text-[10px] font-semibold uppercase tracking-wider transition-colors ${
|
||||
viewMode === "flow"
|
||||
? "bg-gold/20 text-gold"
|
||||
: "text-text-muted hover:text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
Flow
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation breadcrumb (only in graph mode) */}
|
||||
{viewMode === "graph" && navigationLevel === "overview" && (
|
||||
{navigationLevel === "overview" && (
|
||||
<div className="px-4 py-2 rounded-full bg-elevated border border-border-subtle text-xs font-semibold tracking-wider uppercase text-text-secondary shadow-lg">
|
||||
Project Overview
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === "graph" && navigationLevel === "layer-detail" && (
|
||||
{navigationLevel === "layer-detail" && (
|
||||
<div className="flex items-center gap-1.5 px-4 py-2 rounded-full bg-elevated border border-gold/30 text-xs font-semibold tracking-wider uppercase shadow-lg">
|
||||
<button
|
||||
onClick={navigateToOverview}
|
||||
@@ -89,12 +49,6 @@ export default function Breadcrumb() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === "flow" && (
|
||||
<div className="px-4 py-2 rounded-full bg-elevated border border-border-subtle text-xs font-semibold tracking-wider uppercase text-text-secondary shadow-lg">
|
||||
Request Flow
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import Breadcrumb from "./Breadcrumb";
|
||||
import { useDashboardStore } from "../store";
|
||||
import {
|
||||
applyDagreLayout,
|
||||
applySwimLaneLayout,
|
||||
NODE_WIDTH,
|
||||
NODE_HEIGHT,
|
||||
LAYER_CLUSTER_WIDTH,
|
||||
@@ -416,169 +415,12 @@ function useLayerDetailGraph() {
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Flow (swim-lane) view: all layers as columns ──────────────────────
|
||||
|
||||
function useFlowViewGraph() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const searchResults = useDashboardStore((s) => s.searchResults);
|
||||
const selectNode = useDashboardStore((s) => s.selectNode);
|
||||
const tourHighlightedNodeIds = useDashboardStore(
|
||||
(s) => s.tourHighlightedNodeIds,
|
||||
);
|
||||
const diffMode = useDashboardStore((s) => s.diffMode);
|
||||
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
|
||||
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
|
||||
|
||||
const handleNodeSelect = useCallback(
|
||||
(nodeId: string) => {
|
||||
selectNode(nodeId);
|
||||
},
|
||||
[selectNode],
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!graph || graph.layers.length === 0)
|
||||
return { nodes: [] as Node[], edges: [] as Edge[] };
|
||||
|
||||
const allLayerNodeIds = new Set(
|
||||
graph.layers.flatMap((l) => l.nodeIds),
|
||||
);
|
||||
|
||||
const fileGraphNodes = graph.nodes.filter(
|
||||
(n) => n.type === "file" && allLayerNodeIds.has(n.id),
|
||||
);
|
||||
|
||||
const fileNodeIds = new Set(fileGraphNodes.map((n) => n.id));
|
||||
const neighborNodeIds = new Set<string>();
|
||||
if (selectedNodeId) {
|
||||
for (const edge of graph.edges) {
|
||||
if (edge.source === selectedNodeId && fileNodeIds.has(edge.target))
|
||||
neighborNodeIds.add(edge.target);
|
||||
if (edge.target === selectedNodeId && fileNodeIds.has(edge.source))
|
||||
neighborNodeIds.add(edge.source);
|
||||
}
|
||||
neighborNodeIds.add(selectedNodeId);
|
||||
}
|
||||
|
||||
const diffNodeIds = diffMode
|
||||
? new Set([...changedNodeIds, ...affectedNodeIds])
|
||||
: new Set<string>();
|
||||
|
||||
const flowNodes: CustomFlowNode[] = fileGraphNodes.map((node) => {
|
||||
const matchResult = searchResults.find((r) => r.nodeId === node.id);
|
||||
const hasSelection = !!selectedNodeId;
|
||||
return {
|
||||
id: node.id,
|
||||
type: "custom" as const,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: node.name ?? node.filePath?.split("/").pop() ?? node.id,
|
||||
nodeType: node.type,
|
||||
summary: node.summary,
|
||||
complexity: node.complexity,
|
||||
isHighlighted: !!matchResult,
|
||||
searchScore: matchResult?.score,
|
||||
isSelected: selectedNodeId === node.id,
|
||||
isTourHighlighted: tourHighlightedNodeIds.includes(node.id),
|
||||
isDiffChanged: diffMode && changedNodeIds.has(node.id),
|
||||
isDiffAffected: diffMode && affectedNodeIds.has(node.id),
|
||||
isDiffFaded:
|
||||
diffMode &&
|
||||
!changedNodeIds.has(node.id) &&
|
||||
!affectedNodeIds.has(node.id),
|
||||
isNeighbor:
|
||||
hasSelection &&
|
||||
neighborNodeIds.has(node.id) &&
|
||||
selectedNodeId !== node.id,
|
||||
isSelectionFaded: hasSelection && !neighborNodeIds.has(node.id),
|
||||
onNodeClick: handleNodeSelect,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const flowEdges: Edge[] = [];
|
||||
let edgeIdx = 0;
|
||||
for (const edge of graph.edges) {
|
||||
if (!fileNodeIds.has(edge.source) || !fileNodeIds.has(edge.target))
|
||||
continue;
|
||||
|
||||
const isSelectedEdge =
|
||||
!!selectedNodeId &&
|
||||
(edge.source === selectedNodeId || edge.target === selectedNodeId);
|
||||
const hasSelection = !!selectedNodeId;
|
||||
const sourceInDiff = diffMode && diffNodeIds.has(edge.source);
|
||||
const targetInDiff = diffMode && diffNodeIds.has(edge.target);
|
||||
const isImpacted = diffMode && (sourceInDiff || targetInDiff);
|
||||
|
||||
let edgeStyle: React.CSSProperties;
|
||||
let edgeLabelStyle: React.CSSProperties;
|
||||
let edgeAnimated: boolean;
|
||||
|
||||
if (isImpacted) {
|
||||
edgeStyle = {
|
||||
stroke:
|
||||
sourceInDiff && targetInDiff
|
||||
? "rgba(224, 82, 82, 0.7)"
|
||||
: "rgba(212, 160, 48, 0.5)",
|
||||
strokeWidth: 2.5,
|
||||
};
|
||||
edgeLabelStyle = { fill: "#a39787", fontSize: 10 };
|
||||
edgeAnimated = true;
|
||||
} else if (diffMode) {
|
||||
edgeStyle = { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 };
|
||||
edgeLabelStyle = { fill: "rgba(163,151,135,0.3)", fontSize: 10 };
|
||||
edgeAnimated = false;
|
||||
} else if (isSelectedEdge) {
|
||||
edgeStyle = { stroke: "rgba(212,165,116,0.8)", strokeWidth: 2.5 };
|
||||
edgeLabelStyle = { fill: "#d4a574", fontSize: 11, fontWeight: 600 };
|
||||
edgeAnimated = true;
|
||||
} else if (hasSelection) {
|
||||
edgeStyle = { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 };
|
||||
edgeLabelStyle = { fill: "rgba(163,151,135,0.2)", fontSize: 10 };
|
||||
edgeAnimated = false;
|
||||
} else {
|
||||
edgeStyle = { stroke: "rgba(212,165,116,0.25)", strokeWidth: 1 };
|
||||
edgeLabelStyle = { fill: "#a39787", fontSize: 9 };
|
||||
edgeAnimated = false;
|
||||
}
|
||||
|
||||
flowEdges.push({
|
||||
id: `fe-${edgeIdx++}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.type,
|
||||
animated: edgeAnimated,
|
||||
style: edgeStyle,
|
||||
labelStyle: edgeLabelStyle,
|
||||
});
|
||||
}
|
||||
|
||||
const result = applySwimLaneLayout(
|
||||
graph,
|
||||
flowNodes as unknown as Node[],
|
||||
flowEdges,
|
||||
);
|
||||
return { nodes: result.nodes, edges: result.edges };
|
||||
}, [
|
||||
graph,
|
||||
selectedNodeId,
|
||||
searchResults,
|
||||
tourHighlightedNodeIds,
|
||||
handleNodeSelect,
|
||||
diffMode,
|
||||
changedNodeIds,
|
||||
affectedNodeIds,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Main inner component (must be inside ReactFlowProvider) ────────────
|
||||
|
||||
function GraphViewInner() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const navigationLevel = useDashboardStore((s) => s.navigationLevel);
|
||||
const activeLayerId = useDashboardStore((s) => s.activeLayerId);
|
||||
const viewMode = useDashboardStore((s) => s.viewMode);
|
||||
const selectNode = useDashboardStore((s) => s.selectNode);
|
||||
const drillIntoLayer = useDashboardStore((s) => s.drillIntoLayer);
|
||||
const focusNodeId = useDashboardStore((s) => s.focusNodeId);
|
||||
@@ -586,14 +428,9 @@ function GraphViewInner() {
|
||||
|
||||
const overviewGraph = useOverviewGraph();
|
||||
const detailGraph = useLayerDetailGraph();
|
||||
const flowGraph = useFlowViewGraph();
|
||||
|
||||
const { nodes: initialNodes, edges: initialEdges } =
|
||||
viewMode === "flow"
|
||||
? flowGraph
|
||||
: navigationLevel === "overview"
|
||||
? overviewGraph
|
||||
: detailGraph;
|
||||
navigationLevel === "overview" ? overviewGraph : detailGraph;
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
@@ -608,22 +445,16 @@ function GraphViewInner() {
|
||||
setEdges(initialEdges);
|
||||
}, [initialEdges, setEdges]);
|
||||
|
||||
// Fit view on level/layer/view-mode transitions
|
||||
// Fit view on level/layer transitions
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
fitView({ duration: 400, padding: 0.2 });
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}, [navigationLevel, activeLayerId, viewMode, fitView]);
|
||||
}, [navigationLevel, activeLayerId, fitView]);
|
||||
|
||||
const onNodeClick = useCallback(
|
||||
(_: React.MouseEvent, node: { id: string }) => {
|
||||
// In flow view, all clicks are selections (no drill-in)
|
||||
if (viewMode === "flow") {
|
||||
if (node.id.startsWith("lane:")) return;
|
||||
selectNode(node.id);
|
||||
return;
|
||||
}
|
||||
if (navigationLevel === "overview") {
|
||||
drillIntoLayer(node.id);
|
||||
} else if (node.id.startsWith("portal:")) {
|
||||
@@ -633,7 +464,7 @@ function GraphViewInner() {
|
||||
selectNode(node.id);
|
||||
}
|
||||
},
|
||||
[viewMode, navigationLevel, drillIntoLayer, selectNode],
|
||||
[navigationLevel, drillIntoLayer, selectNode],
|
||||
);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
|
||||
export type Persona = "non-technical" | "junior" | "experienced";
|
||||
export type NavigationLevel = "overview" | "layer-detail";
|
||||
export type ViewMode = "graph" | "flow";
|
||||
|
||||
|
||||
/** Find which layer a node belongs to. Returns layerId or null. */
|
||||
function findNodeLayer(graph: KnowledgeGraph, nodeId: string): string | null {
|
||||
@@ -33,9 +33,6 @@ interface DashboardStore {
|
||||
navigationLevel: NavigationLevel;
|
||||
activeLayerId: string | null;
|
||||
|
||||
// View mode: graph (default free-form) or flow (swim-lane)
|
||||
viewMode: ViewMode;
|
||||
|
||||
codeViewerOpen: boolean;
|
||||
codeViewerNodeId: string | null;
|
||||
|
||||
@@ -65,7 +62,6 @@ interface DashboardStore {
|
||||
goBackNode: () => void;
|
||||
drillIntoLayer: (layerId: string) => void;
|
||||
navigateToOverview: () => void;
|
||||
setViewMode: (mode: ViewMode) => void;
|
||||
setFocusNode: (nodeId: string | null) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
toggleLayers: () => void;
|
||||
@@ -117,8 +113,6 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
|
||||
navigationLevel: "overview",
|
||||
activeLayerId: null,
|
||||
viewMode: "graph",
|
||||
|
||||
codeViewerOpen: false,
|
||||
codeViewerNodeId: null,
|
||||
|
||||
@@ -243,16 +237,6 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
codeViewerNodeId: null,
|
||||
}),
|
||||
|
||||
setViewMode: (mode) =>
|
||||
set({
|
||||
viewMode: mode,
|
||||
// When switching to flow view, go to overview level (flow shows all layers)
|
||||
navigationLevel: mode === "flow" ? "overview" : get().navigationLevel,
|
||||
activeLayerId: mode === "flow" ? null : get().activeLayerId,
|
||||
selectedNodeId: null,
|
||||
focusNodeId: null,
|
||||
}),
|
||||
|
||||
setFocusNode: (nodeId) => set({ focusNodeId: nodeId, selectedNodeId: nodeId }),
|
||||
setSearchMode: (mode) => set({ searchMode: mode }),
|
||||
setSearchQuery: (query) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import dagre from "@dagrejs/dagre";
|
||||
import type { Node, Edge } from "@xyflow/react";
|
||||
import type { KnowledgeGraph } from "@understand-anything/core/types";
|
||||
import type { LayoutMessage, LayoutResult } from "./layout.worker";
|
||||
|
||||
export const NODE_WIDTH = 280;
|
||||
@@ -10,12 +9,6 @@ export const LAYER_CLUSTER_HEIGHT = 180;
|
||||
export const PORTAL_NODE_WIDTH = 240;
|
||||
export const PORTAL_NODE_HEIGHT = 80;
|
||||
|
||||
// Swim-lane constants
|
||||
export const LANE_WIDTH = 320;
|
||||
export const LANE_GAP = 40;
|
||||
export const LANE_PADDING = 30;
|
||||
export const LANE_HEADER_HEIGHT = 40;
|
||||
|
||||
/**
|
||||
* Synchronous dagre layout — used for small graphs.
|
||||
*/
|
||||
@@ -144,146 +137,3 @@ export function applyDagreLayoutAsync(
|
||||
});
|
||||
}
|
||||
|
||||
// ── Swim-lane layout ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Preferred order of layers for the swim-lane flow view.
|
||||
* Reflects a typical request lifecycle: entry → middleware → logic → data → external.
|
||||
*/
|
||||
const LAYER_FLOW_ORDER = [
|
||||
"API Layer",
|
||||
"UI Layer",
|
||||
"Middleware Layer",
|
||||
"Service Layer",
|
||||
"External Services",
|
||||
"Data Layer",
|
||||
"Background Tasks",
|
||||
"Utility Layer",
|
||||
"Configuration Layer",
|
||||
"Test Layer",
|
||||
];
|
||||
|
||||
function getLayerSortIndex(layerName: string): number {
|
||||
const idx = LAYER_FLOW_ORDER.indexOf(layerName);
|
||||
return idx >= 0 ? idx : LAYER_FLOW_ORDER.length;
|
||||
}
|
||||
|
||||
export interface SwimLaneResult {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
lanes: Array<{ layerId: string; layerName: string; columnIndex: number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swim-lane layout: each layer becomes a vertical column (lane), ordered
|
||||
* by the request lifecycle. Within each lane, dagre handles vertical
|
||||
* positioning. File nodes are children of their lane group node.
|
||||
*/
|
||||
export function applySwimLaneLayout(
|
||||
graph: KnowledgeGraph,
|
||||
fileNodes: Node[],
|
||||
allEdges: Edge[],
|
||||
): SwimLaneResult {
|
||||
const sortedLayers = [...graph.layers].sort(
|
||||
(a, b) => getLayerSortIndex(a.name) - getLayerSortIndex(b.name),
|
||||
);
|
||||
|
||||
const nodeToLayerId = new Map<string, string>();
|
||||
for (const layer of sortedLayers) {
|
||||
for (const nid of layer.nodeIds) {
|
||||
nodeToLayerId.set(nid, layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
const nodesByLayer = new Map<string, Node[]>();
|
||||
for (const layer of sortedLayers) {
|
||||
nodesByLayer.set(layer.id, []);
|
||||
}
|
||||
for (const node of fileNodes) {
|
||||
const lid = nodeToLayerId.get(node.id);
|
||||
if (lid && nodesByLayer.has(lid)) {
|
||||
nodesByLayer.get(lid)!.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const laneHeights = new Map<string, number>();
|
||||
|
||||
for (const [layerId, nodes] of nodesByLayer) {
|
||||
if (nodes.length === 0) {
|
||||
laneHeights.set(layerId, LANE_HEADER_HEIGHT + LANE_PADDING * 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({ rankdir: "TB", nodesep: 20, ranksep: 40, marginx: 0, marginy: 0 });
|
||||
|
||||
const laneNodeIds = new Set(nodes.map((n) => n.id));
|
||||
for (const node of nodes) {
|
||||
g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||
}
|
||||
|
||||
for (const edge of allEdges) {
|
||||
if (laneNodeIds.has(edge.source) && laneNodeIds.has(edge.target)) {
|
||||
g.setEdge(edge.source, edge.target);
|
||||
}
|
||||
}
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
let maxY = 0;
|
||||
for (const node of nodes) {
|
||||
const pos = g.node(node.id);
|
||||
if (pos) {
|
||||
(node as Node & { _laneY: number })._laneY = pos.y - NODE_HEIGHT / 2;
|
||||
maxY = Math.max(maxY, pos.y + NODE_HEIGHT / 2);
|
||||
}
|
||||
}
|
||||
|
||||
laneHeights.set(layerId, LANE_HEADER_HEIGHT + maxY + LANE_PADDING * 2);
|
||||
}
|
||||
|
||||
const maxLaneHeight = Math.max(200, ...Array.from(laneHeights.values()));
|
||||
|
||||
const resultNodes: Node[] = [];
|
||||
const lanes: SwimLaneResult["lanes"] = [];
|
||||
|
||||
sortedLayers.forEach((layer, colIdx) => {
|
||||
const laneX = colIdx * (LANE_WIDTH + LANE_GAP);
|
||||
const laneId = `lane:${layer.id}`;
|
||||
|
||||
lanes.push({ layerId: layer.id, layerName: layer.name, columnIndex: colIdx });
|
||||
|
||||
resultNodes.push({
|
||||
id: laneId,
|
||||
type: "group",
|
||||
position: { x: laneX, y: 0 },
|
||||
data: { label: layer.name },
|
||||
style: {
|
||||
width: LANE_WIDTH,
|
||||
height: maxLaneHeight,
|
||||
backgroundColor: "rgba(212,165,116,0.03)",
|
||||
borderRadius: 12,
|
||||
border: "1px solid rgba(212,165,116,0.1)",
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const nodes = nodesByLayer.get(layer.id) ?? [];
|
||||
for (const node of nodes) {
|
||||
const laneY = (node as Node & { _laneY?: number })._laneY ?? 0;
|
||||
resultNodes.push({
|
||||
...node,
|
||||
parentId: laneId,
|
||||
extent: "parent" as const,
|
||||
position: {
|
||||
x: (LANE_WIDTH - NODE_WIDTH) / 2,
|
||||
y: LANE_HEADER_HEIGHT + LANE_PADDING + laneY,
|
||||
},
|
||||
});
|
||||
delete (node as Node & { _laneY?: number })._laneY;
|
||||
}
|
||||
});
|
||||
|
||||
return { nodes: resultNodes, edges: allEdges, lanes };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user