position new created table in the top left corner

This commit is contained in:
KarimTamani
2025-05-24 17:48:49 +01:00
parent 94ff6ddf33
commit 057353156a
12 changed files with 430 additions and 343 deletions
-56
View File
@@ -1,56 +0,0 @@
import { TableType } from "@/lib/schemas/table-schema";
import { Node, useReactFlow } from "@xyflow/react";
import { useMemo } from "react";
const useGetOverlappingNodes = (): Set<string> => {
const { getNodes } = useReactFlow();
const nodes = getNodes();
const overlappingNodes: Set<string> = useMemo(() => {
const overlaps: Set<string> = new Set();
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
if (isNodesOverlapping(nodes[i], nodes[j])) {
overlaps.add(nodes[i].id);
overlaps.add(nodes[j].id);
}
}
}
return overlaps
}, [nodes]);
return overlappingNodes;
}
function isNodesOverlapping(nodeA: Node, nodeB: Node) {
const nodeAWidth: number = nodeA.measured?.width || parseInt(nodeA.style?.width as string) || 224;
const nodeAHeight: number = nodeA.measured?.height || (nodeA.data.table as TableType).fields.length * 32 + 36;
const nodeBWidth: number = nodeB.measured?.width || parseInt(nodeB.style?.width as string) || 224;
const nodeBHeight: number = nodeB.measured?.height || (nodeB.data.table as TableType).fields.length * 32 + 36;
const a = {
left: nodeA.position.x,
right: nodeA.position.x + nodeAWidth,
top: nodeA.position.y,
bottom: nodeA.position.y + nodeAHeight,
};
const b = {
left: nodeB.position.x,
right: nodeB.position.x + nodeBWidth,
top: nodeB.position.y,
bottom: nodeB.position.y + nodeBHeight,
};
return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
}
export default useGetOverlappingNodes;
-30
View File
@@ -1,30 +0,0 @@
import { Edge, useReactFlow, useStore } from "@xyflow/react";
import { useEffect, useState } from "react";
import hash from 'object-hash';
const useGetRelatedEdges = (nodeId: string) => {
const [relatedEdges, setRelatedEdges] = useState<Edge[]>([]);
const { getEdges } = useReactFlow();
const edges = getEdges();
useEffect(() => {
const newRelatedEdges: Edge[] = edges.filter((edge: Edge) => edge.source == nodeId || edge.target == nodeId);
if (hash(newRelatedEdges) != hash(relatedEdges)) {
setRelatedEdges(newRelatedEdges)
}
}, [edges, nodeId])
return relatedEdges
}
export default useGetRelatedEdges;
+87
View File
@@ -0,0 +1,87 @@
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { areArraysEqual } from "@/utils/utils";
import { Edge, Node, useReactFlow } from "@xyflow/react";
import { useEffect, useMemo, useState } from "react";
/**
* Custom hook to manage and visually highlight edges in a React Flow diagram
* based on selected nodes and their relationships.
*/
const useHighlightedEdges = (nodes: Node[], relationships: RelationshipType[], edges: Edge[]) => {
const { setNodes, setEdges } = useReactFlow();
// Keeps track of currently selected node IDs
const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([]);
/**
* Effect to update selected node IDs when node selection changes.
* Prevents unnecessary state updates by comparing the new selection with the previous one.
*/
useEffect(() => {
const newSelectedNodesIds: string[] = nodes
.filter((node: Node) => node.selected)
.map((node: Node) => node.id);
// Only update state if the selected nodes have changed
if (areArraysEqual(newSelectedNodesIds, selectedNodeIds)) return;
setSelectedNodeIds(newSelectedNodesIds);
}, [nodes]);
/**
* Memoized list of edge IDs that are connected to selected nodes.
* This avoids recomputation unless `selectedNodeIds` or `relationships` change.
*/
const selectedEdgeIds: string[] = useMemo(() => {
return relationships
.filter((relationship: RelationshipType) =>
selectedNodeIds.includes(relationship.sourceTableId) ||
selectedNodeIds.includes(relationship.targetTableId)
)
.map((relationship: RelationshipType) => relationship.id);
}, [selectedNodeIds, relationships]);
/**
* Effect to update edge animation state based on selection.
* Edges that are connected to selected nodes will be animated.
*/
useEffect(() => {
setEdges((edges: any) => {
return edges.map((edge: any) => {
const selected: boolean = selectedEdgeIds.includes(edge.id);
// Only update edge if its animation state needs to change
return (edge.animated === selected)
? edge
: { ...edge, animated: selected } as Edge;
});
});
}, [selectedEdgeIds]);
/**
* Effect to update nodes with a list of their connected and animated/selected edges.
* This information can be used to visually emphasize connected edges in node components.
*/
useEffect(() => {
setNodes((nodes: any) => {
return nodes.map((node: any) => {
// Find edges that are either animated or selected and connected to this node
const newHighlightedEdges: Edge[] = edges.filter((edge: Edge) =>
(edge.animated || edge.selected) &&
(edge.source === node.id || edge.target === node.id)
);
// Add the highlighted edges to the node's data
return {
...node,
data: {
...node.data,
highlightedEdges: newHighlightedEdges
}
};
});
});
}, [edges]);
};
export default useHighlightedEdges;
+153
View File
@@ -0,0 +1,153 @@
import { TableType } from "@/lib/schemas/table-schema";
import { areArraysEqual } from "@/utils/utils";
import { Node, useReactFlow } from "@xyflow/react";
import { useCallback, useEffect, useMemo, useState } from "react";
// Return type of the hook
type UseOverlapingType = {
isOverlapping: boolean, // Indicates if any nodes are overlapping
puls: () => void // Triggers a short visual pulse effect for overlapping nodes
}
/**
* Custom hook to detect overlapping nodes in a React Flow diagram,
* annotate them with metadata (`overlapping`, `pulsing`),
* and provide a pulse trigger for visual feedback.
*/
const useOverlappingNodes = (nodes: Node[]): UseOverlapingType => {
const [isPulsing, setIsPulsing] = useState<boolean>(false);
const { setNodes } = useReactFlow();
/**
* Triggers a brief "pulse" effect (e.g. for animations or styles)
* by toggling a boolean flag for a short time.
*/
const puls = useCallback(() => {
setIsPulsing(true);
setTimeout(() => setIsPulsing(false), 200);
}, []);
/**
* Compute which nodes are currently overlapping.
* This is memoized to avoid recalculating unless nodes change.
*/
const overlappingNodes: Set<string> = useMemo(() => {
const overlaps: Set<string> = new Set();
// Compare every pair of nodes
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
if (isNodesOverlapping(nodes[i], nodes[j])) {
overlaps.add(nodes[i].id);
overlaps.add(nodes[j].id);
}
}
}
return overlaps;
}, [nodes]);
/**
* When overlapping nodes change, update each nodes `data.overlapping` property accordingly.
* Avoids unnecessary updates using deep comparison.
*/
useEffect(() => {
const overlappingIds: string[] = Array.from(overlappingNodes);
const previousOverlappingNodesIds: string[] = nodes
.filter((node: Node) => node.data.overlapping)
.map((node: Node) => node.id);
if (!areArraysEqual(previousOverlappingNodesIds, overlappingIds)) {
setNodes((nodes: any) => {
return nodes.map((node: any) => {
const isOverlaped: boolean = overlappingIds.includes(node.id);
if (isOverlaped === node.data.overlapping)
return node; // No change needed
else
return {
...node,
data: {
...node.data,
overlapping: isOverlaped
}
};
});
});
}
}, [overlappingNodes]);
/**
* When `isPulsing` is active, apply a `pulsing` property to overlapping nodes
* to trigger animations or visual indicators in the UI.
*/
useEffect(() => {
setNodes((nodes: any) => {
return nodes.map((node: any) => {
if (!node.data.overlapping)
return node;
return {
...node,
data: {
...node.data,
pulsing: isPulsing
}
};
});
});
}, [isPulsing]);
// True if any overlapping nodes exist
const isOverlapping: boolean = overlappingNodes.size > 0;
// Return memoized object to avoid unnecessary rerenders
return useMemo(() => ({
puls,
isOverlapping
}), [isOverlapping, puls]);
};
export default useOverlappingNodes;
/**
* Helper function to check whether two nodes are overlapping
* Uses their position and dimensions to determine intersection.
*/
function isNodesOverlapping(nodeA: Node, nodeB: Node) {
const nodeAWidth: number =
nodeA.measured?.width ||
parseInt(nodeA.style?.width as string) ||
224; // Default fallback width
const nodeAHeight: number =
nodeA.measured?.height ||
(nodeA.data.table as TableType).fields.length * 32 + 36; // Estimate height based on number of fields
const nodeBWidth: number =
nodeB.measured?.width ||
parseInt(nodeB.style?.width as string) ||
224;
const nodeBHeight: number =
nodeB.measured?.height ||
(nodeB.data.table as TableType).fields.length * 32 + 36;
// Bounding box of node A
const a = {
left: nodeA.position.x,
right: nodeA.position.x + nodeAWidth,
top: nodeA.position.y,
bottom: nodeA.position.y + nodeAHeight,
};
// Bounding box of node B
const b = {
left: nodeB.position.x,
right: nodeB.position.x + nodeBWidth,
top: nodeB.position.y,
bottom: nodeB.position.y + nodeBHeight,
};
// Check if the bounding boxes intersect
return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom);
}