From 2d5a082210a0bdb4b81e708dce957d1ac7c8362a Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Wed, 21 May 2025 19:53:33 +0100 Subject: [PATCH] split the database context into 2 data context and operatoins context to redeuce re-rendering --- src/App.tsx | 6 +- src/hooks/use-get-overlapping-nodes.tsx | 59 ++++++ src/hooks/use-get-related-edges.tsx | 24 +++ src/hooks/use-relationship-to-edge.tsx | 26 ++- src/hooks/use-table-to-node.tsx | 28 ++- .../database/database-control-buttons.tsx | 2 +- src/pages/database/database-page.tsx | 196 +++++++++++++----- .../relationship-accordion-body.tsx | 4 +- .../relationship-accordion-header.tsx | 12 +- .../relationship-controller.tsx | 9 +- .../table-accordion-item/field/field-item.tsx | 7 +- .../table-accordion-item/field/field-list.tsx | 4 +- .../table-accordion-body.tsx | 4 +- .../table-accordion-header.tsx | 4 +- .../tables-controller/tables-controller.tsx | 6 +- src/pages/database/table/field.tsx | 23 +- src/pages/database/table/relationship.tsx | 1 + src/pages/database/table/table.tsx | 176 +++++++++------- .../database-history-provider.tsx | 12 +- .../database-provider/database-context.ts | 28 ++- .../database-provider/database-provider.tsx | 74 ++++--- tailwind.config.js | 2 +- 22 files changed, 481 insertions(+), 226 deletions(-) create mode 100644 src/hooks/use-get-overlapping-nodes.tsx create mode 100644 src/hooks/use-get-related-edges.tsx diff --git a/src/App.tsx b/src/App.tsx index f5259a2..0a5de85 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import DatabaseProvider from "./providers/database-provider/database-provider"; import DiagramProvider from "./providers/diagram-provider/diagram-provider"; import { NextUIProvider } from "@nextui-org/react"; import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { HeroUIProvider, ToastProvider } from "@heroui/react"; @@ -16,7 +17,8 @@ function App() { const appRoutes = useAppRoutes(); return ( - + + - + ); } diff --git a/src/hooks/use-get-overlapping-nodes.tsx b/src/hooks/use-get-overlapping-nodes.tsx new file mode 100644 index 0000000..8bc4f65 --- /dev/null +++ b/src/hooks/use-get-overlapping-nodes.tsx @@ -0,0 +1,59 @@ +import { TableType } from "@/lib/schemas/table-schema"; +import { Node, useReactFlow } from "@xyflow/react"; +import { useMemo } from "react"; + +const useGetOverlappingNodes = (): Set => { + const { getNodes } = useReactFlow(); + const nodes = getNodes(); + + const overlappingNodes: Set = useMemo(() => { + const overlaps: Set = 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; \ No newline at end of file diff --git a/src/hooks/use-get-related-edges.tsx b/src/hooks/use-get-related-edges.tsx new file mode 100644 index 0000000..5b46acf --- /dev/null +++ b/src/hooks/use-get-related-edges.tsx @@ -0,0 +1,24 @@ +import { Edge, useStore } from "@xyflow/react"; +import { useEffect, useState } from "react"; +import hash from 'object-hash'; + + + + + +const useGetRelatedEdges = ( nodeId : string) => { + const edges = useStore((store) => store.edges) as Edge[]; + const [relatedEdges, setRelatedEdges] = useState([]); + + useEffect(() => { + const newRelatedEdges: Edge[] = edges.filter((edge: Edge) => edge.source == nodeId || edge.target == nodeId); + setRelatedEdges((previousEdges :Edge[]) => { + return hash(newRelatedEdges) == hash(previousEdges) ? previousEdges : newRelatedEdges + }) + }, [edges, nodeId]) + + return relatedEdges +} + + +export default useGetRelatedEdges; \ No newline at end of file diff --git a/src/hooks/use-relationship-to-edge.tsx b/src/hooks/use-relationship-to-edge.tsx index e6ba4d2..ef16700 100644 --- a/src/hooks/use-relationship-to-edge.tsx +++ b/src/hooks/use-relationship-to-edge.tsx @@ -1,26 +1,40 @@ import { RelationshipType } from "@/lib/schemas/relationship-schema"; import { LEFT_PREFIX, TARGET_PREFIX } from "@/pages/database/table/field"; - import { Edge, useReactFlow } from "@xyflow/react"; import { useEffect } from "react"; - +import hash from 'object-hash'; export const useRelationshipToEdge = (relationships: RelationshipType[]): void => { const { setEdges } = useReactFlow(); useEffect(() => { - const edges = relationships.map((relationship: RelationshipType) => { + + const relationshipEdges = relationships.map((relationship: RelationshipType) => { return { id: relationship.id, source: relationship.sourceTableId, sourceHandle: LEFT_PREFIX + relationship.sourceFieldId, target: relationship.targetTableId, targetHandle: TARGET_PREFIX + relationship.targetFieldId, - data : { - relationship + selected: false , + animated : false , + data: { + relationship } } as Edge }) - setEdges(edges) + + setEdges((edges: any) => { + return relationshipEdges.map((relationshipEdge: any) => { + const edge = edges.find((edge: Edge) => edge.id == relationshipEdge.id); + if (!edge) + return relationshipEdge; + + const relationshipEdgeHash: string = hash(relationshipEdge); + const edgeHash: string = hash(edge); + + return relationshipEdgeHash == edgeHash ? edge : relationshipEdge; + }) + }); }, [relationships]) } \ No newline at end of file diff --git a/src/hooks/use-table-to-node.tsx b/src/hooks/use-table-to-node.tsx index 8731f16..bb33d91 100644 --- a/src/hooks/use-table-to-node.tsx +++ b/src/hooks/use-table-to-node.tsx @@ -1,29 +1,47 @@ - + import { TableType } from "@/lib/schemas/table-schema"; import { Node, useReactFlow } from "@xyflow/react"; import { useEffect } from "react"; - +import useGetOverlappingNodes from "./use-get-overlapping-nodes"; +import hash from 'object-hash'; export const useTableToNode = (tables: TableType[]): void => { const { setNodes } = useReactFlow(); + useEffect(() => { - const nodes = tables.map((table: TableType) => { + const tableNodes = tables.map((table: TableType) => { return { id: table.id, type: "table", + position: { x: table.posX, y: table.posY }, data: { - table + table, }, style: { width: 224 } } as Node }) - setNodes(nodes) + + + setNodes((nodes) => { + + return tableNodes.map((tableNode: Node) => { + const node = nodes.find((node: Node) => node.id == tableNode.id); + if (!node) { + return tableNode; + } + + const hashNode: string = hash((node.data as any).table); + const hashTableNode: string = hash((tableNode.data as any).table); + return hashNode == hashTableNode ? node : tableNode; + + }) + }) }, [tables]) } \ No newline at end of file diff --git a/src/pages/database/database-control-buttons.tsx b/src/pages/database/database-control-buttons.tsx index 7e1a5c3..a0dbc6a 100644 --- a/src/pages/database/database-control-buttons.tsx +++ b/src/pages/database/database-control-buttons.tsx @@ -52,7 +52,7 @@ const DatabaseControlButtons: React.FC = ({ adjustPositions }) }, []); return ( - = () => { - const { database, updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabase(); + const { database } = useDatabase(); + const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship, getField } = useDatabaseOperations(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const { setIsConnectionInProgress } = useDiagram(); const [selectedNodeIds, setSelectedNodeIds] = useState([]); - - const { tables, relationships } = database ; + const [isTableOverlappingPulsing, setIsTableOverlappingPulsing] = useState(false); + const { tables, relationships } = database; const { fitView } = useReactFlow(); const nodeTypes = useMemo(() => ({ table: Table }), []); const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []); 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, ""); - createRelationship({ - id: v4(), - sourceTableId: connection.source, - targetTableId: connection.target, - sourceFieldId: (connection.sourceHandle as string).split("_").pop(), - targetFieldId: (connection.targetHandle as string).replace(TARGET_PREFIX, "") - } as RelationshipInsertType) + const sourceField: FieldType | undefined = getField(connection.source, sourceFieldId as string); + const targetField: FieldType | undefined = getField(connection.target, targetFieldId); + console.log(sourceField, targetField) + if (sourceField?.typeId == targetField?.typeId) { - setEdges((eds) => addEdge(connection, eds)); + createRelationship({ + id: v4(), + sourceTableId: connection.source, + targetTableId: connection.target, + sourceFieldId, + targetFieldId + } as RelationshipInsertType); + + setEdges((eds) => addEdge(connection, eds)); + } else { + addToast({ + title: "Invalid Relationship", + description: "Relationship should be between primary key and foreign key of the same time", + color: "danger", + }) + } setIsConnectionInProgress(false); - }, []); + }, [database]); const handleNodesChanges: OnNodesChange = useCallback((changes: NodeChange[]) => { @@ -124,6 +142,7 @@ const DatabasePage: React.FC = () => { useTableToNode(tables); useRelationshipToEdge(relationships); + const adjustPositions = useCallback(async () => { updateTablePositions(await adjustTablesPositions(nodes, relationships)); setTimeout(() => { @@ -133,46 +152,127 @@ const DatabasePage: React.FC = () => { }, 500) }, [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); + }, []) + return (
- - - +
+ - + - - - + + + + + +
+ { + overlappingNodes.size > 0 && + + + + + + + + Overlapping Tables + + + } +
+
diff --git a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx index aad07f1..c80fc39 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-accordion-item/relationship-accordion-body.tsx @@ -1,7 +1,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"; import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { Button, Select, SelectItem, SharedSelection } from "@heroui/react"; import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, SquareArrowLeft, SquareArrowRight, Trash2 } from "lucide-react"; import { Key, useEffect, useState } from "react"; @@ -19,7 +19,7 @@ interface RelationshipAccordionBodyProps { const RelationshipAccordionBody: React.FC = ({ relationship }) => { const [cardinality, setCardinality] = useState(new Set([relationship.cardinality])); - const { editRelationship, deleteRelationship } = useDatabase(); + const { editRelationship, deleteRelationship } = useDatabaseOperations(); const { t } = useTranslation(); 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 d590a3f..6731edb 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,10 +1,10 @@ import { useRelationshipName } from "@/hooks/use-relationship-name"; import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } 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"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -19,7 +19,7 @@ const RelationshipAccordionHeader: React.FC = const [editMode, setEditMode] = useState(false); const { name: defaultName } = useRelationshipName(relationship); - const { editRelationship, deleteRelationship } = useDatabase(); + const { editRelationship, deleteRelationship } = useDatabaseOperations(); const { t } = useTranslation(); const [popOverOpen, setPopOverOpen] = useState(false); const [name, setName] = useState(relationship.name ? relationship.name : defaultName); @@ -40,6 +40,11 @@ const RelationshipAccordionHeader: React.FC = deleteRelationship(relationship.id); setPopOverOpen(false); } + + + useEffect(() => { + setName(relationship.name ? relationship.name : defaultName) ; + } , [relationship.name]) return (
= }
- ) } 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 7398bee..e6b76c6 100644 --- a/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx +++ b/src/pages/database/db-controller/relationship-controller/relationship-controller.tsx @@ -8,7 +8,7 @@ import RelationshipAccordionBody from "./relationship-accordion-item/relationshi import Modal from "@/components/modal/modal"; import CreateRelationshipForm from "./create-relationship-form/create-relationship-form"; import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { v4 } from "uuid"; import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; @@ -24,8 +24,9 @@ const RelationshipController: React.FC = ({ }) => { const [relationship, setRelationship] = useState(undefined); const [isValid, setIsValid] = useState(false); const { isOpen, onOpen, onOpenChange } = useDisclosure(); - const { createRelationship, database } = useDatabase(); - const { relationships } = database ; + const { database } = useDatabase(); + const { createRelationship, } = useDatabaseOperations(); + const { relationships } = database; const { t } = useTranslation(); const [selectedRelationship, setSelectedRelationship] = useState(new Set([])); @@ -37,7 +38,7 @@ const RelationshipController: React.FC = ({ }) => { createRelationship({ id: newRelationshipId, - ...relationship, + ...relationship, } as RelationshipInsertType); setSelectedRelationship(new Set([newRelationshipId]) as any); diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx index a6f8254..3e774ab 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-item.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; import { CSS } from "@dnd-kit/utilities"; import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema"; import { Key, useEffect, useState } from "react"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import Autocomplete from "@/components/auto-complete/auto-complete"; import ToggleButton from "@/components/toggle/toggle"; interface Props { @@ -21,8 +21,9 @@ const FieldItem: React.FC = ({ field }) => { const [fieldName, setFieldName] = useState(field.name); const [popOverOpen, setPopOverOpen] = useState(false); - const { deleteField, editField, data_types } = useDatabase(); - + const { data_types } = useDatabase(); + const {deleteField, editField} = useDatabaseOperations() ; + const [note, setNote] = useState(field.note as string | undefined); const [selectedType, setSelectedType] = useState(field.typeId as string | undefined); const { t } = useTranslation(); diff --git a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-list.tsx b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-list.tsx index d8969c8..147d4e5 100644 --- a/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-list.tsx +++ b/src/pages/database/db-controller/tables-controller/table-accordion-item/field/field-list.tsx @@ -8,7 +8,7 @@ import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-ki import { FieldType } from "@/lib/schemas/field-schema"; import { TableType } from "@/lib/schemas/table-schema"; import { useCallback, useEffect, useState } from "react"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { v4 } from "uuid"; import { getNextSequence } from "@/utils/field"; @@ -22,7 +22,7 @@ const FieldList: React.FC = ({ table }) => { const { t } = useTranslation(); const [fields, setFields] = useState(table.fields); - const { createField, orderTableFields } = useDatabase(); + const { createField, orderTableFields } = useDatabaseOperations(); useEffect(() => { setFields(table.fields) 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 be30ec0..c622d7a 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 @@ -8,7 +8,7 @@ import ColorPicker from "@/components/color-picker/color-picker"; import FieldList from "./field/field-list"; import IndexesList from "./index/indexes-list"; import { TableType } from "@/lib/schemas/table-schema"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { getNextSequence } from "@/utils/field"; import { v4 } from "uuid"; @@ -22,7 +22,7 @@ const TableAccordionBody: React.FC = ({ table }) => { const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"])); const [note, setNote] = useState(table.note ? table.note : ""); const { t } = useTranslation(); - const { editTable, createField } = useDatabase(); + const { editTable, createField } = useDatabaseOperations(); const onColorChange = useCallback((color: string | undefined) => { 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 ecd1a9c..996f027 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 @@ -6,7 +6,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { TableInsertType, TableType } from "@/lib/schemas/table-schema"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { v4 } from "uuid"; import { getNextSequence } from "@/utils/field"; import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; @@ -18,7 +18,7 @@ export interface TableAccordionHeaderProps { const TableAccordionHeader: React.FC = ({ table, isOpen }) => { - const { editTable, deleteTable, createField } = useDatabase(); + const { editTable, deleteTable, createField } = useDatabaseOperations(); const [popOverOpen, setPopOverOpen] = useState(false); const [tableName, setTableName] = useState(table.name); 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 1158906..2005be5 100644 --- a/src/pages/database/db-controller/tables-controller/tables-controller.tsx +++ b/src/pages/database/db-controller/tables-controller/tables-controller.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { useCallback, useEffect, useState } from "react"; 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 { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { TableInsertType, TableType } from "@/lib/schemas/table-schema"; import { v4 } from "uuid"; import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; @@ -17,8 +17,8 @@ interface Props { } const TablesController: React.FC = ({ }) => { - const { database, createTable } = useDatabase(); - + const { database } = useDatabase(); + const {createTable} = useDatabaseOperations() ; const { tables } = database ; const { t } = useTranslation(); const [selectedTable, setSelectedTable] = useState(new Set([])); diff --git a/src/pages/database/table/field.tsx b/src/pages/database/table/field.tsx index 2084784..e55d45c 100644 --- a/src/pages/database/table/field.tsx +++ b/src/pages/database/table/field.tsx @@ -1,6 +1,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"; import { FieldType } from "@/lib/schemas/field-schema"; -import { useDatabase } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } 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"; @@ -12,8 +12,8 @@ import React, { useEffect, useState } from "react"; interface Props { field: FieldType, showHandles?: boolean, - highlight?: boolean , - showTargetHandle? : boolean + highlight?: boolean, + showTargetHandle?: boolean } @@ -25,21 +25,18 @@ export const TARGET_PREFIX = "target_"; const Field: React.FC = ({ field, showHandles, highlight }) => { const [editMode, setEditMode] = useState(false); - const { deleteField, editField } = useDatabase(); + const { deleteField, editField } = useDatabaseOperations(); const [fieldName, setFieldName] = useState(field.name); - const {isConnectionInProgress} = useDiagram() ; + const { isConnectionInProgress } = useDiagram(); useEffect(() => { setFieldName(field.name); }, [field.name]); - - const removeField = () => { deleteField(field.id) } - const saveFieldName = () => { editField({ id: field.id, @@ -48,11 +45,11 @@ const Field: React.FC = ({ field, showHandles, highlight }) => { setEditMode(false); } - - + // console.log("render field ", field.name) + return (
{ @@ -125,12 +122,12 @@ const Field: React.FC = ({ field, showHandles, highlight }) => { type="source" position={Position.Left} id={LEFT_PREFIX + field.id} - className="w-4 h-4 border-4 bg-primary dark:border-default-900" + className="w-4 h-4 border-3 bg-primary dark:border-default-900" />
diff --git a/src/pages/database/table/relationship.tsx b/src/pages/database/table/relationship.tsx index 97101a3..4683861 100644 --- a/src/pages/database/table/relationship.tsx +++ b/src/pages/database/table/relationship.tsx @@ -113,6 +113,7 @@ const Relationship: React.FC> = (props) => { } }, [data?.relationship.cardinality, selected]); + // console.log ("render relationship " , props.data?.relationship.sourceTable.name , props.data?.relationship.targetTable.name) return ( <> diff --git a/src/pages/database/table/table.tsx b/src/pages/database/table/table.tsx index 7cf5b25..22f1cfc 100644 --- a/src/pages/database/table/table.tsx +++ b/src/pages/database/table/table.tsx @@ -14,26 +14,28 @@ 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 } from "@/providers/database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { useTranslation } from "react-i18next"; import { RelationshipType } from "@/lib/schemas/relationship-schema"; import { useDiagram } from "@/providers/diagram-provider/diagram-provider"; - +import useGetRelatedEdges from "@/hooks/use-get-related-edges"; export type TableProps = Node<{ table: TableType, + overlapping?: boolean, + pulsing?: boolean, }> -const Table: React.FC> = ({ selected, data: { table } }) => { - +const Table: React.FC> = (props) => { + const { selected, data: { table, overlapping = false, pulsing = false } } = props; const [editMode, setEditMode] = useState(false); const [tableName, setTableName] = useState(table.name); - const { editTable } = useDatabase(); - + const { editTable } = useDatabaseOperations(); + const edges = useGetRelatedEdges(table.id as string); const { focusOnTable } = useDiagram(); - const { t } = useTranslation(); + const { t } = useTranslation(); - useEffect(() => { + useEffect(() => { setTableName(table.name); }, [table.name]) @@ -46,8 +48,6 @@ const Table: React.FC> = ({ selected, data: { table } }) = focusOnTable(table.id, false); }, [table]) - const edges = useStore((store) => store.edges) as Edge[]; - const highlightedEdges: Edge[] = useMemo(() => { return edges.filter((edge: Edge) => edge.animated || edge.selected); }, [edges]); @@ -68,86 +68,102 @@ const Table: React.FC> = ({ selected, data: { table } }) = }, [table.fields, selected, highlightedEdges]); + useEffect(() => { + console.log("re-render ", table.name) + + }, [useDatabase]) ; + return ( - - -
-
-
-
- - { - editMode && <> - setTableName(event.target.value)} - value={tableName} - onBlur={saveTableName} - type="text" - className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 font-bold bg-slate-100 focus-visible:ring-0 text-sm dark:bg-transparent dark:text-white" - /> + + +
+
+
+
+ + { + editMode && <> + setTableName(event.target.value)} + value={tableName} + onBlur={saveTableName} + type="text" + className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 font-bold bg-slate-100 focus-visible:ring-0 text-sm dark:bg-transparent dark:text-white" + /> + + + } + { + !editMode && + <> + + + + + + {t("table.double_click")} + + + +
+ - - } - { - !editMode && - <> - - - - - - {t("table.double_click")} - - +
+ + } +
+
+ {fields} +
+
-
- - -
- - } -
-
- {fields} -
-
- ) }; export default React.memo(Table) - \ No newline at end of file diff --git a/src/providers/database-history/database-history-provider.tsx b/src/providers/database-history/database-history-provider.tsx index 0e8f134..9598c8d 100644 --- a/src/providers/database-history/database-history-provider.tsx +++ b/src/providers/database-history/database-history-provider.tsx @@ -1,7 +1,7 @@ import { useCallback, useContext, useEffect, useRef } from "react"; import DatabaseHistoryContext from "./database-history-context"; import useUndo from 'use-undo'; -import { useDatabase } from "../database-provider/database-provider"; +import { useDatabase, useDatabaseOperations } from "../database-provider/database-provider"; import hash from 'object-hash'; import { DBDiffOperation, mapDiffToDBDiffOperation, normalizeDatabase } from "@/utils/database"; import { compare } from 'fast-json-patch'; @@ -12,7 +12,9 @@ interface Props { children: React.ReactNode }; const DatabaseHistoryProvider: React.FC = ({ children }) => { const udpateDbFlag = useRef(false); - const { database, executeDbDiffOps } = useDatabase(); + const { database } = useDatabase(); + const { executeDbDiffOps} = useDatabaseOperations() ; + const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo(database); useEffect(() => { @@ -21,8 +23,6 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { const databaseHash: string = hash(database, { algorithm: 'sha1' }); if (presentHash != databaseHash) set(database); - - }, [database]); @@ -47,15 +47,11 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { const differences = compare(normalizedDatabase, normalizedPresent); if (differences && differences.length > 0) { - const operations: DBDiffOperation[] = mapDiffToDBDiffOperation(differences); executeDbDiffOps(operations) } }, [datatbaseState.present]); - - - return ( Promise, editTable: (table: TableInsertType) => Promise, deleteTable: (id: string) => Promise, - updateTablePositions: (tables: TableInsertType[]) => Promise , + updateTablePositions: (tables: TableInsertType[]) => Promise, deleteMultiTables: (ids: string[]) => Promise // field operations createField: (field: FieldInsertType) => Promise, editField: (field: FieldInsertType) => Promise, deleteField: (id: string) => Promise, orderTableFields: (fields: FieldType[]) => Promise, + getField: (tableId: string, id: string) => FieldType | undefined, // relationship operations createRelationship: (relationship: RelationshipInsertType) => Promise, editRelationship: (relationship: RelationshipInsertType) => Promise, deleteRelationship: (id: string) => Promise, - deleteMultiRelationships: (ids: string[]) => Promise , - - executeDbDiffOps : ( operations : DBDiffOperation[]) => void , - + deleteMultiRelationships: (ids: string[]) => Promise, + // execute the diff operation whenver user click in undo or redo + executeDbDiffOps: (operations: DBDiffOperation[]) => void, } +export const DatabaseDataContext = createContext({} as DatabaseDataContextType); +export const DatabaseOperationsContext = createContext({} as DatabaseOperationsContextType); -export default createContext({} as DatabaseContextType); - diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index fc50df7..79351b4 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -1,8 +1,8 @@ -import DatabaseContext from "./database-context"; +import { DatabaseDataContext, DatabaseOperationsContext } from "./database-context"; import { useCallback, useContext, useEffect, useState } from "react"; import { db, powerSyncDb } from "../sync-provider/sync-provider"; -import { TableInsertType, tables } from "@/lib/schemas/table-schema"; +import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema"; import { useQuery } from "@powersync/react"; import { toCompilableQuery } from "@powersync/drizzle-driver"; import { asc, desc, eq, inArray, or } from "drizzle-orm"; @@ -113,6 +113,13 @@ const DatabaseProvider: React.FC = ({ children }) => { }) }, [db]); + + 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 => { const caseStatements = fieldsList .map((field, index) => `WHEN '${field.id}' THEN ${index}`) @@ -188,7 +195,7 @@ const DatabaseProvider: React.FC = ({ children }) => { await tx.insert(fields).values(Object.values(operation.table.fields)); } 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") { @@ -216,43 +223,52 @@ const DatabaseProvider: React.FC = ({ children }) => { } }) - }, [db, currentDatabaseId]) + }, [db, currentDatabaseId]); + return ( - - { - !isLoading && database && - - {children} - - } - + + + + { + !isLoading && database && + + {children} + + } + + ) } -export const useDatabase = () => useContext(DatabaseContext); +export const useDatabase = () => useContext(DatabaseDataContext); +export const useDatabaseOperations = () => useContext(DatabaseOperationsContext); export default DatabaseProvider; diff --git a/tailwind.config.js b/tailwind.config.js index 32bcc5a..95d86d7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,7 +6,7 @@ export default { "./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", - "./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal).js", + "./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal|toast).js", ], darkMode: "class",