Skip to content

Commit f4774ed

Browse files
authored
feat: Add simple floating edge to relational diagram
Add simple floating edge to relational diagram
2 parents ce5de4b + edc0990 commit f4774ed

File tree

2 files changed

+201
-3
lines changed

2 files changed

+201
-3
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import {
2+
BaseEdge,
3+
getSmoothStepPath,
4+
Handle,
5+
InternalNode,
6+
Node,
7+
Position,
8+
useInternalNode,
9+
type Edge,
10+
type EdgeProps,
11+
} from "@xyflow/react";
12+
13+
// returns the position (top,right,bottom or right) passed node compared to
14+
function getParams(
15+
nodeA: InternalNode<Node>,
16+
nodeB: InternalNode<Node>,
17+
columnId: string | undefined | null
18+
): {
19+
x: number;
20+
y: number;
21+
position: Position;
22+
} {
23+
const centerA = getNodeCenter(nodeA);
24+
const centerB = getNodeCenter(nodeB);
25+
26+
const position = centerA.x > centerB.x ? Position.Left : Position.Right;
27+
28+
const { x, y } = getHandleCoordsByPosition(nodeA, position, columnId);
29+
return { x, y, position };
30+
}
31+
32+
function getHandleCoordsByPosition(
33+
node: InternalNode<Node>,
34+
handlePosition: Position,
35+
columnId: string | undefined | null
36+
): {
37+
x: number;
38+
y: number;
39+
} {
40+
if (
41+
!node.internals.handleBounds ||
42+
!node.internals.handleBounds.source ||
43+
!node.internals.handleBounds.target
44+
) {
45+
return {
46+
x: node.internals.positionAbsolute.x,
47+
y: node.internals.positionAbsolute.y,
48+
};
49+
}
50+
51+
let handle: Handle | undefined;
52+
53+
// According to erd-table-column.tsx, handle left type is target and right type is source
54+
switch (handlePosition) {
55+
case Position.Left:
56+
handle = node.internals.handleBounds.target.find(
57+
(h) => h.id === columnId
58+
);
59+
break;
60+
case Position.Right:
61+
handle = node.internals.handleBounds.source.find(
62+
(h) => h.id === columnId
63+
);
64+
break;
65+
}
66+
67+
if (!handle) {
68+
return {
69+
x: node.internals.positionAbsolute.x,
70+
y: node.internals.positionAbsolute.y,
71+
};
72+
}
73+
74+
let offsetX = handle.width / 2;
75+
let offsetY = handle.height / 2;
76+
77+
// this is a tiny detail to make the markerEnd of an edge visible.
78+
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
79+
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
80+
switch (handlePosition) {
81+
case Position.Left:
82+
offsetX = 0;
83+
break;
84+
case Position.Right:
85+
offsetX = handle.width;
86+
break;
87+
case Position.Top:
88+
offsetY = 0;
89+
break;
90+
case Position.Bottom:
91+
offsetY = handle.height;
92+
break;
93+
}
94+
95+
const x = node.internals.positionAbsolute.x + handle.x + offsetX;
96+
const y = node.internals.positionAbsolute.y + handle.y + offsetY;
97+
98+
return {
99+
x,
100+
y,
101+
};
102+
}
103+
104+
function getNodeCenter(node: InternalNode<Node>): {
105+
x: number;
106+
y: number;
107+
} {
108+
return {
109+
x: node.internals.positionAbsolute.x + (node.measured?.width ?? 0) / 2,
110+
y: node.internals.positionAbsolute.y + (node.measured?.height ?? 0) / 2,
111+
};
112+
}
113+
114+
export function getEdgeParams(
115+
source: InternalNode<Node>,
116+
target: InternalNode<Node>,
117+
sourceColumnId: string | undefined | null,
118+
targetColumnId: string | undefined | null
119+
): {
120+
sx: number;
121+
sy: number;
122+
tx: number;
123+
ty: number;
124+
sourcePos: Position;
125+
targetPos: Position;
126+
} {
127+
const {
128+
x: sx,
129+
y: sy,
130+
position: sourcePos,
131+
} = getParams(source, target, sourceColumnId);
132+
const {
133+
x: tx,
134+
y: ty,
135+
position: targetPos,
136+
} = getParams(target, source, targetColumnId);
137+
138+
return {
139+
sx,
140+
sy,
141+
tx,
142+
ty,
143+
sourcePos,
144+
targetPos,
145+
};
146+
}
147+
148+
interface ERDSchemaNodeColumnEdgeProps extends Edge {}
149+
150+
export default function ERDTableColumnEdge({
151+
id,
152+
source,
153+
target,
154+
sourceHandleId,
155+
targetHandleId,
156+
markerEnd,
157+
markerStart,
158+
style,
159+
}: EdgeProps<ERDSchemaNodeColumnEdgeProps>) {
160+
const sourceNode = useInternalNode(source);
161+
const targetNode = useInternalNode(target);
162+
163+
if (!sourceNode || !targetNode) {
164+
return null;
165+
}
166+
167+
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
168+
sourceNode,
169+
targetNode,
170+
sourceHandleId,
171+
targetHandleId
172+
);
173+
174+
const [edgePath] = getSmoothStepPath({
175+
sourceX: sx,
176+
sourceY: sy,
177+
sourcePosition: sourcePos,
178+
targetPosition: targetPos,
179+
targetX: tx,
180+
targetY: ty,
181+
});
182+
183+
return (
184+
<BaseEdge
185+
id={id}
186+
path={edgePath}
187+
markerEnd={markerEnd}
188+
markerStart={markerStart}
189+
style={style}
190+
/>
191+
);
192+
}

src/components/gui/tabs/relational-diagram-tab/index.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import {
2323
} from "@xyflow/react";
2424
import "@xyflow/react/dist/style.css";
2525
import { LucideRefreshCcw } from "lucide-react";
26+
import { useTheme } from "next-themes";
2627
import { useEffect, useState } from "react";
2728
import SchemaNameSelect from "../../schema-editor/schema-name-select";
2829
import { Toolbar } from "../../toolbar";
2930
import { DownloadImageDiagram } from "./download-image-diagram";
30-
import { useTheme } from "next-themes";
31+
import ERDTableColumnEdge from "./erd-table-column-edge";
3132

3233
const NODE_MARGIN = 50;
3334
const MAX_NODE_WIDTH = 300;
@@ -89,7 +90,7 @@ function mapSchema(
8990
foreignKeyList.add(`${item.name}.${column.name}`);
9091

9192
initialEdges.push({
92-
type: "smoothstep",
93+
type: "erdTableColumn",
9394
markerEnd: {
9495
type: MarkerType.ArrowClosed,
9596
width: 14,
@@ -129,7 +130,7 @@ function mapSchema(
129130
foreignKeyList.add(`${item.name}.${columnName}`);
130131

131132
initialEdges.push({
132-
type: "smoothstep",
133+
type: "erdTableColumn",
133134
markerEnd: {
134135
type: MarkerType.ArrowClosed,
135136
width: 14,
@@ -299,6 +300,10 @@ function LayoutFlow() {
299300
databaseSchema: DatabaseSchemaNode,
300301
};
301302

303+
const edgeTypes = {
304+
erdTableColumn: ERDTableColumnEdge,
305+
};
306+
302307
const appTheme = (forcedTheme ?? resolvedTheme) as "dark" | "light";
303308

304309
return (
@@ -379,6 +384,7 @@ function LayoutFlow() {
379384
onEdgesChange={onEdgesChange}
380385
fitView
381386
nodeTypes={nodeTypes}
387+
edgeTypes={edgeTypes}
382388
>
383389
<Background bgColor={appTheme === "dark" ? "#0a0a0a" : "white"} />
384390
<Controls />

0 commit comments

Comments
 (0)