mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 11:15:42 +00:00
position new created table in the top left corner
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 node’s `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);
|
||||
}
|
||||
@@ -1,52 +1,66 @@
|
||||
// Importing necessary types and hooks from React Flow (XYFlow)
|
||||
import {
|
||||
addEdge, Background, Connection, Controls, EdgeChange,
|
||||
EdgeRemoveChange, NodeChange, NodePositionChange,
|
||||
NodeRemoveChange, OnEdgesChange, OnNodesChange,
|
||||
ReactFlow, useEdgesState, useNodesState, useReactFlow
|
||||
} from "@xyflow/react";
|
||||
|
||||
import { addEdge, applyNodeChanges, Background, Connection, Controls, Edge, EdgeChange, EdgeRemoveChange, MiniMap, Node, NodeChange, NodePositionChange, NodeRemoveChange, NodeSelectionChange, OnEdgesChange, OnNodesChange, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
// React built-ins
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
// Custom components and styles
|
||||
import Table from "./table/table";
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import Relationship from "./table/relationship";
|
||||
import DBController from "./db-controller/db-controller";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
|
||||
// Custom context providers and hooks
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useTableToNode } from "@/hooks/use-table-to-node";
|
||||
import { useRelationshipToEdge } from "@/hooks/use-relationship-to-edge";
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
// Utils and constants
|
||||
import { v4 } from "uuid";
|
||||
import { TARGET_PREFIX } from "./table/field";
|
||||
import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker";
|
||||
import { areArraysEqual } from "@/utils/utils";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { addToast, Button, Image } from "@heroui/react";
|
||||
import { AlertTriangle, LayoutGrid } from "lucide-react";
|
||||
import { addToast, Button } from "@heroui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { adjustTablesPositions } from "@/utils/tables";
|
||||
|
||||
import DatabaseControlButtons from "./database-control-buttons";
|
||||
import useGetOverlappingNodes from "@/hooks/use-get-overlapping-nodes";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import useHighlightedEdges from "@/hooks/use-highlighted-edges";
|
||||
import useOverlappingNodes from "@/hooks/use-overlapping-nodes";
|
||||
|
||||
|
||||
|
||||
// Main functional component
|
||||
const DatabasePage: React.FC<never> = () => {
|
||||
|
||||
// Extract database state and operations
|
||||
const { database, getField } = useDatabase();
|
||||
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
|
||||
|
||||
// Node and edge state hooks
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
// Diagram-related state (e.g. connection in progress)
|
||||
const { setIsConnectionInProgress } = useDiagramOps();
|
||||
const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([]);
|
||||
const [isTableOverlappingPulsing, setIsTableOverlappingPulsing] = useState<boolean>(false);
|
||||
|
||||
// Destructure tables and relationships from database
|
||||
const { tables, relationships } = database;
|
||||
|
||||
// Hook to allow zooming and centering the diagram
|
||||
const { fitView } = useReactFlow();
|
||||
|
||||
// Define custom node and edge types
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
|
||||
|
||||
|
||||
|
||||
useTableToNode(tables);
|
||||
useRelationshipToEdge(relationships);
|
||||
|
||||
|
||||
// Called when a connection is made between fields
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
const sourceFieldId: string | undefined = (connection.sourceHandle as string).split("_").pop();
|
||||
const targetFieldId: string = (connection.targetHandle as string).replace(TARGET_PREFIX, "");
|
||||
@@ -54,9 +68,8 @@ const DatabasePage: React.FC<never> = () => {
|
||||
const sourceField: FieldType | undefined = getField(connection.source, sourceFieldId as string);
|
||||
const targetField: FieldType | undefined = getField(connection.target, targetFieldId);
|
||||
|
||||
|
||||
// Check if both fields have the same type (valid relationship)
|
||||
if (sourceField?.typeId == targetField?.typeId) {
|
||||
|
||||
createRelationship({
|
||||
id: v4(),
|
||||
sourceTableId: connection.source,
|
||||
@@ -65,28 +78,29 @@ const DatabasePage: React.FC<never> = () => {
|
||||
targetFieldId
|
||||
} as RelationshipInsertType);
|
||||
|
||||
// Add edge to the diagram
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
} else {
|
||||
// Show error toast if invalid relationship
|
||||
addToast({
|
||||
title: "Invalid Relationship",
|
||||
description: "Relationship should be between primary key and foreign key of the same time",
|
||||
color: "danger",
|
||||
})
|
||||
});
|
||||
}
|
||||
setIsConnectionInProgress(false);
|
||||
|
||||
setIsConnectionInProgress(false);
|
||||
}, [database]);
|
||||
|
||||
// Called when nodes are updated (position changes or removed)
|
||||
const handleNodesChanges: OnNodesChange<never> = useCallback(async (changes: NodeChange<never>[]) => {
|
||||
|
||||
|
||||
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) =>
|
||||
change.type == "position" &&
|
||||
!change.dragging
|
||||
change.type == "position" && !change.dragging
|
||||
) as NodePositionChange[];
|
||||
|
||||
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
|
||||
|
||||
// Save new positions to the database
|
||||
if (nodePositionChanges.length > 0)
|
||||
await updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
|
||||
id: change.id,
|
||||
@@ -94,140 +108,50 @@ const DatabasePage: React.FC<never> = () => {
|
||||
posY: change.position?.y
|
||||
} as TableInsertType)));
|
||||
|
||||
// Delete tables if removed
|
||||
if (nodeRemoveChanges.length > 0)
|
||||
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
|
||||
|
||||
return onNodesChange(changes);
|
||||
|
||||
}, [onNodesChange]);
|
||||
|
||||
|
||||
|
||||
// Called when edges (relationships) change
|
||||
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
||||
|
||||
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
|
||||
|
||||
// Delete relationships from database
|
||||
if (edgeRemoveChanges.length > 0) {
|
||||
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id))
|
||||
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
|
||||
}
|
||||
|
||||
return onEdgesChange(changes as EdgeChange<never>[]);
|
||||
}, [onEdgesChange]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const newSelectedNodesIds: string[] = nodes.filter((node: Node) => node.selected).map((node: Node) => node.id)
|
||||
if (areArraysEqual(newSelectedNodesIds, selectedNodeIds)) return; setSelectedNodeIds(newSelectedNodesIds);
|
||||
}, [nodes]);
|
||||
|
||||
|
||||
const selectedEdgeIds: string[] = useMemo(() => {
|
||||
return relationships.filter((relationship: RelationshipType) =>
|
||||
selectedNodeIds.includes(relationship.sourceTableId) ||
|
||||
selectedNodeIds.includes(relationship.targetTableId)
|
||||
).map((relationship: RelationshipType) => relationship.id);
|
||||
}, [selectedNodeIds, relationships]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setEdges((edges: any) => {
|
||||
return edges.map((edge: any) => {
|
||||
const selected: boolean = selectedEdgeIds.includes(edge.id);
|
||||
return (edge.animated == selected) ? edge : { ...edge, animated: selected } as Edge;
|
||||
})
|
||||
});
|
||||
}, [selectedEdgeIds]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setNodes((nodes: any) => {
|
||||
return nodes.map((node: any) => {
|
||||
const newHighlitedEdges: Edge[] = edges.filter((edge: Edge) =>
|
||||
(edge.animated || edge.selected)
|
||||
&& (edge.source == node.id || edge.target == node.id)
|
||||
);
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
highlightedEdges: newHighlitedEdges
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [edges]);
|
||||
|
||||
// When user starts connecting fields
|
||||
const onConnectStart = useCallback(() => {
|
||||
setIsConnectionInProgress(true);
|
||||
}, []);
|
||||
|
||||
|
||||
// When user ends connecting fields
|
||||
const onConnectEnd = useCallback(() => {
|
||||
setIsConnectionInProgress(false);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Automatically reposition tables to avoid overlap and fit the view
|
||||
const adjustPositions = useCallback(async () => {
|
||||
updateTablePositions(await adjustTablesPositions(nodes, relationships));
|
||||
setTimeout(() => {
|
||||
fitView({
|
||||
duration: 500
|
||||
})
|
||||
}, 300)
|
||||
});
|
||||
}, 300);
|
||||
}, [relationships, nodes]);
|
||||
|
||||
|
||||
const overlappingNodes = useGetOverlappingNodes();
|
||||
|
||||
useEffect(() => {
|
||||
const overlappingIds = Array.from(overlappingNodes);
|
||||
|
||||
const previousOverlappingNodes = nodes.filter((node: Node) => node.data.overlapping);
|
||||
|
||||
if (!areArraysEqual(previousOverlappingNodes, overlappingIds)) {
|
||||
setNodes((nodes: any) => {
|
||||
return nodes.map((node: any) => {
|
||||
const isOverlaped: boolean = overlappingIds.includes(node.id);
|
||||
|
||||
if (isOverlaped == node.data.overlapping)
|
||||
return node;
|
||||
else
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
overlapping: isOverlaped
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [overlappingNodes]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setNodes((nodes: any) => {
|
||||
return nodes.map((node: any) => {
|
||||
if (!node.data.overlapping)
|
||||
return node;
|
||||
else
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
pulsing: isTableOverlappingPulsing
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [isTableOverlappingPulsing])
|
||||
|
||||
const pulsOverlappingTables = useCallback(() => {
|
||||
setIsTableOverlappingPulsing(true);
|
||||
setTimeout(() => setIsTableOverlappingPulsing(false), 200);
|
||||
}, [])
|
||||
// Convert tables and relationships into flow elements
|
||||
useTableToNode(tables);
|
||||
useRelationshipToEdge(relationships);
|
||||
useHighlightedEdges(nodes, relationships, edges);
|
||||
const { isOverlapping, puls } = useOverlappingNodes(nodes);
|
||||
|
||||
return (
|
||||
|
||||
@@ -275,7 +199,7 @@ const DatabasePage: React.FC<never> = () => {
|
||||
className="absolute left-[12px] top-[64px] "
|
||||
>
|
||||
{
|
||||
overlappingNodes.size > 0 &&
|
||||
isOverlapping &&
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
@@ -285,7 +209,7 @@ const DatabasePage: React.FC<never> = () => {
|
||||
isIconOnly
|
||||
color="danger"
|
||||
className="size-8 p-1 "
|
||||
onPressEnd={pulsOverlappingTables}
|
||||
onPressEnd={puls}
|
||||
>
|
||||
<AlertTriangle className="size-4 text-white" />
|
||||
</Button>
|
||||
|
||||
@@ -9,30 +9,47 @@ import { useDatabase, useDatabaseOperations } from "@/providers/database-provide
|
||||
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useViewport } from "@xyflow/react";
|
||||
|
||||
|
||||
interface Props { }
|
||||
|
||||
const PADDING_X = 40 ;
|
||||
const PADDING_Y = 80 ;
|
||||
|
||||
const TablesController: React.FC<Props> = ({ }) => {
|
||||
|
||||
const { database } = useDatabase();
|
||||
const {createTable} = useDatabaseOperations() ;
|
||||
const { tables } = database ;
|
||||
const { createTable } = useDatabaseOperations();
|
||||
const { tables } = database;
|
||||
const viewport = useViewport();
|
||||
const { t } = useTranslation();
|
||||
const [selectedTable, setSelectedTable] = useState(new Set([]));
|
||||
const { focusedTableId } = useDiagram();
|
||||
|
||||
const addNewTable = useCallback(async () => {
|
||||
const newTableId: string = v4();
|
||||
|
||||
const { x, y, zoom } = viewport;
|
||||
// Convert screen (0,0) to flow coordinates using viewport values
|
||||
const posX = -x / zoom + (PADDING_X / zoom);
|
||||
const posY = -y / zoom + (PADDING_Y / zoom);
|
||||
|
||||
|
||||
await createTable({
|
||||
id: newTableId,
|
||||
name: `table_${tables.length + 1}`,
|
||||
posX ,
|
||||
posY ,
|
||||
fields : [{
|
||||
id : v4() ,
|
||||
name : "id" ,
|
||||
isPrimary : true ,
|
||||
unique : true
|
||||
}]
|
||||
} as TableInsertType);
|
||||
|
||||
|
||||
setSelectedTable(new Set([newTableId]) as any);
|
||||
}, [tables]);
|
||||
}, [tables, viewport]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDiagram, useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { Button, cn, select } from "@heroui/react";
|
||||
import { Handle, Position, useConnection } from "@xyflow/react";
|
||||
import { Check, KeyRound, Trash, Trash2 } from "lucide-react";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { Button, cn } from "@heroui/react";
|
||||
import { Handle, Position } from "@xyflow/react";
|
||||
import { Check, KeyRound, Trash2 } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import hash from 'object-hash';
|
||||
@@ -45,11 +45,11 @@ const Field: React.FC<Props> = (props) => {
|
||||
setEditMode(false);
|
||||
}, [fieldName])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"group relative flex h-8 items-center justify-between gap-1 border-t border-default dark:border-default/5 px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-primary-500 transition-all duration-200 ease-in-out",
|
||||
"group relative flex h-8 items-center justify-between gap-1 border-t border-default dark:border-default/5 px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-primary/5 transition-all duration-200 ease-in-out",
|
||||
highlight ? "bg-primary/5" : ""
|
||||
)}>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import { Edge, Node, NodeProps, useConnection, useStore } from "@xyflow/react";
|
||||
import { Edge, Node, NodeProps } from "@xyflow/react";
|
||||
import hash from 'object-hash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Card, cn, } from "@heroui/react";
|
||||
@@ -14,21 +14,22 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/to
|
||||
import FieldComponent from "./field";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableInsertType, TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import useGetRelatedEdges from "@/hooks/use-get-related-edges";
|
||||
|
||||
|
||||
|
||||
export type TableProps = Node<{
|
||||
table: TableType,
|
||||
overlapping?: boolean,
|
||||
pulsing?: boolean,
|
||||
highlightedEdges : Edge[]
|
||||
highlightedEdges: Edge[]
|
||||
|
||||
}>
|
||||
|
||||
const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overlapping = false, pulsing = false , highlightedEdges = [] } }) => {
|
||||
const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overlapping = false, pulsing = false, highlightedEdges = [] } }) => {
|
||||
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
@@ -50,9 +51,9 @@ const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overl
|
||||
const focus = useCallback(() => {
|
||||
focusOnTable(table.id, false);
|
||||
}, [table])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const fields: React.ReactNode[] = useMemo(() => {
|
||||
return table.fields.map((field: FieldType) => {
|
||||
const highlight: boolean = highlightedEdges.find((edge: any) =>
|
||||
@@ -67,15 +68,15 @@ const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overl
|
||||
/>)
|
||||
})
|
||||
}, [table.fields, selected, highlightedEdges]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<Card className={cn(
|
||||
"w-full h-full bg-background rounded-lg noselect overflow-visible dark:bg-default-900",
|
||||
selected
|
||||
? 'ring-2 ring-primary'
|
||||
? 'ring-2 ring-offset-1 ring-primary dark:ring-offset-default-900'
|
||||
: '',
|
||||
overlapping
|
||||
? 'ring-2 dark:ring-offset-default-900 ring-danger ring-offset-1 scale-105 shadow-danger '
|
||||
@@ -164,6 +165,5 @@ const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table, overl
|
||||
export default React.memo(Table, (previousState: any, newState: any) => {
|
||||
const previousStateHash: string = hash(previousState.data);
|
||||
const newStateHash: string = hash(newState.data);
|
||||
|
||||
return previousStateHash == newStateHash && previousState.selected == newState.selected;
|
||||
})
|
||||
|
||||
@@ -26,13 +26,13 @@ interface DatabaseOperationsContextType {
|
||||
createTable: (table: TableInsertType) => Promise<void>,
|
||||
editTable: (table: TableInsertType) => Promise<QueryResult>,
|
||||
deleteTable: (id: string) => Promise<void>,
|
||||
updateTablePositions: (tables: TableInsertType[]) => Promise<QueryResult>,
|
||||
updateTablePositions: (tables: TableInsertType[]) => Promise<void>,
|
||||
deleteMultiTables: (ids: string[]) => Promise<QueryResult>
|
||||
// field operations
|
||||
createField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
editField: (field: FieldInsertType) => Promise<QueryResult>,
|
||||
deleteField: (id: string) => Promise<void>,
|
||||
orderTableFields: (fields: FieldType[]) => Promise<QueryResult>,
|
||||
orderTableFields: (fields: FieldType[]) => Promise<void>,
|
||||
// relationship operations
|
||||
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
import { DatabaseDataContext, DatabaseOperationsContext } from "./database-context";
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { db, powerSyncDb } from "../sync-provider/sync-provider";
|
||||
import { db } from "../sync-provider/sync-provider";
|
||||
import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
|
||||
import { useQuery } from "@powersync/react";
|
||||
import { toCompilableQuery } from "@powersync/drizzle-driver";
|
||||
@@ -14,21 +13,23 @@ import { DBDiffOperation } from "@/utils/database";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { getTimestamp } from "@/utils/utils";
|
||||
|
||||
|
||||
interface Props { children: React.ReactNode }
|
||||
|
||||
const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(undefined);
|
||||
|
||||
// Fetch all databases
|
||||
const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery(
|
||||
db.query.databases.findMany()
|
||||
));
|
||||
|
||||
// Fetch all data types
|
||||
const { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
|
||||
db.query.data_types.findMany()
|
||||
));
|
||||
|
||||
// Fetch the current database with nested tables, fields, and relationships
|
||||
let { data: database, isLoading: loadingCurrentDatabase } = useQuery(
|
||||
toCompilableQuery(
|
||||
db.query.databases.findFirst({
|
||||
@@ -59,9 +60,11 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
)
|
||||
);
|
||||
|
||||
// Normalize result to single object
|
||||
if (database.length == 1)
|
||||
database = database[0] as any;
|
||||
|
||||
// Auto-select first database if none is selected
|
||||
useEffect(() => {
|
||||
if (databases.length > 0 && !currentDatabaseId) {
|
||||
setCurrentDatabaseId(databases[0].id);
|
||||
@@ -70,21 +73,24 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
|
||||
|
||||
// CRUD operations for Tables
|
||||
const createTable = useCallback(async (table: TableInsertType): Promise<void> => {
|
||||
|
||||
if (currentDatabaseId) {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(tables).values({
|
||||
...table,
|
||||
databaseId: currentDatabaseId as string,
|
||||
createdAt: table.createdAt ? table.createdAt : getTimestamp()
|
||||
databaseId: currentDatabaseId,
|
||||
createdAt: table.createdAt || getTimestamp()
|
||||
});
|
||||
if (table.fields) {
|
||||
await tx.insert(fields).values(table.fields);
|
||||
await tx.insert(fields).values(
|
||||
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
|
||||
);
|
||||
}
|
||||
})
|
||||
} else
|
||||
} else {
|
||||
throw Error("No Database selected");
|
||||
}
|
||||
}, [db, currentDatabaseId]);
|
||||
|
||||
const editTable = useCallback(async (table: TableInsertType): Promise<QueryResult> => {
|
||||
@@ -99,6 +105,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
return await db.delete(tables).where(inArray(tables.id, ids));
|
||||
}, [db]);
|
||||
|
||||
// CRUD operations for Fields
|
||||
const createField = useCallback(async (field: FieldInsertType): Promise<QueryResult> => {
|
||||
return await db.insert(fields).values(field);
|
||||
}, [db]);
|
||||
@@ -107,6 +114,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
return await db.update(fields).set(field).where(eq(fields.id, field.id));
|
||||
}, [db]);
|
||||
|
||||
// Delete field and its related relationships
|
||||
const deleteField = useCallback(async (id: string): Promise<void> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
await tx.delete(relationships).where(or(eq(relationships.sourceFieldId, id), eq(relationships.targetFieldId, id)));
|
||||
@@ -114,37 +122,31 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
})
|
||||
}, [db]);
|
||||
|
||||
|
||||
// Helper to find a field by table and field ID
|
||||
const getField = useCallback((tableId: string, id: string) => {
|
||||
const table: TableType | undefined = (database as any).tables.find((table: TableType) => table.id == tableId);
|
||||
if (table)
|
||||
return table.fields.find((field: FieldType) => field.id == id);
|
||||
}, [database])
|
||||
|
||||
const orderTableFields = useCallback(async (fieldsList: FieldType[]): Promise<QueryResult> => {
|
||||
|
||||
const caseStatements = fieldsList
|
||||
.map((field, index) => `WHEN '${field.id}' THEN ${index}`)
|
||||
.join('\n ');
|
||||
const ids = fieldsList.map(u => `'${u.id}'`).join(',\n ');
|
||||
const sql = `
|
||||
UPDATE fields
|
||||
SET sequence = CASE id
|
||||
${caseStatements}
|
||||
END
|
||||
WHERE id IN (
|
||||
${ids}
|
||||
);`;
|
||||
return await powerSyncDb.execute(sql);
|
||||
|
||||
}, [powerSyncDb]);
|
||||
// Reorder fields in a table
|
||||
const orderTableFields = useCallback(async (fieldsList: FieldType[]): Promise<void> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
for (let index = 0; index < fieldsList.length; index++) {
|
||||
await tx.update(fields).set({
|
||||
sequence: index
|
||||
}).where(eq(fields.id, fieldsList[index].id))
|
||||
}
|
||||
})
|
||||
}, [db]);
|
||||
|
||||
// CRUD operations for Relationships
|
||||
const createRelationship = useCallback(async (relationship: RelationshipInsertType): Promise<QueryResult> => {
|
||||
if (currentDatabaseId) {
|
||||
return await db.insert(relationships).values({
|
||||
...relationship,
|
||||
databaseId: currentDatabaseId,
|
||||
createdAt: relationship.createdAt ? relationship.createdAt : getTimestamp()
|
||||
createdAt: relationship.createdAt || getTimestamp()
|
||||
});
|
||||
}
|
||||
throw Error("No database selected");
|
||||
@@ -162,45 +164,29 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
return await db.delete(relationships).where(inArray(relationships.id, ids));
|
||||
}, [db]);
|
||||
|
||||
const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise<QueryResult> => {
|
||||
|
||||
const posXCases = tableList
|
||||
.map(table => `WHEN '${table.id}' THEN ${table.posX}`)
|
||||
.join('\n ');
|
||||
const posYCases = tableList
|
||||
.map(table => `WHEN '${table.id}' THEN ${table.posY}`)
|
||||
.join('\n ');
|
||||
const ids = tableList.map(table => `'${table.id}'`).join(',\n ');
|
||||
const sql = `
|
||||
UPDATE tables
|
||||
SET
|
||||
posX = CASE id
|
||||
${posXCases}
|
||||
END,
|
||||
posY = CASE id
|
||||
${posYCases}
|
||||
END
|
||||
WHERE id IN (
|
||||
${ids}
|
||||
);`;
|
||||
return await powerSyncDb.execute(sql);
|
||||
|
||||
}, [powerSyncDb]);
|
||||
// Update table positions (for UI layout)
|
||||
const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise<void> => {
|
||||
return await db.transaction(async (tx) => {
|
||||
for (const table of tableList) {
|
||||
await tx.update(tables).set({
|
||||
posX: (table as any).posX,
|
||||
posY: (table as any).posY
|
||||
}).where(eq(tables.id, (table as any).id))
|
||||
}
|
||||
})
|
||||
}, [db]);
|
||||
|
||||
// Apply a list of diff operations to sync database
|
||||
const executeDbDiffOps = useCallback(async (operations: DBDiffOperation[]) => {
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
|
||||
for (const operation of operations) {
|
||||
|
||||
if (operation.type == "CREATE_TABLE") {
|
||||
await tx.insert(tables).values(operation.table);
|
||||
|
||||
if (operation.table.fields && Object.values(operation.table.fields).length > 0)
|
||||
await tx.insert(fields).values(Object.values(operation.table.fields));
|
||||
}
|
||||
else if (operation.type === "UPDATE_TABLE") {
|
||||
|
||||
} else if (operation.type === "UPDATE_TABLE") {
|
||||
await tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId));
|
||||
|
||||
} else if (operation.type === "DELETE_TABLE") {
|
||||
@@ -227,14 +213,11 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
}, [db]);
|
||||
|
||||
|
||||
|
||||
const databaseOpsValue = useMemo(() => ({
|
||||
createTable,
|
||||
editTable,
|
||||
@@ -245,7 +228,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
editField,
|
||||
deleteField,
|
||||
orderTableFields,
|
||||
|
||||
createRelationship,
|
||||
editRelationship,
|
||||
deleteRelationship,
|
||||
@@ -261,7 +243,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
editField,
|
||||
deleteField,
|
||||
orderTableFields,
|
||||
|
||||
createRelationship,
|
||||
editRelationship,
|
||||
deleteRelationship,
|
||||
@@ -270,7 +251,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
<DatabaseDataContext.Provider value={{
|
||||
data_types,
|
||||
database: database as unknown as DatabaseType,
|
||||
@@ -278,7 +258,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
getField,
|
||||
}}>
|
||||
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
|
||||
|
||||
{
|
||||
!isLoading && database &&
|
||||
<DatabaseHistoryProvider>
|
||||
@@ -294,6 +273,3 @@ export const useDatabase = () => useContext(DatabaseDataContext);
|
||||
export const useDatabaseOperations = () => useContext(DatabaseOperationsContext);
|
||||
|
||||
export default DatabaseProvider;
|
||||
|
||||
|
||||
|
||||
|
||||
+40
-25
@@ -1,11 +1,11 @@
|
||||
// Importing types from schema definitions
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { excludeFields } from "./utils";
|
||||
|
||||
|
||||
|
||||
// Define the possible operations that can be performed when diffing databases
|
||||
export type DBDiffOperation =
|
||||
| { type: 'CREATE_TABLE'; table: TableType }
|
||||
| { type: 'DELETE_TABLE'; tableId: string }
|
||||
@@ -17,26 +17,30 @@ export type DBDiffOperation =
|
||||
| { type: 'DELETE_RELATIONSHIP'; relationshipId: string }
|
||||
| { type: 'UPDATE_RELATIONSHIP'; relationshipId: string; changes: Partial<RelationshipType> };
|
||||
|
||||
/**
|
||||
* Convert a list of low-level JSON patch operations into higher-level database diff operations
|
||||
*/
|
||||
export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
|
||||
const operations: DBDiffOperation[] = [];
|
||||
|
||||
// Track changes for tables, fields, and relationships
|
||||
const tableChanges: Record<string, Partial<TableType>> = {};
|
||||
const fieldChanges: Record<string, Record<string, Partial<FieldType>>> = {};
|
||||
const fieldCreates: Record<string, FieldType[]> = {};
|
||||
const fieldDeletes: Record<string, string[]> = {};
|
||||
|
||||
const relationshipChanges: Record<string, Partial<RelationshipType>> = {};
|
||||
const relationshipCreates: RelationshipType[] = [];
|
||||
const relationshipDeletes: string[] = [];
|
||||
|
||||
// Loop through each patch operation
|
||||
for (const op of patch) {
|
||||
const parts = op.path.split('/').filter(Boolean);
|
||||
const parts = op.path.split('/').filter(Boolean); // Split JSON path into parts
|
||||
|
||||
// Handle table-related changes
|
||||
if (parts[0] == "tables") {
|
||||
const tableId = parts[1];
|
||||
|
||||
|
||||
// Whole table operations (add/delete table)
|
||||
if (parts.length === 2) {
|
||||
if (op.op === 'add') {
|
||||
operations.push({ type: 'CREATE_TABLE', table: op.value });
|
||||
@@ -45,7 +49,7 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Field-related operations inside a table
|
||||
else if (parts[2] === 'fields') {
|
||||
const fieldId = parts[3];
|
||||
|
||||
@@ -63,25 +67,29 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Updating table attributes (e.g., name, position, etc.)
|
||||
else if (op.op === 'replace') {
|
||||
const attr = parts[2];
|
||||
tableChanges[tableId] ??= {};
|
||||
tableChanges[tableId][attr as keyof TableType] = op.value;
|
||||
}
|
||||
}
|
||||
|
||||
else if (parts[0] === 'relationships') {
|
||||
|
||||
// Handle relationship-related changes
|
||||
else if (parts[0] === 'relationships') {
|
||||
const relationshipId = parts[1];
|
||||
|
||||
// Create or delete the entire relationship
|
||||
if (parts.length === 2) {
|
||||
if (op.op === 'add') {
|
||||
relationshipCreates.push(op.value);
|
||||
} else if (op.op === 'remove') {
|
||||
relationshipDeletes.push(relationshipId);
|
||||
}
|
||||
} else if (op.op === 'replace') {
|
||||
}
|
||||
|
||||
// Update relationship properties (e.g., type, constraints)
|
||||
else if (op.op === 'replace') {
|
||||
const attr = parts.slice(2).join('/');
|
||||
relationshipChanges[relationshipId] ??= {};
|
||||
relationshipChanges[relationshipId][attr as keyof RelationshipType] = op.value;
|
||||
@@ -89,7 +97,7 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Push table update operations
|
||||
for (const [tableId, changes] of Object.entries(tableChanges)) {
|
||||
operations.push({
|
||||
type: 'UPDATE_TABLE',
|
||||
@@ -97,7 +105,8 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
changes
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Push field update operations
|
||||
for (const [tableId, fields] of Object.entries(fieldChanges)) {
|
||||
for (const [fieldId, changes] of Object.entries(fields)) {
|
||||
operations.push({
|
||||
@@ -108,7 +117,8 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Push field creation operations
|
||||
for (const [tableId, fields] of Object.entries(fieldCreates)) {
|
||||
for (const field of fields) {
|
||||
operations.push({
|
||||
@@ -118,7 +128,8 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Push field deletion operations
|
||||
for (const [tableId, fieldIds] of Object.entries(fieldDeletes)) {
|
||||
for (const fieldId of fieldIds) {
|
||||
operations.push({
|
||||
@@ -128,23 +139,31 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Push relationship creation operations
|
||||
for (const relationship of relationshipCreates) {
|
||||
operations.push({ type: 'CREATE_RELATIONSHIP', relationship });
|
||||
}
|
||||
|
||||
// Push relationship deletion operations
|
||||
for (const relationshipId of relationshipDeletes) {
|
||||
operations.push({ type: 'DELETE_RELATIONSHIP', relationshipId });
|
||||
}
|
||||
|
||||
// Push relationship update operations
|
||||
for (const [relationshipId, changes] of Object.entries(relationshipChanges)) {
|
||||
operations.push({ type: 'UPDATE_RELATIONSHIP', relationshipId, changes });
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Database object to a normalized form:
|
||||
* - Convert arrays to maps keyed by ID for easier access
|
||||
* - Exclude some fields from relationships to avoid circular references
|
||||
*/
|
||||
export function normalizeDatabase(db: DatabaseType): any {
|
||||
|
||||
return {
|
||||
...db,
|
||||
tables: Object.fromEntries(
|
||||
@@ -159,16 +178,12 @@ export function normalizeDatabase(db: DatabaseType): any {
|
||||
])
|
||||
),
|
||||
relationships: Object.fromEntries(
|
||||
db.relationships.map((rel : RelationshipType) => [
|
||||
rel.id,
|
||||
excludeFields(rel , {
|
||||
root : ["sourceField" , "targetField" , "sourceTable" , "targetTable" ]
|
||||
db.relationships.map((rel: RelationshipType) => [
|
||||
rel.id,
|
||||
excludeFields(rel, {
|
||||
root: ["sourceField", "targetField", "sourceTable", "targetTable"]
|
||||
})
|
||||
|
||||
])
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
+4
-3
@@ -42,8 +42,8 @@ const adjustTablesPositions = async (
|
||||
const node = layoutedGraph?.children?.find((n) => n.id === table.id);
|
||||
if (node) {
|
||||
// ELK positions are top-left, adjust to center like before
|
||||
table.posX = node.x || 0 + (node.width / 2) + 112; // 112 = half your fixed width
|
||||
table.posY = node.y || 0 + (node.height / 2) + 75; // 75 = half your fixed height
|
||||
table.posX = node.x || 0 + (node.width / 2) + 112;
|
||||
table.posY = node.y || 0 + (node.height / 2) + 75;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,6 +89,7 @@ const getDefaultTableOverlapping = (table: TableType, tables: TableType[]): bool
|
||||
|
||||
export {
|
||||
adjustTablesPositions,
|
||||
getDefaultTableOverlapping
|
||||
getDefaultTableOverlapping ,
|
||||
isTablesOverlapping
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user