From 7f7779d579263ec6251dc374d358407e9d47ccbb Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Fri, 16 May 2025 01:23:28 +0100 Subject: [PATCH] Fix nodes re-render whene dragging --- package.json | 4 +- src/App.tsx | 9 +- src/components/sidebar/sidebar.tsx | 4 +- src/hooks/use-table-to-node.tsx | 1 + src/lib/colors.ts | 19 +-- src/pages/database/database-page.tsx | 139 ++++++++++------ .../relationship-accordion-header.tsx | 7 +- .../relationship-controller.tsx | 14 +- .../table-accordion-body.tsx | 2 +- .../table-accordion-header.tsx | 27 ++-- .../tables-controller/tables-controller.tsx | 14 +- src/pages/database/table/field.tsx | 12 +- src/pages/database/table/relationship.tsx | 17 +- src/pages/database/table/table.tsx | 115 +++++--------- .../database-provider/database-provider.tsx | 149 +++++++++--------- .../diagram-provider/diagram-context.tsx | 18 +++ .../diagram-provider/diagram-provider.tsx | 98 ++++++++++++ src/styles/globals.css | 37 ++++- src/utils/tables.ts | 57 +++++++ src/utils/utils.ts | 13 ++ 20 files changed, 508 insertions(+), 248 deletions(-) create mode 100644 src/providers/diagram-provider/diagram-context.tsx create mode 100644 src/providers/diagram-provider/diagram-provider.tsx create mode 100644 src/utils/tables.ts create mode 100644 src/utils/utils.ts diff --git a/package.json b/package.json index 05d4641..78afc02 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 5f0ede0..cf0ad0c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { - - {appRoutes} - + + + {appRoutes} + + diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx index a58b98c..9c2d100 100644 --- a/src/components/sidebar/sidebar.tsx +++ b/src/components/sidebar/sidebar.tsx @@ -12,9 +12,9 @@ const sidebarItemClass : string = "text-gray-700 size-4 data-[active=true]:text- const Sidebar: React.FC = ({ }) => { const { t , i18n } = useTranslation() ; - console.log ( i18n.language) + - console.log () + const location = useLocation() ; const sidebarItems: SidebarItemProps[] = useMemo(() => [ diff --git a/src/hooks/use-table-to-node.tsx b/src/hooks/use-table-to-node.tsx index baa8db5..8731f16 100644 --- a/src/hooks/use-table-to-node.tsx +++ b/src/hooks/use-table-to-node.tsx @@ -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, diff --git a/src/lib/colors.ts b/src/lib/colors.ts index 5db6713..dab0423 100644 --- a/src/lib/colors.ts +++ b/src/lib/colors.ts @@ -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)]; }; \ No newline at end of file diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index 1a21cce..bfec7da 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -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 = ({ initialTables }) => { +const DatabasePage: React.FC = () => { 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([]); + 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 = ({ initialTables }) => { } as RelationshipInsertType) setEdges((eds) => addEdge(connection, eds)); + setIsConnectionInProgress(false); - }, [setEdges]); + }, []); const handleNodesChanges: OnNodesChange = useCallback((changes: NodeChange[]) => { @@ -60,75 +61,124 @@ const DatabasePage: React.FC = ({ 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 = useCallback((changes: EdgeChange[]) => { + 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[]); + return onEdgesChange(changes as EdgeChange[]); }, [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 ( -
+ }, [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 ( + +
+
- +
+ + + + + + + + + Adjust Positions + + + +
@@ -149,6 +199,7 @@ const DatabasePage: React.FC = ({ initialTables }) => {
+ ) } diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx index 31c2dde..99eb520 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-header.tsx @@ -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 = const { editRelationship, deleteRelationship } = useDatabase(); const { t } = useTranslation(); const [popOverOpen, setPopOverOpen] = useState(false); - const [name, setName] = useState(relationship.name ? relationship.name : defaultName) + const [name, setName] = useState(relationship.name ? relationship.name : defaultName); + const { focusOnRelationship } = useDiagram(); const editRelationshipName = () => { @@ -34,7 +36,7 @@ const RelationshipAccordionHeader: React.FC = } const onDeleteRelationship = () => { - + deleteRelationship(relationship.id); setPopOverOpen(false); } @@ -85,6 +87,7 @@ const RelationshipAccordionHeader: React.FC = size="sm" isIconOnly variant="light" + onPressEnd={() => focusOnRelationship(relationship.id, true)} > diff --git a/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx b/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx index d528658..0575514 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx @@ -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 = ({ }) => { 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 = ({ }) => { }, [relationship]); + useEffect(() => { + if (focusedRelationshipId) { + setSelectedRelationship(new Set([focusedRelationshipId]) as any); + } + + }, [focusedRelationshipId]) + return (
@@ -109,12 +118,13 @@ const RelationshipController: React.FC = ({ }) => { + } > diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx index ee012e4..c753d0a 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-body.tsx @@ -25,7 +25,7 @@ const TableAccordionBody: React.FC = ({ 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]); diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx index b80406a..57e1f45 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/table-accordion-header.tsx @@ -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 = ({ table, isOp const { t } = useTranslation(); const [editMode, setEditMode] = useState(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 = ({ table, isOp nullable: true, }) } - + return (
= ({ table, isOp !editMode && <>
- + +
diff --git a/src/pages/database/db-controller/tables-controller/tables-controller.tsx b/src/pages/database/db-controller/tables-controller/tables-controller.tsx index 0a5e6ed..91cbeff 100644 --- a/src/pages/database/db-controller/tables-controller/tables-controller.tsx +++ b/src/pages/database/db-controller/tables-controller/tables-controller.tsx @@ -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 = ({ }) => { 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 = ({ }) => { }, [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 = ({ }) => { selectedKeys={selectedTable} onSelectionChange={setSelectedTable as any} isCompact - + > {tables.map((table: TableType) => ( = ({ field, showHandles, highlight }) => { const [editMode, setEditMode] = useState(false); const { deleteField, editField } = useDatabase(); const [fieldName, setFieldName] = useState(field.name); - + const {isConnectionInProgress} = useDiagram() ; useEffect(() => { setFieldName(field.name); @@ -46,8 +48,8 @@ const Field: React.FC = ({ field, showHandles, highlight }) => { setEditMode(false); } - const connection = useConnection(); - console.log(highlight) + + return (
= ({ field, showHandles, highlight }) => {
> = (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> = (props) => { const targetLeftX = targetX - 2; const targetRightX = targetX + targetWidth + 3; - + const { sourceSide, targetSide } = useMemo(() => { const distances = { @@ -122,14 +124,13 @@ const Relationship: React.FC> = (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> = (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> = (props) => { ) } -export default Relationship; +export default React.memo(Relationship); /* diff --git a/src/pages/database/table/table.tsx b/src/pages/database/table/table.tsx index 780cac6..2105a7f 100644 --- a/src/pages/database/table/table.tsx +++ b/src/pages/database/table/table.tsx @@ -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> = React.memo(({ - selected, - dragging, - id, - data: { table, }, -}) => { +const Table: React.FC> = ({ selected, data: { table } }) => { const [editMode, setEditMode] = useState(false); const [tableName, setTableName] = useState(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 () + }) + }, [table.fields, selected, highlightedEdges]); + + + - console.log(highlightedEdges) + // console.log ("re-render" , table.name) + return ( > = 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} > -
}
- - -
- {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 () - })} +
+ {fields}
- ) -}); +}; + +export default React.memo(Table) +//export default React.memo(Table ) ; - -export default Table; - - -/* - - +/* focused = { false} tableNodeId={id} field={field} diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 7111aa6..f1f6197 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -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 = ({ children }) => { )); - const createTable = async (table: TableInsertType): Promise => { - return await db.insert(tables).values(table) - } - const editTable = async (table: TableInsertType): Promise => { - return await db.update(tables).set(table).where(eq(tables.id, table.id)) - } - const deleteTable = async (id: string): Promise => { + const createTable = useCallback(async (table: TableInsertType): Promise => { + return await db.insert(tables).values(table); + }, [db]); + + const editTable = useCallback(async (table: TableInsertType): Promise => { + return await db.update(tables).set(table).where(eq(tables.id, table.id)); + }, [db]); + + const deleteTable = useCallback(async (id: string): Promise => { return await db.delete(tables).where(eq(tables.id, id)); - } - const deleteMultiTables = async (ids: string[]): Promise => { - return await db.delete(tables).where(inArray(tables.id, ids )) - } + }, [db]); - const createField = async (field: FieldInsertType): Promise => { + const deleteMultiTables = useCallback(async (ids: string[]): Promise => { + return await db.delete(tables).where(inArray(tables.id, ids)); + }, [db]); + + const createField = useCallback(async (field: FieldInsertType): Promise => { return await db.insert(fields).values(field); - } - const editField = async (field: FieldInsertType): Promise => { - return await db.update(fields).set(field).where(eq(fields.id, field.id)) - } - const deleteField = async (id: string): Promise => { + }, [db]); + + const editField = useCallback(async (field: FieldInsertType): Promise => { + return await db.update(fields).set(field).where(eq(fields.id, field.id)); + }, [db]); + + const deleteField = useCallback(async (id: string): Promise => { return await db.delete(fields).where(eq(fields.id, id)); - } + }, [db]); - const orderTableFields = async (fields: FieldType[]): Promise => { - const caseStatements = fields - .map((field: FieldType, index: number) => `WHEN '${field.id}' THEN ${index}`) + const orderTableFields = useCallback(async (fieldsList: FieldType[]): Promise => { + 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 => { - return await db.insert(relationships).values(relationship); - } - const editRelationship = async (relationship: RelationshipInsertType): Promise => { - return await db.update(relationships).set(relationship).where(eq(relationships.id, relationship.id)) - } - const deleteRelationship = async (id: string): Promise => { - return await db.delete(relationships).where(eq(relationships.id, id)); - } - - const deleteMultiRelationships = async (ids: string[]): Promise => { - return await db.delete(relationships).where(inArray(relationships.id, ids )) - } - - const updateTablePositions = async (tables: TableInsertType[]): Promise => { - - 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 => { + return await db.insert(relationships).values(relationship); + }, [db]); + + const editRelationship = useCallback(async (relationship: RelationshipInsertType): Promise => { + return await db.update(relationships).set(relationship).where(eq(relationships.id, relationship.id)); + }, [db]); + + const deleteRelationship = useCallback(async (id: string): Promise => { + return await db.delete(relationships).where(eq(relationships.id, id)); + }, [db]); + + const deleteMultiRelationships = useCallback(async (ids: string[]): Promise => { + return await db.delete(relationships).where(inArray(relationships.id, ids)); + }, [db]); + + const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise => { + 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 ( = ({ children }) => { editTable, deleteTable, updateTablePositions, - deleteMultiTables , + deleteMultiTables, createField, editField, @@ -143,7 +146,7 @@ const DatabaseProvider: React.FC = ({ children }) => { createRelationship, editRelationship, deleteRelationship, - deleteMultiRelationships , + deleteMultiRelationships, tables: tablesList as TableType[], relationships: relationshipsList as RelationshipType[], diff --git a/src/providers/diagram-provider/diagram-context.tsx b/src/providers/diagram-provider/diagram-context.tsx new file mode 100644 index 0000000..f9f2e97 --- /dev/null +++ b/src/providers/diagram-provider/diagram-context.tsx @@ -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 + +} + + + +export default createContext({} as DiagramContextType); \ No newline at end of file diff --git a/src/providers/diagram-provider/diagram-provider.tsx b/src/providers/diagram-provider/diagram-provider.tsx new file mode 100644 index 0000000..4c5c67f --- /dev/null +++ b/src/providers/diagram-provider/diagram-provider.tsx @@ -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 = ({ children }) => { + + const { setNodes, fitView, setEdges } = useReactFlow(); + const navigate = useNavigate(); + const [focusedTableId, setFocusedTableId] = useState(undefined) + const [focusedRelationshipId, setFocusedRelationshipId] = useState(undefined) + const [isConnectionInProgress , setIsConnectionInProgress] = useState( 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 ( + + {children} + + ) + +} + + +export const useDiagram = () => useContext(DiagramContext); + + +export default DiagramProvider; \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index 899af16..9df5ba4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -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; } -} \ No newline at end of file +} + + + + +/* 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 */ +} diff --git a/src/utils/tables.ts b/src/utils/tables.ts new file mode 100644 index 0000000..d4e0af9 --- /dev/null +++ b/src/utils/tables.ts @@ -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 => { + + // 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, + +} \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..bf7945c --- /dev/null +++ b/src/utils/utils.ts @@ -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 +} \ No newline at end of file