From 1668792f1c6fa9f7b4951e6d40b9cefbba897767 Mon Sep 17 00:00:00 2001 From: Levy van der Valk Date: Wed, 16 Oct 2024 15:18:40 +0200 Subject: [PATCH 01/10] feat: initial version of elk based pathing --- .../designer/TableGraphPane/edges/ElkEdge.tsx | 132 ++++++++++++++++++ .../views/designer/TableGraphPane/helpers.tsx | 38 +++-- .../views/designer/TableGraphPane/index.tsx | 76 +++++++++- 3 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx diff --git a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx new file mode 100644 index 00000000..0f2fdd7d --- /dev/null +++ b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx @@ -0,0 +1,132 @@ +import { useMemo } from "react"; +import { BaseEdge, EdgeProps, SmoothStepEdge } from "reactflow"; + +interface EdgeData extends Object { + data: any; + elkData?: { + bendSections: import("elkjs/lib/elk.bundled").ElkEdgeSection[]; + isDragged: boolean; + } +} + +export function ElkStepEdge({ + sourceX, + sourceY, + targetX, + targetY, + data, + ...rest +}: EdgeProps) { + const bendSection = useMemo(() => !!data?.elkData?.bendSections ? data.elkData.bendSections[0] : undefined, [data?.elkData?.bendSections]); + + const edgePath = useMemo(() => { + if (!bendSection) { + return `M${sourceX},${sourceY} L${targetX},${targetY}`; + } + + const bendRadius = 8; + + const bends: string = bendSection.bendPoints?.map((v, index, arr) => { + const lastSection = arr[index - 1] ?? bendSection.startPoint; + const nextSection = arr[index + 1] ?? bendSection.endPoint; + + if ( + v.x === lastSection.x && + nextSection.x > v.x + ) { + if (v.y < lastSection.y) { + // bends to the right from the bottom + return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; + } + + // bends to the right from the top + return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; + } else if ( + v.x === lastSection.x && + nextSection.x < v.x + ) { + if (v.y < lastSection.y) { + // bends to the left from the bottom + return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; + } + + // bends to the left from the top + return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; + } else if ( + v.y === lastSection.y && + nextSection.y > v.y + ) { + if (v.x < lastSection.x) { + // bends down from the right + return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; + } + + // bends down from the left + return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; + } else if ( + v.y === lastSection.y && + nextSection.y < v.y + ) { + if (v.x < lastSection.x) { + // bends up from the right + return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; + } + + // bends up from the left + return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; + } + + return `L${v.x},${v.y}`; + }).join(' ') || ""; + + return `M${bendSection.startPoint.x},${bendSection.startPoint.y} ${bends} L${bendSection.endPoint.x},${bendSection.endPoint.y}`; + }, [bendSection]); + + const labelPosition = useMemo(() => { + if (!bendSection) { + return { + x: sourceX + (targetX - sourceX) / 2, + y: sourceY + (targetY - sourceY) / 2, + }; + } + + const position = { + x: bendSection.startPoint.x + (bendSection.endPoint.x - bendSection.startPoint.x) / 2, + y: bendSection.startPoint.y + (bendSection.endPoint.y - bendSection.startPoint.y) / 2, + }; + + if (bendSection.bendPoints && bendSection.bendPoints.length > 0) { + const firstMiddleBendLocation = Math.floor(bendSection.bendPoints.length / 2) - 1; + const firstMiddleBend = bendSection.bendPoints[firstMiddleBendLocation]; + const lastMiddleBendLocation = Math.ceil(bendSection.bendPoints.length / 2) - 1; + const lastMiddleBend = lastMiddleBendLocation === firstMiddleBendLocation ? bendSection.bendPoints[lastMiddleBendLocation + 1] : bendSection.bendPoints[lastMiddleBendLocation]; + + position.x = firstMiddleBend.x + (lastMiddleBend.x - firstMiddleBend.x) / 2; + position.y = firstMiddleBend.y + (lastMiddleBend.y - firstMiddleBend.y) / 2; + } + + return position; + }, [bendSection]); + + if (!bendSection || data?.elkData?.isDragged) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/screens/database/views/designer/TableGraphPane/helpers.tsx b/src/screens/database/views/designer/TableGraphPane/helpers.tsx index 54059807..488f10d6 100644 --- a/src/screens/database/views/designer/TableGraphPane/helpers.tsx +++ b/src/screens/database/views/designer/TableGraphPane/helpers.tsx @@ -7,6 +7,7 @@ import { extractKindRecords } from "~/util/surrealql"; import { EdgeNode } from "./nodes/EdgeNode"; import { TableNode } from "./nodes/TableNode"; import classes from "./style.module.scss"; +import { ElkStepEdge } from "./edges/ElkEdge"; type EdgeWarning = { type: "edge"; @@ -26,6 +27,10 @@ export const NODE_TYPES = { edge: EdgeNode, }; +export const EDGE_TYPES = { + elk: ElkStepEdge, +}; + export type InternalNode = Node & { width: number; height: number }; export type GraphWarning = EdgeWarning | LinkWarning; @@ -74,7 +79,7 @@ export function buildFlowNodes( switch (lineStyle) { case "metro": { - baseEdge.type = "smoothstep"; + baseEdge.type = "elk"; baseEdge.pathOptions = { borderRadius: 50 }; break; } @@ -254,9 +259,9 @@ export async function applyNodeLayout( nodes: DimensionNode[], edges: Edge[], direction: DiagramDirection, -): Promise { +): Promise<[NodeChange[], { id: string, bendSections: any[] }[]]> { if (nodes.some((node) => !node.width || !node.height)) { - return []; + return [[], []]; } const ELK = await import("elkjs/lib/elk.bundled"); @@ -285,17 +290,24 @@ export async function applyNodeLayout( }); const children = layout.children || []; - - return children.map(({ id, x, y }) => { - return { + const layoutEdges = layout.edges || []; + + return [ + children.map(({ id, x, y }) => { + return { + id, + type: "position", + position: { + x: x ?? 0, + y: y ?? 0, + }, + }; + }), + layoutEdges.map(({ id, sections }) => ({ id, - type: "position", - position: { - x: x ?? 0, - y: y ?? 0, - }, - }; - }); + bendSections: sections || [] + })), + ]; } /** diff --git a/src/screens/database/views/designer/TableGraphPane/index.tsx b/src/screens/database/views/designer/TableGraphPane/index.tsx index 7b1529ce..fb7f7f48 100644 --- a/src/screens/database/views/designer/TableGraphPane/index.tsx +++ b/src/screens/database/views/designer/TableGraphPane/index.tsx @@ -21,6 +21,7 @@ import { type ElementRef, useEffect, useLayoutEffect, + useMemo, useRef, useState, } from "react"; @@ -48,6 +49,7 @@ import { } from "~/util/icons"; import { + EDGE_TYPES, type GraphWarning, NODE_TYPES, applyNodeLayout, @@ -74,6 +76,7 @@ import type { DiagramDirection, DiagramMode, TableInfo } from "~/types"; import { showInfo } from "~/util/helpers"; import { themeColor } from "~/util/mantine"; import { GraphWarningLine } from "./components"; +import { getSetting } from "~/util/config"; export interface TableGraphPaneProps { active: string | null; @@ -131,7 +134,28 @@ export function TableGraphPane(props: TableGraphPaneProps) { if (changes.length > 0) { doFitRef.current = true; - handleOnNodesChange(layoutChanges); + handleOnNodesChange(layoutChanges[0]); + + setEdges((prev) => { + return prev.map((curr) => { + const found = layoutChanges[1].find((edge) => edge.id === curr.id); + + if (!found) { + return curr; + } + + return { + ...curr, + data: { + ...curr.data, + elkData: { + bendSections: found.bendSections, + isDragged: false, + } + } + }; + }) + }); fitView(); } } @@ -371,10 +395,58 @@ export function TableGraphPane(props: TableGraphPaneProps) { edges={edges} minZoom={0.1} nodeTypes={NODE_TYPES} + edgeTypes={EDGE_TYPES} nodesConnectable={false} edgesFocusable={false} proOptions={{ hideAttribution: true }} - onNodesChange={onNodesChange} + onNodesChange={(c) => { + onNodesChange(c); + + const lineStyle = getSetting("appearance", "lineStyle"); + + if (lineStyle !== "metro") { + return; + } + + const changes = new Set(); + + for (const change of c) { + if (change.type === "position") { + const node = nodes.find(({ id }) => id === change.id); + + if (!node) { + continue; + } + + const edgesToChange = edges.filter(({ target, source }) => target === node.id || source === node.id).map(({ id }) => id); + + for (const edge of edgesToChange) { + changes.add(edge); + } + } + } + + if (changes.size === 0) { + return; + } + + setEdges((prev) => prev.map((edge) => { + if (!changes.has(edge.id)) { + return edge; + } + + return { + ...edge, + data: { + ...edge.data, + elkData: edge.data.elkData ? { + ...edge.data.elkData, + isDragged: true, + } : undefined, + }, + } + })); + }} onEdgesChange={onEdgesChange} className={classes.diagram} style={{ opacity: computing ? 0 : 1 }} From 4bd7357d095d626fbcfe1929f63861138b5bb948 Mon Sep 17 00:00:00 2001 From: Levy van der Valk Date: Wed, 16 Oct 2024 17:09:30 +0200 Subject: [PATCH 02/10] change: added dynamic bend radius --- .../views/designer/TableGraphPane/edges/ElkEdge.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx index 0f2fdd7d..1c0e03f6 100644 --- a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx +++ b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx @@ -24,12 +24,18 @@ export function ElkStepEdge({ return `M${sourceX},${sourceY} L${targetX},${targetY}`; } - const bendRadius = 8; - const bends: string = bendSection.bendPoints?.map((v, index, arr) => { const lastSection = arr[index - 1] ?? bendSection.startPoint; const nextSection = arr[index + 1] ?? bendSection.endPoint; + const lastLength = Math.max(Math.abs(lastSection.x - v.x), Math.abs(lastSection.y - v.y)); + const nextLength = Math.max(Math.abs(nextSection.x - v.x), Math.abs(nextSection.y - v.y)); + + const _bend = Math.min(lastLength, nextLength) / 2; + const maxBendRadius = 36; + + const bendRadius = _bend < maxBendRadius ? _bend : maxBendRadius; + if ( v.x === lastSection.x && nextSection.x > v.x From ab5015dc88b59dd0fb9120677c53f54278c9cb64 Mon Sep 17 00:00:00 2001 From: Levy van der Valk Date: Wed, 16 Oct 2024 17:14:24 +0200 Subject: [PATCH 03/10] change: added min bend radius --- .../views/designer/TableGraphPane/edges/ElkEdge.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx index 1c0e03f6..722c4074 100644 --- a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx +++ b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx @@ -25,6 +25,9 @@ export function ElkStepEdge({ } const bends: string = bendSection.bendPoints?.map((v, index, arr) => { + const minBendRadius = 8; + const maxBendRadius = 36; + const lastSection = arr[index - 1] ?? bendSection.startPoint; const nextSection = arr[index + 1] ?? bendSection.endPoint; @@ -32,9 +35,7 @@ export function ElkStepEdge({ const nextLength = Math.max(Math.abs(nextSection.x - v.x), Math.abs(nextSection.y - v.y)); const _bend = Math.min(lastLength, nextLength) / 2; - const maxBendRadius = 36; - - const bendRadius = _bend < maxBendRadius ? _bend : maxBendRadius; + const bendRadius = Math.max(Math.min(_bend, maxBendRadius), minBendRadius); if ( v.x === lastSection.x && From 091f0c07738fc25ef4186a5c8a571e66a2b79207 Mon Sep 17 00:00:00 2001 From: Levy van der Valk Date: Thu, 17 Oct 2024 10:27:29 +0200 Subject: [PATCH 04/10] fix: allow rounding errors in checking --- .../designer/TableGraphPane/edges/ElkEdge.tsx | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx index 722c4074..09d37615 100644 --- a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx +++ b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx @@ -37,52 +37,74 @@ export function ElkStepEdge({ const _bend = Math.min(lastLength, nextLength) / 2; const bendRadius = Math.max(Math.min(_bend, maxBendRadius), minBendRadius); + // NOTE: values are rounded because the values can differ by a very small amount + const lastRounded = { + x: Math.round(lastSection.x), + y: Math.round(lastSection.y), + }; + + const currentRounded = { + x: Math.round(v.x), + y: Math.round(v.y), + } + + const nextRounded = { + x: Math.round(nextSection.x), + y: Math.round(nextSection.y), + }; + if ( - v.x === lastSection.x && - nextSection.x > v.x + currentRounded.x === lastRounded.x && + nextRounded.x > currentRounded.x ) { - if (v.y < lastSection.y) { - // bends to the right from the bottom + // Bend to the right + if (lastRounded.y > currentRounded.y) { + // From the bottom return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; } - // bends to the right from the top + // From the top return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; } else if ( - v.x === lastSection.x && - nextSection.x < v.x + currentRounded.x === lastRounded.x && + nextRounded.x < currentRounded.x ) { - if (v.y < lastSection.y) { - // bends to the left from the bottom + // Bend to the left + if (lastRounded.y > currentRounded.y) { + // From the bottom return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; } - // bends to the left from the top + // From the top return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; } else if ( - v.y === lastSection.y && - nextSection.y > v.y + currentRounded.y === lastRounded.y && + nextRounded.y > currentRounded.y ) { - if (v.x < lastSection.x) { - // bends down from the right + // Bend to the bottom + if (lastRounded.x > currentRounded.x) { + // From the right return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; } - // bends down from the left + // From the left return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; } else if ( - v.y === lastSection.y && - nextSection.y < v.y + currentRounded.y === lastRounded.y && + nextRounded.y < currentRounded.y ) { - if (v.x < lastSection.x) { - // bends up from the right + // Bend to the top + if (lastRounded.x > currentRounded.x) { + // From the right return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; } - // bends up from the left + // From the left return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; } + console.error("Unknown bend direction", lastRounded, currentRounded, nextRounded); + return `L${v.x},${v.y}`; }).join(' ') || ""; From 5e37db84d8ec07f41b4f192ad3d863676d24786c Mon Sep 17 00:00:00 2001 From: Levy van der Valk Date: Thu, 17 Oct 2024 10:36:09 +0200 Subject: [PATCH 05/10] fix: linting --- .../designer/TableGraphPane/edges/ElkEdge.tsx | 22 ++++++++++++------- .../views/designer/TableGraphPane/helpers.tsx | 2 +- .../views/designer/TableGraphPane/index.tsx | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx index 09d37615..08b69da1 100644 --- a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx +++ b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; -import { BaseEdge, EdgeProps, SmoothStepEdge } from "reactflow"; +import { BaseEdge, type EdgeProps, SmoothStepEdge } from "reactflow"; -interface EdgeData extends Object { +interface EdgeData extends Record { data: any; elkData?: { bendSections: import("elkjs/lib/elk.bundled").ElkEdgeSection[]; @@ -17,7 +17,7 @@ export function ElkStepEdge({ data, ...rest }: EdgeProps) { - const bendSection = useMemo(() => !!data?.elkData?.bendSections ? data.elkData.bendSections[0] : undefined, [data?.elkData?.bendSections]); + const bendSection = useMemo(() => data?.elkData?.bendSections ? data.elkData.bendSections[0] : undefined, [data?.elkData?.bendSections]); const edgePath = useMemo(() => { if (!bendSection) { @@ -65,7 +65,9 @@ export function ElkStepEdge({ // From the top return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; - } else if ( + } + + if ( currentRounded.x === lastRounded.x && nextRounded.x < currentRounded.x ) { @@ -77,7 +79,9 @@ export function ElkStepEdge({ // From the top return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; - } else if ( + } + + if ( currentRounded.y === lastRounded.y && nextRounded.y > currentRounded.y ) { @@ -89,7 +93,9 @@ export function ElkStepEdge({ // From the left return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; - } else if ( + } + + if ( currentRounded.y === lastRounded.y && nextRounded.y < currentRounded.y ) { @@ -109,7 +115,7 @@ export function ElkStepEdge({ }).join(' ') || ""; return `M${bendSection.startPoint.x},${bendSection.startPoint.y} ${bends} L${bendSection.endPoint.x},${bendSection.endPoint.y}`; - }, [bendSection]); + }, [bendSection, sourceX, sourceY, targetX, targetY]); const labelPosition = useMemo(() => { if (!bendSection) { @@ -135,7 +141,7 @@ export function ElkStepEdge({ } return position; - }, [bendSection]); + }, [bendSection, sourceX, sourceY, targetX, targetY]); if (!bendSection || data?.elkData?.isDragged) { return ( diff --git a/src/screens/database/views/designer/TableGraphPane/helpers.tsx b/src/screens/database/views/designer/TableGraphPane/helpers.tsx index 488f10d6..5662f788 100644 --- a/src/screens/database/views/designer/TableGraphPane/helpers.tsx +++ b/src/screens/database/views/designer/TableGraphPane/helpers.tsx @@ -4,10 +4,10 @@ import type { DiagramDirection, TableInfo } from "~/types"; import { getSetting } from "~/util/config"; import { extractEdgeRecords } from "~/util/schema"; import { extractKindRecords } from "~/util/surrealql"; +import { ElkStepEdge } from "./edges/ElkEdge"; import { EdgeNode } from "./nodes/EdgeNode"; import { TableNode } from "./nodes/TableNode"; import classes from "./style.module.scss"; -import { ElkStepEdge } from "./edges/ElkEdge"; type EdgeWarning = { type: "edge"; diff --git a/src/screens/database/views/designer/TableGraphPane/index.tsx b/src/screens/database/views/designer/TableGraphPane/index.tsx index fb7f7f48..1919c362 100644 --- a/src/screens/database/views/designer/TableGraphPane/index.tsx +++ b/src/screens/database/views/designer/TableGraphPane/index.tsx @@ -73,10 +73,10 @@ import { useIsLight } from "~/hooks/theme"; import { useConfigStore } from "~/stores/config"; import { useInterfaceStore } from "~/stores/interface"; import type { DiagramDirection, DiagramMode, TableInfo } from "~/types"; +import { getSetting } from "~/util/config"; import { showInfo } from "~/util/helpers"; import { themeColor } from "~/util/mantine"; import { GraphWarningLine } from "./components"; -import { getSetting } from "~/util/config"; export interface TableGraphPaneProps { active: string | null; From 81f68d33aa443044f036f91f77df185c8c3c2f87 Mon Sep 17 00:00:00 2001 From: Levy van der Valk Date: Thu, 17 Oct 2024 14:18:12 +0200 Subject: [PATCH 06/10] change: moved drag change detection to onNodesChange callback --- .../views/designer/TableGraphPane/index.tsx | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/src/screens/database/views/designer/TableGraphPane/index.tsx b/src/screens/database/views/designer/TableGraphPane/index.tsx index 1919c362..dab621b8 100644 --- a/src/screens/database/views/designer/TableGraphPane/index.tsx +++ b/src/screens/database/views/designer/TableGraphPane/index.tsx @@ -159,6 +159,51 @@ export function TableGraphPane(props: TableGraphPaneProps) { fitView(); } } + + const lineStyle = getSetting("appearance", "lineStyle"); + + if (lineStyle !== "metro") { + return; + } + + const uniqueChanges = new Set(); + + for (const change of changes) { + if (change.type === "position") { + const node = nodes.find(({ id }) => id === change.id); + + if (!node) { + continue; + } + + const edgesToChange = edges.filter(({ target, source }) => target === node.id || source === node.id).map(({ id }) => id); + + for (const edge of edgesToChange) { + uniqueChanges.add(edge); + } + } + } + + if (uniqueChanges.size === 0) { + return; + } + + setEdges((prev) => prev.map((edge) => { + if (!uniqueChanges.has(edge.id)) { + return edge; + } + + return { + ...edge, + data: { + ...edge.data, + elkData: edge.data.elkData ? { + ...edge.data.elkData, + isDragged: true, + } : undefined, + }, + } + })); }); // biome-ignore lint/correctness/useExhaustiveDependencies: Fit view when nodes change @@ -399,54 +444,7 @@ export function TableGraphPane(props: TableGraphPaneProps) { nodesConnectable={false} edgesFocusable={false} proOptions={{ hideAttribution: true }} - onNodesChange={(c) => { - onNodesChange(c); - - const lineStyle = getSetting("appearance", "lineStyle"); - - if (lineStyle !== "metro") { - return; - } - - const changes = new Set(); - - for (const change of c) { - if (change.type === "position") { - const node = nodes.find(({ id }) => id === change.id); - - if (!node) { - continue; - } - - const edgesToChange = edges.filter(({ target, source }) => target === node.id || source === node.id).map(({ id }) => id); - - for (const edge of edgesToChange) { - changes.add(edge); - } - } - } - - if (changes.size === 0) { - return; - } - - setEdges((prev) => prev.map((edge) => { - if (!changes.has(edge.id)) { - return edge; - } - - return { - ...edge, - data: { - ...edge.data, - elkData: edge.data.elkData ? { - ...edge.data.elkData, - isDragged: true, - } : undefined, - }, - } - })); - }} + onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} className={classes.diagram} style={{ opacity: computing ? 0 : 1 }} From 30a81e8430f9fc70b50ee508af09bf138757a2fb Mon Sep 17 00:00:00 2001 From: Julian Mills Date: Thu, 17 Oct 2024 14:54:50 +0200 Subject: [PATCH 07/10] change: simplify data --- .../designer/TableGraphPane/edges/ElkEdge.tsx | 198 +++++++++--------- .../views/designer/TableGraphPane/helpers.tsx | 14 +- .../views/designer/TableGraphPane/index.tsx | 43 ++-- .../TableGraphPane/nodes/BaseNode.tsx | 100 +++++---- .../TableGraphPane/nodes/EdgeNode.tsx | 8 +- .../TableGraphPane/nodes/TableNode.tsx | 8 +- 6 files changed, 199 insertions(+), 172 deletions(-) diff --git a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx index 08b69da1..a484e942 100644 --- a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx +++ b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx @@ -1,13 +1,6 @@ import { useMemo } from "react"; import { BaseEdge, type EdgeProps, SmoothStepEdge } from "reactflow"; - -interface EdgeData extends Record { - data: any; - elkData?: { - bendSections: import("elkjs/lib/elk.bundled").ElkEdgeSection[]; - isDragged: boolean; - } -} +import type { EdgeData } from "../helpers"; export function ElkStepEdge({ sourceX, @@ -17,102 +10,104 @@ export function ElkStepEdge({ data, ...rest }: EdgeProps) { - const bendSection = useMemo(() => data?.elkData?.bendSections ? data.elkData.bendSections[0] : undefined, [data?.elkData?.bendSections]); + const bendSection = data?.path; const edgePath = useMemo(() => { if (!bendSection) { return `M${sourceX},${sourceY} L${targetX},${targetY}`; } - const bends: string = bendSection.bendPoints?.map((v, index, arr) => { - const minBendRadius = 8; - const maxBendRadius = 36; - - const lastSection = arr[index - 1] ?? bendSection.startPoint; - const nextSection = arr[index + 1] ?? bendSection.endPoint; - - const lastLength = Math.max(Math.abs(lastSection.x - v.x), Math.abs(lastSection.y - v.y)); - const nextLength = Math.max(Math.abs(nextSection.x - v.x), Math.abs(nextSection.y - v.y)); - - const _bend = Math.min(lastLength, nextLength) / 2; - const bendRadius = Math.max(Math.min(_bend, maxBendRadius), minBendRadius); - - // NOTE: values are rounded because the values can differ by a very small amount - const lastRounded = { - x: Math.round(lastSection.x), - y: Math.round(lastSection.y), - }; - - const currentRounded = { - x: Math.round(v.x), - y: Math.round(v.y), - } - - const nextRounded = { - x: Math.round(nextSection.x), - y: Math.round(nextSection.y), - }; - - if ( - currentRounded.x === lastRounded.x && - nextRounded.x > currentRounded.x - ) { - // Bend to the right - if (lastRounded.y > currentRounded.y) { - // From the bottom - return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; - } - - // From the top - return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; - } - - if ( - currentRounded.x === lastRounded.x && - nextRounded.x < currentRounded.x - ) { - // Bend to the left - if (lastRounded.y > currentRounded.y) { - // From the bottom - return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; - } - - // From the top - return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; - } - - if ( - currentRounded.y === lastRounded.y && - nextRounded.y > currentRounded.y - ) { - // Bend to the bottom - if (lastRounded.x > currentRounded.x) { - // From the right - return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; - } - - // From the left - return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; - } - - if ( - currentRounded.y === lastRounded.y && - nextRounded.y < currentRounded.y - ) { - // Bend to the top - if (lastRounded.x > currentRounded.x) { - // From the right - return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; - } - - // From the left - return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; - } - - console.error("Unknown bend direction", lastRounded, currentRounded, nextRounded); - - return `L${v.x},${v.y}`; - }).join(' ') || ""; + const bends: string = + bendSection.bendPoints + ?.map((v, index, arr) => { + const minBendRadius = 8; + const maxBendRadius = 36; + + const lastSection = arr[index - 1] ?? bendSection.startPoint; + const nextSection = arr[index + 1] ?? bendSection.endPoint; + + const lastLength = Math.max( + Math.abs(lastSection.x - v.x), + Math.abs(lastSection.y - v.y), + ); + const nextLength = Math.max( + Math.abs(nextSection.x - v.x), + Math.abs(nextSection.y - v.y), + ); + + const _bend = Math.min(lastLength, nextLength) / 2; + const bendRadius = Math.max(Math.min(_bend, maxBendRadius), minBendRadius); + + // NOTE: values are rounded because the values can differ by a very small amount + const lastRounded = { + x: Math.round(lastSection.x), + y: Math.round(lastSection.y), + }; + + const currentRounded = { + x: Math.round(v.x), + y: Math.round(v.y), + }; + + const nextRounded = { + x: Math.round(nextSection.x), + y: Math.round(nextSection.y), + }; + + if (currentRounded.x === lastRounded.x && nextRounded.x > currentRounded.x) { + // Bend to the right + if (lastRounded.y > currentRounded.y) { + // From the bottom + return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; + } + + // From the top + return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x + bendRadius},${v.y}`; + } + + if (currentRounded.x === lastRounded.x && nextRounded.x < currentRounded.x) { + // Bend to the left + if (lastRounded.y > currentRounded.y) { + // From the bottom + return `L${v.x},${v.y + bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; + } + + // From the top + return `L${v.x},${v.y - bendRadius} Q${v.x},${v.y}, ${v.x - bendRadius},${v.y}`; + } + + if (currentRounded.y === lastRounded.y && nextRounded.y > currentRounded.y) { + // Bend to the bottom + if (lastRounded.x > currentRounded.x) { + // From the right + return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; + } + + // From the left + return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y + bendRadius}`; + } + + if (currentRounded.y === lastRounded.y && nextRounded.y < currentRounded.y) { + // Bend to the top + if (lastRounded.x > currentRounded.x) { + // From the right + return `L${v.x + bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; + } + + // From the left + return `L${v.x - bendRadius},${v.y} Q${v.x},${v.y}, ${v.x},${v.y - bendRadius}`; + } + + console.error( + "Unknown bend direction", + lastRounded, + currentRounded, + nextRounded, + ); + + return `L${v.x},${v.y}`; + }) + .join(" ") || ""; return `M${bendSection.startPoint.x},${bendSection.startPoint.y} ${bends} L${bendSection.endPoint.x},${bendSection.endPoint.y}`; }, [bendSection, sourceX, sourceY, targetX, targetY]); @@ -134,7 +129,10 @@ export function ElkStepEdge({ const firstMiddleBendLocation = Math.floor(bendSection.bendPoints.length / 2) - 1; const firstMiddleBend = bendSection.bendPoints[firstMiddleBendLocation]; const lastMiddleBendLocation = Math.ceil(bendSection.bendPoints.length / 2) - 1; - const lastMiddleBend = lastMiddleBendLocation === firstMiddleBendLocation ? bendSection.bendPoints[lastMiddleBendLocation + 1] : bendSection.bendPoints[lastMiddleBendLocation]; + const lastMiddleBend = + lastMiddleBendLocation === firstMiddleBendLocation + ? bendSection.bendPoints[lastMiddleBendLocation + 1] + : bendSection.bendPoints[lastMiddleBendLocation]; position.x = firstMiddleBend.x + (lastMiddleBend.x - firstMiddleBend.x) / 2; position.y = firstMiddleBend.y + (lastMiddleBend.y - firstMiddleBend.y) / 2; @@ -143,7 +141,7 @@ export function ElkStepEdge({ return position; }, [bendSection, sourceX, sourceY, targetX, targetY]); - if (!bendSection || data?.elkData?.isDragged) { + if (!bendSection || data?.isDragged) { return ( { +): Promise<[NodeChange[], NodeEdge[]]> { if (nodes.some((node) => !node.width || !node.height)) { return [[], []]; } @@ -305,7 +313,7 @@ export async function applyNodeLayout( }), layoutEdges.map(({ id, sections }) => ({ id, - bendSections: sections || [] + path: sections?.[0], })), ]; } diff --git a/src/screens/database/views/designer/TableGraphPane/index.tsx b/src/screens/database/views/designer/TableGraphPane/index.tsx index dab621b8..53d5ac6f 100644 --- a/src/screens/database/views/designer/TableGraphPane/index.tsx +++ b/src/screens/database/views/designer/TableGraphPane/index.tsx @@ -134,6 +134,7 @@ export function TableGraphPane(props: TableGraphPaneProps) { if (changes.length > 0) { doFitRef.current = true; + handleOnNodesChange(layoutChanges[0]); setEdges((prev) => { @@ -148,14 +149,13 @@ export function TableGraphPane(props: TableGraphPaneProps) { ...curr, data: { ...curr.data, - elkData: { - bendSections: found.bendSections, - isDragged: false, - } - } + path: found.path, + isDragged: false, + }, }; - }) + }); }); + fitView(); } } @@ -176,7 +176,9 @@ export function TableGraphPane(props: TableGraphPaneProps) { continue; } - const edgesToChange = edges.filter(({ target, source }) => target === node.id || source === node.id).map(({ id }) => id); + const edgesToChange = edges + .filter(({ target, source }) => target === node.id || source === node.id) + .map(({ id }) => id); for (const edge of edgesToChange) { uniqueChanges.add(edge); @@ -188,22 +190,21 @@ export function TableGraphPane(props: TableGraphPaneProps) { return; } - setEdges((prev) => prev.map((edge) => { - if (!uniqueChanges.has(edge.id)) { - return edge; - } + setEdges((prev) => + prev.map((edge) => { + if (!uniqueChanges.has(edge.id)) { + return edge; + } - return { - ...edge, - data: { - ...edge.data, - elkData: edge.data.elkData ? { - ...edge.data.elkData, + return { + ...edge, + data: { + ...edge.data, isDragged: true, - } : undefined, - }, - } - })); + }, + }; + }), + ); }); // biome-ignore lint/correctness/useExhaustiveDependencies: Fit view when nodes change diff --git a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx index 2fa6801f..28448d5d 100644 --- a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx +++ b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx @@ -1,14 +1,4 @@ -import { - Box, - Divider, - Flex, - Group, - Paper, - ScrollArea, - Stack, - Text, - Tooltip, -} from "@mantine/core"; +import { Box, Divider, Flex, Group, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core"; import { type MouseEvent, type ReactNode, useRef } from "react"; import { Handle, Position } from "reactflow"; import { Icon } from "~/components/Icon"; @@ -34,11 +24,17 @@ function Summary(props: SummaryProps) { const valueColor = props.value > 0 ? "surreal" : "dimmed"; return ( - + {props.title} - + {props.value} @@ -53,7 +49,12 @@ function FieldKind({ kind }: FieldKindProps) { const simpleKind = simplifyKind(kind); const value = ( - + {simpleKind} ); @@ -67,7 +68,10 @@ function FieldKind({ kind }: FieldKindProps) { position="top" openDelay={0} label={ - + {kind} } @@ -85,8 +89,16 @@ interface FieldProps { function Field({ isLight, name, value }: FieldProps) { return ( - - + + {name} {value} @@ -117,7 +129,10 @@ function Fields(props: FieldsProps) { }); return ( - + - + {fields.map((field) => ( ) : ( - + none ) @@ -152,6 +174,7 @@ function Fields(props: FieldsProps) { } interface BaseNodeProps { + id: string; icon: string; table: TableInfo; isSelected: boolean; @@ -161,6 +184,7 @@ interface BaseNodeProps { } export function BaseNode({ + id, icon, table, isSelected, @@ -173,8 +197,7 @@ export function BaseNode({ const isLight = useIsLight(); const isLTR = diagramDirection === "ltr"; const showMore = - diagramMode === "summary" || - (diagramMode === "fields" && table.fields.length > 0); + diagramMode === "summary" || (diagramMode === "fields" && table.fields.length > 0); const inField = table.fields.find((f) => f.name === "in"); const outField = table.fields.find((f) => f.name === "out"); @@ -215,13 +238,7 @@ export function BaseNode({ > - + - {extractKindRecords( - inField.kind ?? "", - ).join(", ")} + {extractKindRecords(inField.kind ?? "").join(", ")} } /> @@ -256,9 +275,7 @@ export function BaseNode({ name="out" value={ - {extractKindRecords( - outField.kind ?? "", - ).join(", ")} + {extractKindRecords(outField.kind ?? "").join(", ")} } /> @@ -274,9 +291,16 @@ export function BaseNode({ /> {diagramMode === "fields" ? ( - + ) : ( - + ) { return ( ) { return ( Date: Thu, 17 Oct 2024 15:26:31 +0200 Subject: [PATCH 08/10] update reactflow --- package.json | 7 +- pnpm-lock.yaml | 352 ++---------------- src/components/App/hooks/window.tsx | 17 +- .../views/designer/DesignerView/index.tsx | 12 +- .../designer/TableGraphPane/edges/ElkEdge.tsx | 15 +- .../views/designer/TableGraphPane/helpers.tsx | 84 +++-- .../views/designer/TableGraphPane/index.tsx | 168 +++------ .../TableGraphPane/nodes/BaseNode.tsx | 4 +- .../TableGraphPane/nodes/EdgeNode.tsx | 9 +- .../TableGraphPane/nodes/TableNode.tsx | 9 +- src/startup/surrealist.tsx | 2 +- 11 files changed, 175 insertions(+), 504 deletions(-) diff --git a/package.json b/package.json index 51965178..9b2aff01 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "version": "3.0.8", "surreal": "2.0.0", "type": "module", - "authors": ["SurrealDB"], + "authors": [ + "SurrealDB" + ], "scripts": { "build": "tsc && vite build", "build:desktop": "tsc && vite build -c vite.config.desktop.ts", @@ -63,6 +65,7 @@ "@tauri-apps/plugin-shell": "2.0.0-rc.1", "@tauri-apps/plugin-updater": "2.0.0-rc.2", "@theopensource-company/feature-flags": "^0.4.6", + "@xyflow/react": "^12.3.2", "ansi-to-html": "^0.7.2", "clsx": "^2.1.0", "cm6-graphql": "^0.1.1", @@ -90,7 +93,6 @@ "react-leaflet": "^4.2.1", "react-resizable-panels": "^2.0.5", "react-reverse-portal": "^2.1.1", - "reactflow": "^11.10.4", "rss-parser": "^3.13.0", "surrealdb": "^1.0.4", "use-immer": "^0.9.0", @@ -118,7 +120,6 @@ }, "pnpm": { "patchedDependencies": { - "@reactflow/core@11.11.4": "patches/@reactflow__core@11.11.4.patch", "graphql@16.9.0": "patches/graphql@16.9.0.patch" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3d2f21..c8b311ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,6 @@ settings: excludeLinksFromLockfile: false patchedDependencies: - '@reactflow/core@11.11.4': - hash: tj4jyedi5a5rqdyerfsnn6pgdu - path: patches/@reactflow__core@11.11.4.patch graphql@16.9.0: hash: ld77r6ldo77drpc37fyutqbsa4 path: patches/graphql@16.9.0.patch @@ -136,6 +133,9 @@ importers: '@theopensource-company/feature-flags': specifier: ^0.4.6 version: 0.4.6(typescript@5.5.4) + '@xyflow/react': + specifier: ^12.3.2 + version: 12.3.2(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ansi-to-html: specifier: ^0.7.2 version: 0.7.2 @@ -217,9 +217,6 @@ importers: react-reverse-portal: specifier: ^2.1.1 version: 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - reactflow: - specifier: ^11.10.4 - version: 11.11.4(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rss-parser: specifier: ^3.13.0 version: 3.13.0 @@ -1370,42 +1367,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@reactflow/background@11.3.14': - resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/controls@11.2.14': - resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/core@11.11.4': - resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/minimap@11.7.14': - resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/node-resizer@2.2.14': - resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@reactflow/node-toolbar@1.3.14': - resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - '@replit/codemirror-indentation-markers@6.5.3': resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} peerDependencies: @@ -1589,8 +1550,8 @@ packages: '@surrealdb/codemirror@1.0.0-beta.11': resolution: {integrity: sha512-JB9wCTZvummpg3SYQ2cnA52LJqzJnextK3vuW33jTv4tU1wY2LFqbgn8JWgOYeNWyp7J1h5HgNCkBoN8dlAUYQ==} - '@surrealdb/lezer@1.0.0-beta.11': - resolution: {integrity: sha512-otcbmbbAvLTebHqX1wReRTFLULBWej1fpSeCF6TMzsJiR4qDnaz8RXmZeLJbGrYptXthoBxJQKSaoh8M8+diCw==} + '@surrealdb/lezer@1.0.0-beta.12': + resolution: {integrity: sha512-/bG3VLkh9d+lddh5Jcqs7GNj/NTVZxsnpt0PKNqaPIue9Dmtzxd/vysZGgxQRcNbR8+sTjWZgjYQfGKMwERGzA==} '@surrealdb/lezer@1.0.0-beta.5': resolution: {integrity: sha512-sz9ZmLwtYVtOsyY7B2bZJk8z/OWLkSoWmZxKz0uqaAnr4ccqV9uesKuu3/fPRsxgOc5ZzeOUlMnxbPJfeMtgLA==} @@ -1729,99 +1690,24 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@types/d3-array@3.2.1': - resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} - - '@types/d3-axis@3.0.6': - resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} - - '@types/d3-brush@3.0.6': - resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} - - '@types/d3-chord@3.0.6': - resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - '@types/d3-contour@3.0.6': - resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} - - '@types/d3-delaunay@6.0.4': - resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - - '@types/d3-dispatch@3.0.6': - resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} - '@types/d3-drag@3.0.7': resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - '@types/d3-dsv@3.0.7': - resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-fetch@3.0.7': - resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} - - '@types/d3-force@3.0.10': - resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} - - '@types/d3-format@3.0.4': - resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - - '@types/d3-hierarchy@3.1.7': - resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - '@types/d3-path@3.1.0': - resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} - - '@types/d3-polygon@3.0.2': - resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - - '@types/d3-quadtree@3.0.6': - resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - - '@types/d3-random@3.0.3': - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - - '@types/d3-scale-chromatic@3.0.3': - resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==} - - '@types/d3-scale@4.0.8': - resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} - '@types/d3-selection@3.0.10': resolution: {integrity: sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==} - '@types/d3-shape@3.1.6': - resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} - - '@types/d3-time-format@4.0.3': - resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - - '@types/d3-time@3.0.3': - resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - '@types/d3-transition@3.0.8': resolution: {integrity: sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==} '@types/d3-zoom@3.0.8': resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - '@types/d3@7.4.3': - resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -1891,6 +1777,15 @@ packages: '@vue/shared@3.4.38': resolution: {integrity: sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==} + '@xyflow/react@12.3.2': + resolution: {integrity: sha512-+bK3L61BDIvUX++jMiEqIjy5hIIyVmfeiUavpeOZIYKwg6NW0pR5EnHJM2JFfkVqZisFauzS9EgmI+tvTqx9Qw==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.43': + resolution: {integrity: sha512-1zHgad1cWr1mKm2xbFaarK0Jg8WRgaQ8ubSBIo/pRdq3fEgCuqgNkL9NSAP6Rvm8zi3+Lu4JPUMN+EEx5QgX9A==} + acorn@8.12.1: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} @@ -2614,12 +2509,6 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - reactflow@11.11.4: - resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -4224,84 +4113,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@reactflow/background@11.3.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@reactflow/core': 11.11.4(patch_hash=tj4jyedi5a5rqdyerfsnn6pgdu)(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.4)(immer@10.1.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/controls@11.2.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@reactflow/core': 11.11.4(patch_hash=tj4jyedi5a5rqdyerfsnn6pgdu)(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.4)(immer@10.1.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/core@11.11.4(patch_hash=tj4jyedi5a5rqdyerfsnn6pgdu)(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@types/d3': 7.4.3 - '@types/d3-drag': 3.0.7 - '@types/d3-selection': 3.0.10 - '@types/d3-zoom': 3.0.8 - classcat: 5.0.5 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.4)(immer@10.1.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/minimap@11.7.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@reactflow/core': 11.11.4(patch_hash=tj4jyedi5a5rqdyerfsnn6pgdu)(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/d3-selection': 3.0.10 - '@types/d3-zoom': 3.0.8 - classcat: 5.0.5 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.4)(immer@10.1.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/node-resizer@2.2.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@reactflow/core': 11.11.4(patch_hash=tj4jyedi5a5rqdyerfsnn6pgdu)(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - classcat: 5.0.5 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.4)(immer@10.1.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - - '@reactflow/node-toolbar@1.3.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@reactflow/core': 11.11.4(patch_hash=tj4jyedi5a5rqdyerfsnn6pgdu)(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.4)(immer@10.1.1)(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)': dependencies: '@codemirror/language': 6.10.2 @@ -4419,9 +4230,9 @@ snapshots: '@codemirror/language': 6.10.2 '@lezer/common': 1.2.1 '@lezer/javascript': 1.4.18 - '@surrealdb/lezer': 1.0.0-beta.11 + '@surrealdb/lezer': 1.0.0-beta.12 - '@surrealdb/lezer@1.0.0-beta.11': + '@surrealdb/lezer@1.0.0-beta.12': dependencies: '@lezer/highlight': 1.2.1 '@lezer/lr': 1.4.2 @@ -4560,81 +4371,18 @@ snapshots: dependencies: '@babel/types': 7.25.4 - '@types/d3-array@3.2.1': {} - - '@types/d3-axis@3.0.6': - dependencies: - '@types/d3-selection': 3.0.10 - - '@types/d3-brush@3.0.6': - dependencies: - '@types/d3-selection': 3.0.10 - - '@types/d3-chord@3.0.6': {} - '@types/d3-color@3.1.3': {} - '@types/d3-contour@3.0.6': - dependencies: - '@types/d3-array': 3.2.1 - '@types/geojson': 7946.0.14 - - '@types/d3-delaunay@6.0.4': {} - - '@types/d3-dispatch@3.0.6': {} - '@types/d3-drag@3.0.7': dependencies: '@types/d3-selection': 3.0.10 - '@types/d3-dsv@3.0.7': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-fetch@3.0.7': - dependencies: - '@types/d3-dsv': 3.0.7 - - '@types/d3-force@3.0.10': {} - - '@types/d3-format@3.0.4': {} - - '@types/d3-geo@3.1.0': - dependencies: - '@types/geojson': 7946.0.14 - - '@types/d3-hierarchy@3.1.7': {} - '@types/d3-interpolate@3.0.4': dependencies: '@types/d3-color': 3.1.3 - '@types/d3-path@3.1.0': {} - - '@types/d3-polygon@3.0.2': {} - - '@types/d3-quadtree@3.0.6': {} - - '@types/d3-random@3.0.3': {} - - '@types/d3-scale-chromatic@3.0.3': {} - - '@types/d3-scale@4.0.8': - dependencies: - '@types/d3-time': 3.0.3 - '@types/d3-selection@3.0.10': {} - '@types/d3-shape@3.1.6': - dependencies: - '@types/d3-path': 3.1.0 - - '@types/d3-time-format@4.0.3': {} - - '@types/d3-time@3.0.3': {} - - '@types/d3-timer@3.0.2': {} - '@types/d3-transition@3.0.8': dependencies: '@types/d3-selection': 3.0.10 @@ -4644,39 +4392,6 @@ snapshots: '@types/d3-interpolate': 3.0.4 '@types/d3-selection': 3.0.10 - '@types/d3@7.4.3': - dependencies: - '@types/d3-array': 3.2.1 - '@types/d3-axis': 3.0.6 - '@types/d3-brush': 3.0.6 - '@types/d3-chord': 3.0.6 - '@types/d3-color': 3.1.3 - '@types/d3-contour': 3.0.6 - '@types/d3-delaunay': 6.0.4 - '@types/d3-dispatch': 3.0.6 - '@types/d3-drag': 3.0.7 - '@types/d3-dsv': 3.0.7 - '@types/d3-ease': 3.0.2 - '@types/d3-fetch': 3.0.7 - '@types/d3-force': 3.0.10 - '@types/d3-format': 3.0.4 - '@types/d3-geo': 3.1.0 - '@types/d3-hierarchy': 3.1.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-path': 3.1.0 - '@types/d3-polygon': 3.0.2 - '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 - '@types/d3-scale': 4.0.8 - '@types/d3-scale-chromatic': 3.0.3 - '@types/d3-selection': 3.0.10 - '@types/d3-shape': 3.1.6 - '@types/d3-time': 3.0.3 - '@types/d3-time-format': 4.0.3 - '@types/d3-timer': 3.0.2 - '@types/d3-transition': 3.0.8 - '@types/d3-zoom': 3.0.8 - '@types/estree@1.0.5': {} '@types/geojson@7946.0.14': {} @@ -4786,6 +4501,27 @@ snapshots: '@vue/shared@3.4.38': {} + '@xyflow/react@12.3.2(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xyflow/system': 0.0.43 + classcat: 5.0.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + zustand: 4.5.5(@types/react@18.3.4)(immer@10.1.1)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.43': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-selection': 3.0.10 + '@types/d3-transition': 3.0.8 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + acorn@8.12.1: {} ansi-colors@4.1.3: {} @@ -5490,20 +5226,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - reactflow@11.11.4(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@reactflow/background': 11.3.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/controls': 11.2.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/core': 11.11.4(patch_hash=tj4jyedi5a5rqdyerfsnn6pgdu)(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/minimap': 11.7.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-resizer': 2.2.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.4)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - immer - readdirp@3.6.0: dependencies: picomatch: 2.3.1 diff --git a/src/components/App/hooks/window.tsx b/src/components/App/hooks/window.tsx index c28828a7..4f494c4e 100644 --- a/src/components/App/hooks/window.tsx +++ b/src/components/App/hooks/window.tsx @@ -1,22 +1,13 @@ -import { clamp } from "reactflow"; import { useSetting } from "~/hooks/config"; import { useKeymap } from "~/hooks/keymap"; import { useStable } from "~/hooks/stable"; import { useIntent } from "~/hooks/url"; +import { clamp } from "~/util/helpers"; export function useWindowSettings() { - const [windowScale, setWindowScale] = useSetting( - "appearance", - "windowScale", - ); - const [editorScale, setEditorScale] = useSetting( - "appearance", - "editorScale", - ); - const [windowPinned, setWindowPinned] = useSetting( - "behavior", - "windowPinned", - ); + const [windowScale, setWindowScale] = useSetting("appearance", "windowScale"); + const [editorScale, setEditorScale] = useSetting("appearance", "editorScale"); + const [windowPinned, setWindowPinned] = useSetting("behavior", "windowPinned"); const increaseWindowScale = useStable(() => { setWindowScale(clamp(windowScale + 10, 75, 150)); diff --git a/src/screens/database/views/designer/DesignerView/index.tsx b/src/screens/database/views/designer/DesignerView/index.tsx index 5e6240c8..bd70714f 100644 --- a/src/screens/database/views/designer/DesignerView/index.tsx +++ b/src/screens/database/views/designer/DesignerView/index.tsx @@ -1,7 +1,7 @@ import { Box } from "@mantine/core"; import { memo, useEffect } from "react"; import { Panel, PanelGroup } from "react-resizable-panels"; -import { ReactFlowProvider } from "reactflow"; +import { ReactFlowProvider } from "@xyflow/react"; import { Icon } from "~/components/Icon"; import { PanelDragger } from "~/components/Pane/dragger"; import { useActiveConnection, useIsConnected } from "~/hooks/connection"; @@ -52,7 +52,10 @@ export function DesignerView() { return ( <> - + )} - + ; export function ElkStepEdge({ sourceX, @@ -9,7 +17,7 @@ export function ElkStepEdge({ targetY, data, ...rest -}: EdgeProps) { +}: EdgeProps) { const bendSection = data?.path; const edgePath = useMemo(() => { @@ -145,7 +153,6 @@ export function ElkStepEdge({ return ( = { + const node: any = { id: name, type: isEdge ? "edge" : "table", position: { x: 0, y: 0 }, - sourcePosition: Position.Right, - targetPosition: Position.Left, + deletable: false, + // sourcePosition: Position.Right, + // targetPosition: Position.Left, data: { table, isSelected: false, hasIncoming: false, hasOutgoing: false, }, - deletable: false, }; nodes.push(node); @@ -253,7 +250,6 @@ export function buildFlowNodes( return [nodes, edges, warnings]; } -type DimensionNode = { id: string; width: number; height: number }; type NodeEdge = { id: string; path?: ElkEdgeSection }; /** @@ -264,22 +260,21 @@ type NodeEdge = { id: string; path?: ElkEdgeSection }; * @returns The changes to apply */ export async function applyNodeLayout( - nodes: DimensionNode[], + nodes: Node[], edges: Edge[], direction: DiagramDirection, -): Promise<[NodeChange[], NodeEdge[]]> { - if (nodes.some((node) => !node.width || !node.height)) { - return [[], []]; - } - +): Promise<[NodeChange[], EdgeChange[]]> { const ELK = await import("elkjs/lib/elk.bundled"); const elk = new ELK.default(); + + const edgeIndex = objectify(edges, (e) => e.id); + const graph = { id: "root", children: nodes.map((node) => ({ id: node.id, - width: node.width, - height: node.height, + width: node.measured?.width ?? node.width, + height: node.measured?.height ?? node.height, })), edges: edges.map((edge) => ({ id: edge.id, @@ -300,22 +295,35 @@ export async function applyNodeLayout( const children = layout.children || []; const layoutEdges = layout.edges || []; - return [ - children.map(({ id, x, y }) => { - return { - id, - type: "position", - position: { - x: x ?? 0, - y: y ?? 0, - }, - }; - }), - layoutEdges.map(({ id, sections }) => ({ + const nodeChanges: NodeChange[] = children.map(({ id, x, y }) => { + return { id, - path: sections?.[0], - })), - ]; + type: "position", + position: { + x: x ?? 0, + y: y ?? 0, + }, + }; + }); + + const edgeChanges: EdgeChange[] = layoutEdges.map(({ id, sections }) => { + const current = edgeIndex[id]; + + return { + id, + type: "replace", + item: { + ...current, + data: { + ...current.data, + isDragged: false, + path: sections?.[0], + }, + }, + }; + }); + + return [nodeChanges, edgeChanges]; } /** diff --git a/src/screens/database/views/designer/TableGraphPane/index.tsx b/src/screens/database/views/designer/TableGraphPane/index.tsx index 53d5ac6f..39584979 100644 --- a/src/screens/database/views/designer/TableGraphPane/index.tsx +++ b/src/screens/database/views/designer/TableGraphPane/index.tsx @@ -19,21 +19,24 @@ import { import { type ChangeEvent, type ElementRef, + type MouseEvent, useEffect, useLayoutEffect, - useMemo, useRef, useState, } from "react"; import { Background, - type NodeChange, + type Edge, + type Node, + type OnNodesChange, ReactFlow, useEdgesState, + useNodesInitialized, useNodesState, useReactFlow, -} from "reactflow"; +} from "@xyflow/react"; import { iconAPI, @@ -73,7 +76,6 @@ import { useIsLight } from "~/hooks/theme"; import { useConfigStore } from "~/stores/config"; import { useInterfaceStore } from "~/stores/interface"; import type { DiagramDirection, DiagramMode, TableInfo } from "~/types"; -import { getSetting } from "~/util/config"; import { showInfo } from "~/util/helpers"; import { themeColor } from "~/util/mantine"; import { GraphWarningLine } from "./components"; @@ -102,118 +104,31 @@ export function TableGraphPane(props: TableGraphPaneProps) { const { fitView, getViewport, setViewport } = useReactFlow(); const [warnings, setWarnings] = useState([]); - const [nodes, setNodes, handleOnNodesChange] = useNodesState([]); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); - const [computing, setComputing] = useState(false); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [rendering, setRendering] = useState(false); - const doLayoutRef = useRef(false); - const doFitRef = useRef(false); + const { getNodes, getEdges } = useReactFlow(); + const nodesInitialized = useNodesInitialized({ includeHiddenNodes: true }); + const isLayedOut = useRef(false); - const onNodesChange = useStable(async (changes: NodeChange[]) => { - handleOnNodesChange(changes); - - if (doLayoutRef.current) { - doLayoutRef.current = false; - - const direction = activeSession.diagramDirection; - const dimNodes = changes.flatMap((change) => { - if (change.type !== "dimensions" || !change.dimensions) { - return []; - } - - return { - id: change.id, - width: change.dimensions.width, - height: change.dimensions.height, - }; - }); - - const layoutChanges = await applyNodeLayout(dimNodes, edges, direction); - - setComputing(false); - - if (changes.length > 0) { - doFitRef.current = true; - - handleOnNodesChange(layoutChanges[0]); - - setEdges((prev) => { - return prev.map((curr) => { - const found = layoutChanges[1].find((edge) => edge.id === curr.id); - - if (!found) { - return curr; - } - - return { - ...curr, - data: { - ...curr.data, - path: found.path, - isDragged: false, - }, - }; - }); - }); - - fitView(); - } - } - - const lineStyle = getSetting("appearance", "lineStyle"); - - if (lineStyle !== "metro") { - return; - } - - const uniqueChanges = new Set(); - - for (const change of changes) { - if (change.type === "position") { - const node = nodes.find(({ id }) => id === change.id); - - if (!node) { - continue; - } - - const edgesToChange = edges - .filter(({ target, source }) => target === node.id || source === node.id) - .map(({ id }) => id); - - for (const edge of edgesToChange) { - uniqueChanges.add(edge); - } - } - } - - if (uniqueChanges.size === 0) { + useLayoutEffect(() => { + if (isLayedOut.current || !nodesInitialized) { return; } - setEdges((prev) => - prev.map((edge) => { - if (!uniqueChanges.has(edge.id)) { - return edge; - } - - return { - ...edge, - data: { - ...edge.data, - isDragged: true, - }, - }; - }), - ); - }); + isLayedOut.current = true; - // biome-ignore lint/correctness/useExhaustiveDependencies: Fit view when nodes change - useEffect(() => { - if (doFitRef.current) { - doFitRef.current = false; - fitView(); - } - }, [nodes]); + applyNodeLayout(getNodes(), getEdges(), activeSession.diagramDirection) + .then(([nodes, edges]) => { + onNodesChange(nodes); + onEdgesChange(edges); + setTimeout(fitView, 0); + }) + .finally(() => { + setRendering(false); + }); + }, [nodesInitialized, activeSession]); const renderGraph = useStable(async () => { const [nodes, edges, warnings] = buildFlowNodes( @@ -226,15 +141,34 @@ export function TableGraphPane(props: TableGraphPaneProps) { if (nodes.length === 0) { setNodes([]); setEdges([]); - setIsComputing(false); + setRendering(false); return; } + isLayedOut.current = false; + setRendering(true); setNodes(nodes); setEdges(edges); - setComputing(true); + }); + + const handleNodeClick = useStable((_: MouseEvent, node: Node) => { + props.setActiveTable(node.id); + }); - doLayoutRef.current = true; + const handleNodeDragStart = useStable((_: MouseEvent, node: Node) => { + setEdges((edges) => + edges.map((edge) => { + const isDisrupted = edge.source === node.id || edge.target === node.id; + + return { + ...edge, + data: { + ...edge.data, + isDragged: isDisrupted ? true : edge.data?.isDragged ?? false, + }, + }; + }), + ); }); const saveImage = useStable(async (type: "png" | "svg") => { @@ -448,10 +382,12 @@ export function TableGraphPane(props: TableGraphPaneProps) { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} className={classes.diagram} - style={{ opacity: computing ? 0 : 1 }} - onNodeClick={(_ev, node) => { - props.setActiveTable(node.id); + style={{ + opacity: rendering ? 0 : 1, + transition: rendering ? undefined : "opacity .15s", }} + onNodeClick={handleNodeClick} + onNodeDragStart={handleNodeDragStart} onContextMenu={showContextMenu([ { key: "create", diff --git a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx index 28448d5d..cbd6548d 100644 --- a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx +++ b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx @@ -1,6 +1,6 @@ import { Box, Divider, Flex, Group, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core"; import { type MouseEvent, type ReactNode, useRef } from "react"; -import { Handle, Position } from "reactflow"; +import { Handle, Position, useInternalNode } from "@xyflow/react"; import { Icon } from "~/components/Icon"; import { Spacer } from "~/components/Spacer"; import { useActiveConnection } from "~/hooks/connection"; @@ -174,7 +174,6 @@ function Fields(props: FieldsProps) { } interface BaseNodeProps { - id: string; icon: string; table: TableInfo; isSelected: boolean; @@ -184,7 +183,6 @@ interface BaseNodeProps { } export function BaseNode({ - id, icon, table, isSelected, diff --git a/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx b/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx index 3bcca1fe..90b8215c 100644 --- a/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx +++ b/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx @@ -1,12 +1,13 @@ import { iconRelation } from "~/util/icons"; -import type { NodeData } from "../helpers"; import { BaseNode } from "./BaseNode"; -import type { NodeProps } from "reactflow"; +import type { Node, NodeProps } from "@xyflow/react"; +import type { SharedNodeData } from "../helpers"; -export function EdgeNode({ id, data }: NodeProps) { +export type EdgeNode = Node; + +export function EdgeNode({ data }: NodeProps) { return ( ) { +export type TableNode = Node; + +export function TableNode({ data }: NodeProps) { return ( Date: Thu, 17 Oct 2024 17:25:26 +0200 Subject: [PATCH 09/10] improve edge rendering --- .../views/designer/TableGraphPane/helpers.tsx | 37 ++++++++-- .../TableGraphPane/nodes/BaseNode.tsx | 6 +- .../designer/TableGraphPane/style.module.scss | 68 +++++++++---------- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/screens/database/views/designer/TableGraphPane/helpers.tsx b/src/screens/database/views/designer/TableGraphPane/helpers.tsx index dc1366fd..bbe60e08 100644 --- a/src/screens/database/views/designer/TableGraphPane/helpers.tsx +++ b/src/screens/database/views/designer/TableGraphPane/helpers.tsx @@ -1,6 +1,14 @@ import classes from "./style.module.scss"; -import type { Edge, EdgeChange, EdgeTypes, Node, NodeChange, NodeTypes } from "@xyflow/react"; +import { + MarkerType, + type Edge, + type EdgeChange, + type EdgeTypes, + type Node, + type NodeChange, + type NodeTypes, +} from "@xyflow/react"; import { toBlob, toSvg } from "html-to-image"; import type { DiagramDirection, TableInfo } from "~/types"; import { getSetting } from "~/util/config"; @@ -105,8 +113,6 @@ export function buildFlowNodes( type: isEdge ? "edge" : "table", position: { x: 0, y: 0 }, deletable: false, - // sourcePosition: Position.Right, - // targetPosition: Position.Left, data: { table, isSelected: false, @@ -141,6 +147,12 @@ export function buildFlowNodes( id: `tb-${table.schema.name}-from-edge-${fromTable}`, source: fromTable, target: table.schema.name, + markerEnd: { + type: MarkerType.Arrow, + width: 14, + height: 14, + color: "#ffffff", + }, }); const node = nodeIndex[fromTable]; @@ -169,6 +181,12 @@ export function buildFlowNodes( id: `tb-${table.schema.name}-to-edge-${toTable}`, source: table.schema.name, target: toTable, + markerEnd: { + type: MarkerType.Arrow, + width: 14, + height: 14, + color: "#ffffff", + }, }); const node = nodeIndex[toTable]; @@ -185,6 +203,9 @@ export function buildFlowNodes( // Define all record links if (showLinks) { const uniqueLinks = new Set(); + const linkColor = getComputedStyle(document.body).getPropertyValue( + "--mantine-color-slate-5", + ); for (const table of tables) { for (const field of table.fields) { @@ -234,8 +255,14 @@ export function buildFlowNodes( className: classes.recordLink, label: field.name, labelBgStyle: { fill: "var(--mantine-color-slate-8" }, - labelStyle: { fill: "white" }, + labelStyle: { fill: "currentColor" }, data: { linkCount: 1 }, + markerEnd: { + type: MarkerType.Arrow, + width: 14, + height: 14, + color: linkColor, + }, }; uniqueLinks.add(`${table.schema.name}:${target}`); @@ -250,8 +277,6 @@ export function buildFlowNodes( return [nodes, edges, warnings]; } -type NodeEdge = { id: string; path?: ElkEdgeSection }; - /** * Apply a layout to the given nodes and edges * diff --git a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx index cbd6548d..8fbba62f 100644 --- a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx +++ b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx @@ -200,13 +200,15 @@ export function BaseNode({ const inField = table.fields.find((f) => f.name === "in"); const outField = table.fields.find((f) => f.name === "out"); + console.log(); + return ( <> @@ -214,7 +216,7 @@ export function BaseNode({ type="source" position={isLTR ? Position.Right : Position.Left} style={{ - visibility: hasOutgoing ? "visible" : "hidden", + visibility: "hidden", }} /> diff --git a/src/screens/database/views/designer/TableGraphPane/style.module.scss b/src/screens/database/views/designer/TableGraphPane/style.module.scss index 8bc3994a..1cc13adf 100644 --- a/src/screens/database/views/designer/TableGraphPane/style.module.scss +++ b/src/screens/database/views/designer/TableGraphPane/style.module.scss @@ -3,18 +3,18 @@ inset: 12px; top: 0; - > div > div { + >div>div { height: 100%; } } -.fields-scroll > div > div { +.fields-scroll>div>div { display: block !important; } .list { padding-left: 24px; - + li::marker { color: var(--mantine-color-surreal-5); } @@ -22,37 +22,37 @@ .diagram { - :global(.react-flow__handle) { - // backdrop-filter: blur(2px); - // -webkit-backdrop-filter: blur(2px); - background-color: rgba(0, 0, 0, 0.4) !important; - border: 1px solid var(--mantine-color-slate-5) !important; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.35); - display: flex; - align-items: center; - justify-content: center; - cursor: unset; - - &::before { - content: ""; - width: 9px; - height: 9px; - border-radius: 100%; - background-color: white; - } - } - - :global(.react-flow__handle-left) { - width: 18px; - height: 18px; - left: -9px; - } - - :global(.react-flow__handle-right) { - width: 18px; - height: 18px; - right: -9px; - } + // :global(.react-flow__handle) { + // // backdrop-filter: blur(2px); + // // -webkit-backdrop-filter: blur(2px); + // background-color: rgba(0, 0, 0, 0.4) !important; + // border: 1px solid var(--mantine-color-slate-5) !important; + // box-shadow: 0 2px 5px rgba(0, 0, 0, 0.35); + // display: flex; + // align-items: center; + // justify-content: center; + // cursor: unset; + + // &::before { + // content: ""; + // width: 9px; + // height: 9px; + // border-radius: 100%; + // background-color: white; + // } + // } + + // :global(.react-flow__handle-left) { + // width: 18px; + // height: 18px; + // left: -9px; + // } + + // :global(.react-flow__handle-right) { + // width: 18px; + // height: 18px; + // right: -9px; + // } :global(.react-flow__edge > path) { stroke-width: 2px; From d776af22d49f2f43f44c05d06e07839271226b95 Mon Sep 17 00:00:00 2001 From: Julian Mills Date: Thu, 17 Oct 2024 17:37:04 +0200 Subject: [PATCH 10/10] fmt --- src/screens/database/views/designer/DesignerView/index.tsx | 2 +- .../views/designer/TableGraphPane/edges/ElkEdge.tsx | 2 +- .../database/views/designer/TableGraphPane/helpers.tsx | 6 +++--- .../views/designer/TableGraphPane/nodes/BaseNode.tsx | 2 +- .../views/designer/TableGraphPane/nodes/EdgeNode.tsx | 4 ++-- .../views/designer/TableGraphPane/nodes/TableNode.tsx | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/screens/database/views/designer/DesignerView/index.tsx b/src/screens/database/views/designer/DesignerView/index.tsx index bd70714f..421860fa 100644 --- a/src/screens/database/views/designer/DesignerView/index.tsx +++ b/src/screens/database/views/designer/DesignerView/index.tsx @@ -1,7 +1,7 @@ import { Box } from "@mantine/core"; +import { ReactFlowProvider } from "@xyflow/react"; import { memo, useEffect } from "react"; import { Panel, PanelGroup } from "react-resizable-panels"; -import { ReactFlowProvider } from "@xyflow/react"; import { Icon } from "~/components/Icon"; import { PanelDragger } from "~/components/Pane/dragger"; import { useActiveConnection, useIsConnected } from "~/hooks/connection"; diff --git a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx index 86f4fdb4..6a64504d 100644 --- a/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx +++ b/src/screens/database/views/designer/TableGraphPane/edges/ElkEdge.tsx @@ -1,6 +1,6 @@ -import { useMemo } from "react"; import { BaseEdge, type Edge, type EdgeProps, SmoothStepEdge } from "@xyflow/react"; import type { ElkEdgeSection } from "elkjs/lib/elk-api"; +import { useMemo } from "react"; export type ElkEdge = Edge< { diff --git a/src/screens/database/views/designer/TableGraphPane/helpers.tsx b/src/screens/database/views/designer/TableGraphPane/helpers.tsx index bbe60e08..d1996838 100644 --- a/src/screens/database/views/designer/TableGraphPane/helpers.tsx +++ b/src/screens/database/views/designer/TableGraphPane/helpers.tsx @@ -1,15 +1,17 @@ import classes from "./style.module.scss"; import { - MarkerType, type Edge, type EdgeChange, type EdgeTypes, + MarkerType, type Node, type NodeChange, type NodeTypes, } from "@xyflow/react"; +import type { ElkEdgeSection } from "elkjs/lib/elk-api"; import { toBlob, toSvg } from "html-to-image"; +import { objectify } from "radash"; import type { DiagramDirection, TableInfo } from "~/types"; import { getSetting } from "~/util/config"; import { extractEdgeRecords } from "~/util/schema"; @@ -17,8 +19,6 @@ import { extractKindRecords } from "~/util/surrealql"; import { ElkStepEdge } from "./edges/ElkEdge"; import { EdgeNode } from "./nodes/EdgeNode"; import { TableNode } from "./nodes/TableNode"; -import type { ElkEdgeSection } from "elkjs/lib/elk-api"; -import { objectify } from "radash"; type EdgeWarning = { type: "edge"; diff --git a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx index 8fbba62f..ac5a1776 100644 --- a/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx +++ b/src/screens/database/views/designer/TableGraphPane/nodes/BaseNode.tsx @@ -1,6 +1,6 @@ import { Box, Divider, Flex, Group, Paper, ScrollArea, Stack, Text, Tooltip } from "@mantine/core"; -import { type MouseEvent, type ReactNode, useRef } from "react"; import { Handle, Position, useInternalNode } from "@xyflow/react"; +import { type MouseEvent, type ReactNode, useRef } from "react"; import { Icon } from "~/components/Icon"; import { Spacer } from "~/components/Spacer"; import { useActiveConnection } from "~/hooks/connection"; diff --git a/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx b/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx index 90b8215c..f785654f 100644 --- a/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx +++ b/src/screens/database/views/designer/TableGraphPane/nodes/EdgeNode.tsx @@ -1,7 +1,7 @@ -import { iconRelation } from "~/util/icons"; -import { BaseNode } from "./BaseNode"; import type { Node, NodeProps } from "@xyflow/react"; +import { iconRelation } from "~/util/icons"; import type { SharedNodeData } from "../helpers"; +import { BaseNode } from "./BaseNode"; export type EdgeNode = Node; diff --git a/src/screens/database/views/designer/TableGraphPane/nodes/TableNode.tsx b/src/screens/database/views/designer/TableGraphPane/nodes/TableNode.tsx index e4c31c49..dceba364 100644 --- a/src/screens/database/views/designer/TableGraphPane/nodes/TableNode.tsx +++ b/src/screens/database/views/designer/TableGraphPane/nodes/TableNode.tsx @@ -1,7 +1,7 @@ -import { iconTable } from "~/util/icons"; -import { BaseNode } from "./BaseNode"; import type { Node, NodeProps } from "@xyflow/react"; +import { iconTable } from "~/util/icons"; import type { SharedNodeData } from "../helpers"; +import { BaseNode } from "./BaseNode"; export type TableNode = Node;