= ({ 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