Fix nodes re-render whene dragging

This commit is contained in:
KarimTamani
2025-05-16 01:23:28 +01:00
parent 84d086c585
commit 7f7779d579
20 changed files with 508 additions and 248 deletions
+3 -1
View File
@@ -19,8 +19,9 @@
"@radix-ui/react-tooltip": "^1.2.3",
"@react-aria/visually-hidden": "3.8.21",
"@react-types/shared": "3.28.0",
"@xyflow/react": "^12.6.0",
"@xyflow/react": "^12.6.3",
"drizzle-orm": "^0.43.1",
"elkjs": "^0.10.0",
"framer-motion": "11.15.0",
"i18next-browser-languagedetector": "^8.0.5",
"lucide-react": "^0.501.0",
@@ -34,6 +35,7 @@
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/dagre": "^0.7.52",
"@types/node": "20.5.7",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
+6 -3
View File
@@ -5,6 +5,7 @@ import { ReactFlowProvider } from "@xyflow/react";
import useAppRoutes from "./routes/app-route";
import { SyncProvider } from "./providers/sync-provider/sync-provider";
import DatabaseProvider from "./providers/database-provider/database-provider";
import DiagramProvider from "./providers/diagram-provider/diagram-provider";
function App() {
const appRoutes = useAppRoutes();
@@ -12,9 +13,11 @@ function App() {
<SyncProvider>
<ReactFlowProvider>
<DatabaseProvider>
<TooltipProvider delayDuration={0}>
{appRoutes}
</TooltipProvider>
<DiagramProvider>
<TooltipProvider delayDuration={0}>
{appRoutes}
</TooltipProvider>
</DiagramProvider>
</DatabaseProvider>
</ReactFlowProvider>
</SyncProvider>
+2 -2
View File
@@ -12,9 +12,9 @@ const sidebarItemClass : string = "text-gray-700 size-4 data-[active=true]:text-
const Sidebar: React.FC<SidebarProps> = ({ }) => {
const { t , i18n } = useTranslation() ;
console.log ( i18n.language)
console.log ()
const location = useLocation() ;
const sidebarItems: SidebarItemProps[] = useMemo(() => [
+1
View File
@@ -6,6 +6,7 @@ import { useEffect } from "react";
export const useTableToNode = (tables: TableType[]): void => {
const { setNodes } = useReactFlow();
useEffect(() => {
const nodes = tables.map((table: TableType) => {
return {
id: table.id,
+1 -18
View File
@@ -15,24 +15,7 @@ export const colorOptions = [
];
/*
export const colorOptions = [
'#ff6363', // A brighter red.
'#ff6b8a', // A vibrant pink.
'#c05dcf', // A rich purple.
'#b067e9', // A lighter purple.
'#8a61f5', // A bold indigo.
'#7175fa', // A lighter indigo.
'#8eb7ff', // A sky blue.
'#42e0c0', // A fresh aqua.
'#4dee8a', // A mint green.
'#9ef07a', // A lime green.
'#ffe374', // A warm yellow.
'#ff9f74', // A peachy orange.
];
*/
export const randomColor = () => {
return colorOptions[Math.floor(Math.random() * colorOptions.length)];
};
+95 -44
View File
@@ -1,39 +1,39 @@
import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Connection, Controls, Edge, EdgeChange, EdgeRemoveChange, MiniMap, Node, NodeChange, NodePositionChange, NodeRemoveChange, NodeSelectionChange, OnEdgesChange, OnNodesChange, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react";
import { SetStateAction, useCallback, useEffect, useMemo, useState } from "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";
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, TableType } from "@/lib/schemas/table-schema";
import { TableInsertType } from "@/lib/schemas/table-schema";
import { useDatabase } 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 { v4 } from "uuid";
import { LEFT_PREFIX, TARGET_PREFIX } from "./table/field";
import { TARGET_PREFIX } from "./table/field";
import CardinalityMarker from "@/components/cardinality-marker/cardinality-marker";
import { areArraysEqual } from "@/utils/utils";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { Button } from "@heroui/react";
import { LayoutGrid } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
interface DatabaseProps {
initialTables?: TableType
}
const edgeTypes = {
'relationship-edge': Relationship,
};
const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
const DatabasePage: React.FC<never> = () => {
const { tables, relationships, updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabase();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
useTableToNode(tables);
useRelationshipToEdge(relationships);
const { setIsConnectionInProgress } = useDiagram();
const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([]);
const { fitView } = useReactFlow();
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
const onConnect = useCallback((connection: Connection) => {
createRelationship({
@@ -45,8 +45,9 @@ const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
} as RelationshipInsertType)
setEdges((eds) => addEdge(connection, eds));
setIsConnectionInProgress(false);
}, [setEdges]);
}, []);
const handleNodesChanges: OnNodesChange<never> = useCallback((changes: NodeChange<never>[]) => {
@@ -60,75 +61,124 @@ const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
posY: change.position?.y
} as TableInsertType)));
if (nodeRemoveChanges.length > 0)
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
onNodesChange(changes);
return onNodesChange(changes);
}, [onNodesChange]);
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
if (edgeRemoveChanges.length > 0) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id))
}
onEdgesChange(changes as EdgeChange<never>[]);
return onEdgesChange(changes as EdgeChange<never>[]);
}, [onEdgesChange]);
const selectedTableIds: string[] = useMemo(() => {
return nodes.filter((node: Node) => node.selected).map((node: Node) => node.id)
useEffect(() => {
const newSelectedNodesIds: string[] = nodes.filter((node: Node) => node.selected).map((node: Node) => node.id)
if (areArraysEqual(newSelectedNodesIds, selectedNodeIds)) return; setSelectedNodeIds(newSelectedNodesIds);
}, [nodes]);
const selectedRelationshipIds: string[] = useMemo(() => {
return relationships.filter((relationship: RelationshipType) =>
selectedTableIds.includes(relationship.sourceTableId) ||
selectedTableIds.includes(relationship.targetTableId)
).map((relationship: RelationshipType) => relationship.id);
}, [selectedTableIds, relationships]);
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 = selectedRelationshipIds.includes(edge.id);
const selected: boolean = selectedEdgeIds.includes(edge.id);
return {
...edge ,
animated : selected ,
} as Edge
...edge,
animated: selected,
} as Edge
})
})
}, [setEdges, selectedRelationshipIds])
return (
<div className="w-full h-screen flex">
}, [selectedEdgeIds]);
const onConnectStart = useCallback(() => {
setIsConnectionInProgress(true);
}, []);
const onConnectEnd = useCallback(() => {
setIsConnectionInProgress(false);
}, []);
const adjustPositions = useCallback(async () => {
updateTablePositions(await adjustTablesPositions(nodes, relationships));
setTimeout(() => {
fitView({
duration: 500
})
}, 500)
}, [relationships, nodes])
useTableToNode(tables);
useRelationshipToEdge(relationships);
return (
<div className="w-full h-screen flex relative">
<DBController />
<ReactFlow
nodes={nodes}
edges={edges}
fitView
className="w-full h-full nodes-animated"
className="w-full h-full cursor-default"
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
}}
onlyRenderVisibleElements
// onlyRenderVisibleElements
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<div className="absolute top-[160px]">
<Controls />
</div>
<Controls>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
size="sm"
isIconOnly
variant="flat"
className="size-8 p-1 shadow-none"
onPressEnd={adjustPositions}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
Adjust Positions
</TooltipContent>
</Tooltip>
</Controls>
<Background className="bg-default/30" />
</ReactFlow>
@@ -149,6 +199,7 @@ const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
</defs>
</svg>
</div>
)
}
@@ -1,6 +1,7 @@
import { useRelationshipName } from "@/hooks/use-relationship-name";
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil, Trash } from "lucide-react";
import { useState } from "react";
@@ -21,7 +22,8 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
const { editRelationship, deleteRelationship } = useDatabase();
const { t } = useTranslation();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName)
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
const { focusOnRelationship } = useDiagram();
const editRelationshipName = () => {
@@ -34,7 +36,7 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
}
const onDeleteRelationship = () => {
deleteRelationship(relationship.id);
setPopOverOpen(false);
}
@@ -85,6 +87,7 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
size="sm"
isIconOnly
variant="light"
onPressEnd={() => focusOnRelationship(relationship.id, true)}
>
<Focus className="size-4 text-icon" />
</Button>
@@ -1,7 +1,7 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
import { Accordion, AccordionItem, Button, Input, useDisclosure } from "@heroui/react"
import { Code, List, ListCollapse, Table, Workflow } from "lucide-react"
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header";
import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body";
@@ -10,6 +10,7 @@ import CreateRelationshipForm from "./create-relationship-form/create-relationsh
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
@@ -26,6 +27,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
const { createRelationship, relationships } = useDatabase();
const { t } = useTranslation();
const [selectedRelationship, setSelectedRelationship] = useState(new Set([]));
const { focusedRelationshipId } = useDiagram();
const addRelationship = useCallback(() => {
@@ -41,6 +43,13 @@ const RelationshipController: React.FC<Props> = ({ }) => {
}, [relationship]);
useEffect(() => {
if (focusedRelationshipId) {
setSelectedRelationship(new Set([focusedRelationshipId]) as any);
}
}, [focusedRelationshipId])
return (
<div className="w-full h-full flex flex-col gap-2">
<div className="flex items-center justify-between gap-4 py-1">
@@ -109,12 +118,13 @@ const RelationshipController: React.FC<Props> = ({ }) => {
<AccordionItem
key={relationship.id}
aria-label={relationship.id}
classNames={{
trigger: "w-full hover:bg-default transition-all duration-200 h-12",
base: "rounded-md p-0 overflow-hidden",
}}
subtitle={
<RelationshipAccordionHeader relationship={relationship} />
<RelationshipAccordionHeader relationship={relationship} />
}
>
<RelationshipAccordionBody relationship={relationship} />
@@ -25,7 +25,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
const { editTable, createField } = useDatabase();
const onColorChange = useCallback((color: string | undefined) => {
console.log(color)
editTable({ id: table.id, color: color ? color : null } as TableType);
}, [table]);
@@ -9,6 +9,7 @@ import { TableType } from "@/lib/schemas/table-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { v4 } from "uuid";
import { getNextSequence } from "@/utils/field";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
export interface TableAccordionHeaderProps {
table: TableType,
@@ -23,11 +24,11 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
const { t } = useTranslation();
const [editMode, setEditMode] = useState<boolean>(false);
const { focusOnTable } = useDiagram();
useEffect(()=> {
setTableName(table.name) ;
} , [table.name])
useEffect(() => {
setTableName(table.name);
}, [table.name])
const saveTableName = async () => {
await editTable({ id: table.id, name: tableName });
@@ -51,7 +52,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
nullable: true,
})
}
return (
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center border-l-[6px] "
style={{
@@ -114,13 +115,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
!editMode && <>
<div className="hidden shrink-0 flex-row group-hover:flex">
<Button
size="sm"
isIconOnly
variant="light"
>
<Focus className="size-4 text-icon" />
</Button>
<Button
size="sm"
isIconOnly
@@ -129,6 +124,14 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
>
<Pencil className="size-4 text-icon" />
</Button>
<Button
size="sm"
isIconOnly
variant="light"
onPressEnd={() => focusOnTable(table.id , true)}
>
<Focus className="size-4 text-icon" />
</Button>
</div>
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen}>
@@ -7,8 +7,8 @@ import TableAccordionHeader from "./table-accordion-item/table-accordion-header"
import TableAccordionBody from "./table-accordion-item/table-accordion-body";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { TableType } from "@/lib/schemas/table-schema";
import { v4 } from "uuid";
import { randomColor } from "@/lib/colors";
import { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
interface Props { }
@@ -20,6 +20,7 @@ const TablesController: React.FC<Props> = ({ }) => {
const { tables, createTable } = useDatabase();
const { t } = useTranslation();
const [selectedTable, setSelectedTable] = useState(new Set([]));
const { focusedTableId } = useDiagram();
const addNewTable = useCallback(async () => {
@@ -35,7 +36,12 @@ const TablesController: React.FC<Props> = ({ }) => {
}, [tables]);
useEffect(() => {
if (focusedTableId) {
setSelectedTable(new Set([focusedTableId]) as any);
}
} , [focusedTableId]) ;
const selectedTableId = selectedTable.values().next().value;
return (
@@ -101,7 +107,7 @@ const TablesController: React.FC<Props> = ({ }) => {
selectedKeys={selectedTable}
onSelectionChange={setSelectedTable as any}
isCompact
>
{tables.map((table: TableType) => (
<AccordionItem
+7 -5
View File
@@ -1,6 +1,7 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { FieldType } from "@/lib/schemas/field-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { useDiagram } 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";
@@ -11,7 +12,8 @@ import React, { useEffect, useState } from "react";
interface Props {
field: FieldType,
showHandles?: boolean,
highlight?: boolean
highlight?: boolean ,
showTargetHandle? : boolean
}
@@ -25,7 +27,7 @@ const Field: React.FC<Props> = ({ field, showHandles, highlight }) => {
const [editMode, setEditMode] = useState<boolean>(false);
const { deleteField, editField } = useDatabase();
const [fieldName, setFieldName] = useState<string>(field.name);
const {isConnectionInProgress} = useDiagram() ;
useEffect(() => {
setFieldName(field.name);
@@ -46,8 +48,8 @@ const Field: React.FC<Props> = ({ field, showHandles, highlight }) => {
setEditMode(false);
}
const connection = useConnection();
console.log(highlight)
return (
<div className={cn(
"group relative flex h-8 items-center justify-between gap-1 border-t border-default px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 transition-all duration-200 ease-in-out" ,
@@ -136,7 +138,7 @@ const Field: React.FC<Props> = ({ field, showHandles, highlight }) => {
<div className={
cn(
"absolute w-full left-0 ",
!connection.inProgress ? "invisible" : "visible"
!isConnectionInProgress ? "invisible" : "visible"
)} >
<Handle
id={`${TARGET_PREFIX}${field.id}`}
+9 -8
View File
@@ -1,6 +1,7 @@
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { card, cn } from "@heroui/react";
import { Edge, EdgeProps, getBezierPath, getSmoothStepPath, InternalNode, Node, Position, useReactFlow } from "@xyflow/react";
import React, { useMemo } from "react";
@@ -15,8 +16,9 @@ export type RelationshipProps = Edge<{
const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
let { id, sourceX, sourceY, targetX, targetY, source, target, selected, data , animated } = props;
let { id, sourceX, sourceY, targetX, targetY, source, target, selected, data, animated } = props;
const { getInternalNode, getEdge } = useReactFlow();
const { focusOnRelationship } = useDiagram();
const sourceNode = getInternalNode(source);
@@ -37,7 +39,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
const targetLeftX = targetX - 2;
const targetRightX = targetX + targetWidth + 3;
const { sourceSide, targetSide } = useMemo(() => {
const distances = {
@@ -122,14 +124,13 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
markerEnd={`url(#${endMarker})`}
fill="none"
className={cn([
`!stroke-2 ${selected ? '!stroke-primary' : 'stroke-slate-300'}`,
`!stroke-2 ${selected ? '!stroke-primary' : 'stroke-slate-300'}`,
])}
onClick={(e) => {
if (e.detail === 2) {
console.log("hello world");
// openRelationshipInEditor();
focusOnRelationship(data?.relationship.id as string);
}
}}
style={{
@@ -147,7 +148,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
className="react-flow__edge-interaction"
onClick={(e) => {
if (e.detail === 2) {
// openRelationshipInEditor();
focusOnRelationship(data?.relationship.id as string);
}
}}
/>
@@ -155,7 +156,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
</>)
}
export default Relationship;
export default React.memo(Relationship);
/*
+43 -72
View File
@@ -1,21 +1,12 @@
import { Edge, Handle, Node, NodeProps, NodeResizer, Position, useReactFlow, useStore } from "@xyflow/react";
import { Edge, Node, NodeProps, useConnection, useStore } from "@xyflow/react";
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Card, CardBody, CardHeader, cn, Divider, Input } from "@heroui/react";
import { Button, Card, cn, } from "@heroui/react";
import {
ChevronsLeftRight,
ChevronsRightLeft,
Table2,
ChevronDown,
ChevronUp,
Check,
CircleDotDashed,
SquareDot,
SquarePlus,
SquareMinus,
Divide,
Focus,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
@@ -25,49 +16,63 @@ import { FieldType } from "@/lib/schemas/field-schema";
import { TableType } from "@/lib/schemas/table-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { useTranslation } from "react-i18next";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
export const MAX_TABLE_SIZE = 450;
export const MID_TABLE_SIZE = 337;
export const MIN_TABLE_SIZE = 224;
export const TABLE_MINIMIZED_FIELDS = 10;
export type TableProps = Node<{
table: TableType, isOverlapping?: boolean;
highlightOverlappingTables?: boolean;
table: TableType,
}>
const Table: React.FC<NodeProps<TableProps>> = React.memo(({
selected,
dragging,
id,
data: { table, },
}) => {
const Table: React.FC<NodeProps<TableProps>> = ({ selected, data: { table } }) => {
const [editMode, setEditMode] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
const { editTable } = useDatabase();
const { t } = useTranslation();
const { focusOnTable } = useDiagram();
const { t } = useTranslation();
useEffect(() => {
setTableName(table.name);
}, [table.name])
const saveTableName = async () => {
const saveTableName = useCallback(async () => {
await editTable({ id: table.id, name: tableName });
setEditMode(false);
}
const edges = useStore((store) => Array.from(store.edges.values())) as Edge[];
}, []);
const focus = useCallback(() => {
focusOnTable(table.id, false);
}, [])
const edges = useStore((store) => store.edges) as Edge[];
const highlightedEdges: Edge[] = useMemo(() => {
return edges.filter((edge: Edge) => edge.animated || edge.selected);
}, [edges]);
const fields: React.ReactNode[] = useMemo(() => {
return table.fields.map((field: FieldType) => {
const highlight: boolean = highlightedEdges.find((edge: any) =>
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
(edge.data?.relationship as RelationshipType).targetFieldId == field.id) != null;
return (<FieldComponent
key={field.id}
field={field}
showHandles={selected}
highlight={highlight}
/>)
})
}, [table.fields, selected, highlightedEdges]);
console.log(highlightedEdges)
// console.log ("re-render" , table.name)
return (
<Card className={cn(
@@ -132,61 +137,27 @@ const Table: React.FC<NodeProps<TableProps>> = React.memo(({
size="sm"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
isIconOnly
onPressEnd={focus}
>
<Focus className="size-4 text-icon" />
</Button>
<Button
variant="light"
size="sm"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
isIconOnly
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
</div>
</>
}
</div>
<div
className="transition-[max-height] duration-200 ease-in-out"
>
{table.fields.map((field: FieldType) => {
const highlight: boolean = highlightedEdges.find((edge: any) =>
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
(edge.data?.relationship as RelationshipType).targetFieldId == field.id) != null;
return (<FieldComponent
key={field.id}
field={field}
showHandles={selected}
highlight={highlight}
/>)
})}
<div className="transition-[max-height] duration-200 ease-in-out">
{fields}
</div>
</Card>
)
});
};
export default React.memo(Table)
//export default React.memo(Table ) ;
export default Table;
/*
/*
focused = { false}
tableNodeId={id}
field={field}
@@ -1,6 +1,6 @@
import DatabaseContext from "./database-context";
import { useContext } from "react";
import { useCallback, useContext } from "react";
import { db, powerSyncDb } from "../sync-provider/sync-provider";
import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
import { useQuery } from "@powersync/react";
@@ -47,85 +47,88 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
));
const createTable = async (table: TableInsertType): Promise<QueryResult> => {
return await db.insert(tables).values(table)
}
const editTable = async (table: TableInsertType): Promise<QueryResult> => {
return await db.update(tables).set(table).where(eq(tables.id, table.id))
}
const deleteTable = async (id: string): Promise<QueryResult> => {
const createTable = useCallback(async (table: TableInsertType): Promise<QueryResult> => {
return await db.insert(tables).values(table);
}, [db]);
const editTable = useCallback(async (table: TableInsertType): Promise<QueryResult> => {
return await db.update(tables).set(table).where(eq(tables.id, table.id));
}, [db]);
const deleteTable = useCallback(async (id: string): Promise<QueryResult> => {
return await db.delete(tables).where(eq(tables.id, id));
}
const deleteMultiTables = async (ids: string[]): Promise<QueryResult> => {
return await db.delete(tables).where(inArray(tables.id, ids ))
}
}, [db]);
const createField = async (field: FieldInsertType): Promise<QueryResult> => {
const deleteMultiTables = useCallback(async (ids: string[]): Promise<QueryResult> => {
return await db.delete(tables).where(inArray(tables.id, ids));
}, [db]);
const createField = useCallback(async (field: FieldInsertType): Promise<QueryResult> => {
return await db.insert(fields).values(field);
}
const editField = async (field: FieldInsertType): Promise<QueryResult> => {
return await db.update(fields).set(field).where(eq(fields.id, field.id))
}
const deleteField = async (id: string): Promise<QueryResult> => {
}, [db]);
const editField = useCallback(async (field: FieldInsertType): Promise<QueryResult> => {
return await db.update(fields).set(field).where(eq(fields.id, field.id));
}, [db]);
const deleteField = useCallback(async (id: string): Promise<QueryResult> => {
return await db.delete(fields).where(eq(fields.id, id));
}
}, [db]);
const orderTableFields = async (fields: FieldType[]): Promise<QueryResult> => {
const caseStatements = fields
.map((field: FieldType, index: number) => `WHEN '${field.id}' THEN ${index}`)
const orderTableFields = useCallback(async (fieldsList: FieldType[]): Promise<QueryResult> => {
const caseStatements = fieldsList
.map((field, index) => `WHEN '${field.id}' THEN ${index}`)
.join('\n ');
const ids = fields.map(u => `'${u.id}'`).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)
}
const createRelationship = async (relationship: RelationshipInsertType): Promise<QueryResult> => {
return await db.insert(relationships).values(relationship);
}
const editRelationship = async (relationship: RelationshipInsertType): Promise<QueryResult> => {
return await db.update(relationships).set(relationship).where(eq(relationships.id, relationship.id))
}
const deleteRelationship = async (id: string): Promise<QueryResult> => {
return await db.delete(relationships).where(eq(relationships.id, id));
}
const deleteMultiRelationships = async (ids: string[]): Promise<QueryResult> => {
return await db.delete(relationships).where(inArray(relationships.id, ids ))
}
const updateTablePositions = async (tables: TableInsertType[]): Promise<QueryResult> => {
const posXCases = tables
.map((table: TableInsertType) => `WHEN '${table.id}' THEN ${table.posX}`)
.join('\n ');
const posYCases = tables
.map((table: TableInsertType) => `WHEN '${table.id}' THEN ${table.posY}`)
.join('\n ');
const ids = tables.map((table: TableInsertType) => `'${table.id}'`).join(',\n ');
const sql = `
UPDATE tables
SET
posX = CASE id
${posXCases}
END,
posY = CASE id
${posYCases}
END
WHERE id IN (
${ids}
UPDATE fields
SET sequence = CASE id
${caseStatements}
END
WHERE id IN (
${ids}
);`;
return await powerSyncDb.execute(sql);
}
}, [powerSyncDb]);
const createRelationship = useCallback(async (relationship: RelationshipInsertType): Promise<QueryResult> => {
return await db.insert(relationships).values(relationship);
}, [db]);
const editRelationship = useCallback(async (relationship: RelationshipInsertType): Promise<QueryResult> => {
return await db.update(relationships).set(relationship).where(eq(relationships.id, relationship.id));
}, [db]);
const deleteRelationship = useCallback(async (id: string): Promise<QueryResult> => {
return await db.delete(relationships).where(eq(relationships.id, id));
}, [db]);
const deleteMultiRelationships = useCallback(async (ids: string[]): Promise<QueryResult> => {
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]);
return (
<DatabaseContext.Provider value={{
@@ -133,7 +136,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editTable,
deleteTable,
updateTablePositions,
deleteMultiTables ,
deleteMultiTables,
createField,
editField,
@@ -143,7 +146,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
createRelationship,
editRelationship,
deleteRelationship,
deleteMultiRelationships ,
deleteMultiRelationships,
tables: tablesList as TableType[],
relationships: relationshipsList as RelationshipType[],
@@ -0,0 +1,18 @@
import { createContext, Dispatch, SetStateAction } from "react";
export interface DiagramContextType {
focusedTableId : string | undefined ;
focusedRelationshipId : string | undefined;
isConnectionInProgress : boolean
focusOnTable : ( id : string , transition? : boolean ) => void ,
focusOnRelationship : ( id : string, transition? : boolean ) => void ,
setIsConnectionInProgress : Dispatch<boolean>
}
export default createContext<DiagramContextType>({} as DiagramContextType);
@@ -0,0 +1,98 @@
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { FitViewOptions, useReactFlow } from "@xyflow/react";
import { useNavigate } from "react-router-dom";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import DiagramContext from "./diagram-context";
interface Props { children: React.ReactNode }
const DiagramProvider: React.FC<Props> = ({ children }) => {
const { setNodes, fitView, setEdges } = useReactFlow();
const navigate = useNavigate();
const [focusedTableId, setFocusedTableId] = useState<string | undefined>(undefined)
const [focusedRelationshipId, setFocusedRelationshipId] = useState<string | undefined>(undefined)
const [isConnectionInProgress , setIsConnectionInProgress] = useState<boolean>( false) ;
const focusOnTable = useCallback((id: string, transition: boolean = false) => {
navigate("/database/tables");
setNodes((nodes) =>
nodes.map((node) => {
const selected: boolean = node.id === id;
if (selected && transition) {
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [{
id,
}],
});
}
return {
...node,
selected,
}
})
);
setFocusedTableId(id);
}, [setFocusedTableId])
const focusOnRelationship = useCallback((id: string, transition: boolean = false) => {
navigate("/database/relationships");
setFocusedRelationshipId(id);
setEdges((edges) =>
edges.map((edge) => {
const selected: boolean = edge.id === id;
if (selected && transition) {
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [{
id: (edge.data?.relationship as RelationshipType).sourceTableId
}, {
id: (edge.data?.relationship as RelationshipType).targetTableId
}]
});
}
return {
...edge,
selected
}
})
)
}, [setFocusedRelationshipId]);
const contextValue = useMemo(() => ({
focusedTableId,
focusedRelationshipId,
isConnectionInProgress ,
focusOnTable,
focusOnRelationship ,
setIsConnectionInProgress
}), [focusedTableId, focusedRelationshipId, focusOnTable, focusOnRelationship , isConnectionInProgress , setIsConnectionInProgress ]);
return (
<DiagramContext.Provider
value={contextValue}
>
{children}
</DiagramContext.Provider>
)
}
export const useDiagram = () => useContext(DiagramContext);
export default DiagramProvider;
+36 -1
View File
@@ -28,6 +28,10 @@
.react-flow__renderer {
cursor: default !important; /* or pointer, grab, etc. */
}
.sidebar-item[data-active="true"] svg {
color : hsl(var(--heroui-primary-900));
@@ -56,4 +60,35 @@ div[data-slot="content"] hr[role="separator"] {
to {
stroke-dashoffset: -10;
}
}
}
/* Target the scrollbar */
::-webkit-scrollbar {
width: 4px; /* Width of the scrollbar */
}
/* Track (background of the scrollbar) */
::-webkit-scrollbar-track {
background: hsl(210 40% 96.1%) ; /* Light gray background */
}
/* Handle (the scroll thumb) */
::-webkit-scrollbar-thumb {
background: hsl(var(--heroui-default-300)); /* Darker gray */
border-radius: 6px; /* Rounded corners */
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--nextui-default-400)); /* Darker on hover */
}
+57
View File
@@ -0,0 +1,57 @@
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { TableType } from "@/lib/schemas/table-schema";
import { Node } from "@xyflow/react";
import ELK from "elkjs/lib/elk.bundled.js";
const elk = new ELK();
const adjustTablesPositions = async (
nodes: Node[],
relationships: RelationshipType[]
): Promise<TableType[]> => {
// Extract tables (same as before)
const tables: TableType[] = nodes.map((node: Node) => node.data.table) as TableType[];
// Build the ELK graph structure
const graph = {
id: "root",
layoutOptions: {
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'elk.spacing.nodeNode': '80',
},
children: nodes.map((node) => ({
id: node.id,
width: node.measured?.width ?? 224,
height: node.measured?.height ?? 150,
})),
edges: relationships.map((rel) => ({
id: `${rel.sourceTableId}->${rel.targetTableId}`,
sources: [rel.sourceTableId],
targets: [rel.targetTableId],
})),
};
// Run ELK layout (async)
const layoutedGraph = await elk.layout(graph);
// Map positions back to your tables
tables.forEach((table) => {
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
}
});
return tables;
};
export {
adjustTablesPositions,
}
+13
View File
@@ -0,0 +1,13 @@
const areArraysEqual = (a: string[], b: string[]): boolean => {
if (a.length !== b.length) return false;
const sortedA = [...a].sort();
const sortedB = [...b].sort();
return sortedA.every((val, index) => val === sortedB[index]);
};
export {
areArraysEqual
}