diff --git a/src/hooks/use-overlapping-nodes.tsx b/src/hooks/use-overlapping-nodes.tsx deleted file mode 100644 index 7b78249..0000000 --- a/src/hooks/use-overlapping-nodes.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { TableType } from "@/lib/schemas/table-schema"; -import { areArraysEqual } from "@/utils/utils"; -import { Node, useReactFlow } from "@xyflow/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; - -// Return type of the hook -type UseOverlapingType = { - isOverlapping: boolean, // Indicates if any nodes are overlapping - puls: () => void // Triggers a short visual pulse effect for overlapping nodes -} - -/** - * Custom hook to detect overlapping nodes in a React Flow diagram, - * annotate them with metadata (`overlapping`, `pulsing`), - * and provide a pulse trigger for visual feedback. - */ -const useOverlappingNodes = (nodes: Node[]): UseOverlapingType => { - const [isPulsing, setIsPulsing] = useState(false); - const { setNodes } = useReactFlow(); - - /** - * Triggers a brief "pulse" effect (e.g. for animations or styles) - * by toggling a boolean flag for a short time. - */ - const puls = useCallback(() => { - setIsPulsing(true); - setTimeout(() => setIsPulsing(false), 200); - }, []); - - /** - * Compute which nodes are currently overlapping. - * This is memoized to avoid recalculating unless nodes change. - */ - const overlappingNodes: Set = useMemo(() => { - const overlaps: Set = new Set(); - - // Compare every pair of nodes - for (let i = 0; i < nodes.length; i++) { - for (let j = i + 1; j < nodes.length; j++) { - if (isNodesOverlapping(nodes[i], nodes[j])) { - overlaps.add(nodes[i].id); - overlaps.add(nodes[j].id); - } - } - } - - return overlaps; - }, [nodes]); - - /** - * When overlapping nodes change, update each node’s `data.overlapping` property accordingly. - * Avoids unnecessary updates using deep comparison. - */ - useEffect(() => { - const overlappingIds: string[] = Array.from(overlappingNodes); - const previousOverlappingNodesIds: string[] = nodes - .filter((node: Node) => node.data.overlapping) - .map((node: Node) => node.id); - - if (!areArraysEqual(previousOverlappingNodesIds, overlappingIds)) { - setNodes((nodes: any) => { - return nodes.map((node: any) => { - const isOverlaped: boolean = overlappingIds.includes(node.id); - if (isOverlaped === node.data.overlapping) - return node; // No change needed - else - return { - ...node, - data: { - ...node.data, - overlapping: isOverlaped - } - }; - }); - }); - } - }, [overlappingNodes]); - - /** - * When `isPulsing` is active, apply a `pulsing` property to overlapping nodes - * to trigger animations or visual indicators in the UI. - */ - useEffect(() => { - setNodes((nodes: any) => { - return nodes.map((node: any) => { - if (!node.data.overlapping) - return node; - - return { - ...node, - data: { - ...node.data, - pulsing: isPulsing - } - }; - }); - }); - }, [isPulsing]); - - // True if any overlapping nodes exist - const isOverlapping: boolean = overlappingNodes.size > 0; - - // Return memoized object to avoid unnecessary rerenders - return useMemo(() => ({ - puls, - isOverlapping - }), [isOverlapping, puls]); -}; - -export default useOverlappingNodes; - -/** - * Helper function to check whether two nodes are overlapping - * Uses their position and dimensions to determine intersection. - */ -function isNodesOverlapping(nodeA: Node, nodeB: Node) { - const nodeAWidth: number = - nodeA.measured?.width || - parseInt(nodeA.style?.width as string) || - 224; // Default fallback width - - const nodeAHeight: number = - nodeA.measured?.height || - (nodeA.data.table as TableType).fields.length * 32 + 36; // Estimate height based on number of fields - - const nodeBWidth: number = - nodeB.measured?.width || - parseInt(nodeB.style?.width as string) || - 224; - - const nodeBHeight: number = - nodeB.measured?.height || - (nodeB.data.table as TableType).fields.length * 32 + 36; - - // Bounding box of node A - const a = { - left: nodeA.position.x, - right: nodeA.position.x + nodeAWidth, - top: nodeA.position.y, - bottom: nodeA.position.y + nodeAHeight, - }; - - // Bounding box of node B - const b = { - left: nodeB.position.x, - right: nodeB.position.x + nodeBWidth, - top: nodeB.position.y, - bottom: nodeB.position.y + nodeBHeight, - }; - - // Check if the bounding boxes intersect - return !(a.right <= b.left || a.left >= b.right || a.bottom <= b.top || a.top >= b.bottom); -} diff --git a/src/hooks/use-overlapping-tables.tsx b/src/hooks/use-overlapping-tables.tsx new file mode 100644 index 0000000..cb2c9d4 --- /dev/null +++ b/src/hooks/use-overlapping-tables.tsx @@ -0,0 +1,110 @@ +import { TableType } from "@/lib/schemas/table-schema"; +import { isTablesOverlapping } from "@/utils/tables"; +import { areArraysEqual } from "@/utils/utils"; +import { Node, useReactFlow } from "@xyflow/react"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +// Return type of the hook +type UseOverlapingType = { + isOverlapping: boolean, // Indicates if any tables are overlapping + puls: () => void // Triggers a short visual pulse effect for overlapping tables +} + +/** + * Custom hook to detect overlapping tables in a React Flow diagram, + * annotate them with metadata (`overlapping`, `pulsing`), + * and provide a pulse trigger for visual feedback. + */ +const useOverlappingTables = (tables: TableType[]): UseOverlapingType => { + const [isPulsing, setIsPulsing] = useState(false); + const { setNodes } = useReactFlow(); + const [overlappingTablesIds, setOverlappingTablesIds] = useState>(new Set()); + /** + * Triggers a brief "pulse" effect (e.g. for animations or styles) + * by toggling a boolean flag for a short time. + */ + const puls = useCallback(() => { + setIsPulsing(true); + setTimeout(() => setIsPulsing(false), 200); + }, []); + + /** + * Compute which tables are currently overlapping. + * This is memoized to avoid recalculating unless tables change. + */ + useEffect(() => { + const overlaps: Set = new Set(); + // Compare every pair of tables + for (let i = 0; i < tables.length; i++) { + for (let j = i + 1; j < tables.length; j++) { + if (isTablesOverlapping(tables[i], tables[j])) { + overlaps.add(tables[i].id); + overlaps.add(tables[j].id); + } + } + } + + if (!areArraysEqual(Array.from(overlaps), Array.from(overlappingTablesIds))) { + setOverlappingTablesIds(overlaps); + } + }, [tables]); + + + /** + * When overlapping tables change, update each node’s `data.overlapping` property accordingly. + * Avoids unnecessary updates using deep comparison. + */ + useEffect(() => { + const overlappingIds: string[] = Array.from(overlappingTablesIds); + + + setNodes((nodes: any) => { + return nodes.map((node: any) => { + const isOverlaped: boolean = overlappingIds.includes(node.id); + if (isOverlaped === node.data.overlapping) + return node; // No change needed + else + return { + ...node, + data: { + ...node.data, + overlapping: isOverlaped + } + }; + }); + }); + + }, [overlappingTablesIds]); + + /** + * When `isPulsing` is active, apply a `pulsing` property to overlapping tables + * to trigger animations or visual indicators in the UI. + */ + useEffect(() => { + setNodes((nodes: any) => { + return nodes.map((node: any) => { + if (!node.data.overlapping) + return node; + + return { + ...node, + data: { + ...node.data, + pulsing: isPulsing + } + }; + }); + }); + }, [isPulsing]); + + // True if any overlapping tables exist + const isOverlapping: boolean = overlappingTablesIds.size > 0; + + // Return memoized object to avoid unnecessary rerenders + return useMemo(() => ({ + puls, + isOverlapping + }), [isOverlapping, puls]); +}; + +export default useOverlappingTables; diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index f2eac51..c27e2cf 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -33,9 +33,9 @@ import { AlertTriangle } from "lucide-react"; import { adjustTablesPositions } from "@/utils/tables"; import DatabaseControlButtons from "./database-control-buttons"; import { FieldType } from "@/lib/schemas/field-schema"; -import useHighlightedEdges from "@/hooks/use-highlighted-edges"; -import useOverlappingNodes from "@/hooks/use-overlapping-nodes"; +import useHighlightedEdges from "@/hooks/use-highlighted-edges"; import { useTranslation } from "react-i18next"; +import useOverlappingTables from "@/hooks/use-overlapping-tables"; const DatabasePage: React.FC = () => { @@ -152,7 +152,7 @@ const DatabasePage: React.FC = () => { useTableToNode(tables); useRelationshipToEdge(relationships); useHighlightedEdges(nodes, relationships, edges); - const { isOverlapping, puls } = useOverlappingNodes(nodes); + const { isOverlapping, puls } = useOverlappingTables(tables); return ( 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 c80fc39..a887b70 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 @@ -15,14 +15,13 @@ interface RelationshipAccordionBodyProps { - - const RelationshipAccordionBody: React.FC = ({ relationship }) => { const [cardinality, setCardinality] = useState(new Set([relationship.cardinality])); const { editRelationship, deleteRelationship } = useDatabaseOperations(); const { t } = useTranslation(); + const changeCardinality = (keys: SharedSelection) => { @@ -68,7 +67,7 @@ const RelationshipAccordionBody: React.FC = ({ r
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 3e774ab..1d75b4c 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 @@ -21,9 +21,9 @@ const FieldItem: React.FC = ({ field }) => { const [fieldName, setFieldName] = useState(field.name); const [popOverOpen, setPopOverOpen] = useState(false); - const { data_types } = useDatabase(); - const {deleteField, editField} = useDatabaseOperations() ; - + const { data_types } = useDatabaseOperations(); + 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(); @@ -37,6 +37,7 @@ const FieldItem: React.FC = ({ field }) => { useEffect(() => { setFieldName(field.name); }, [field.name]); + useEffect(() => { setSelectedType(field.typeId as string | undefined); }, [field.typeId]) @@ -87,6 +88,7 @@ const FieldItem: React.FC = ({ field }) => { } as FieldType); } + return (
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 147d4e5..21b792f 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 @@ -7,26 +7,28 @@ import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { FieldType } from "@/lib/schemas/field-schema"; import { TableType } from "@/lib/schemas/table-schema"; -import { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider"; import { v4 } from "uuid"; import { getNextSequence } from "@/utils/field"; - +import hash from "object-hash" ; interface Props { - table: TableType + tableFields: FieldType[] ; + tableId : string ; } -const FieldList: React.FC = ({ table }) => { +const FieldList: React.FC = ({ tableFields , tableId}) => { const { t } = useTranslation(); - const [fields, setFields] = useState(table.fields); + const [fields, setFields] = useState(tableFields); const { createField, orderTableFields } = useDatabaseOperations(); useEffect(() => { - setFields(table.fields) - }, [table.fields]) + + setFields(tableFields) + }, [tableFields]) const sensors = useSensors( useSensor(PointerSensor) @@ -53,14 +55,12 @@ const FieldList: React.FC = ({ table }) => { const addField = () => { createField({ id: v4(), - name: `field_${table.fields.length + 1}`, - tableId: table.id, + name: `field_${fields.length + 1}`, + tableId: tableId, sequence: getNextSequence(fields) , nullable: true, }) - } - - + } return (
= ({ table }) => { } -export default FieldList; \ No newline at end of file +export default React.memo(FieldList , (prevState , newState) => { + return hash(prevState) == hash(newState) ; +}); \ No newline at end of file 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 c622d7a..1eba325 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 @@ -2,7 +2,7 @@ import { Accordion, AccordionItem, Button, cn, Textarea } from "@heroui/react"; import { ChevronLeft, FileKey, FileType, Key, MessageSquareQuote, Plus } from "lucide-react"; -import { MouseEventHandler, useCallback, useEffect, useState } from "react"; +import React, { MouseEventHandler, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import ColorPicker from "@/components/color-picker/color-picker"; import FieldList from "./field/field-list"; @@ -25,7 +25,6 @@ const TableAccordionBody: React.FC = ({ table }) => { const { editTable, createField } = useDatabaseOperations(); const onColorChange = useCallback((color: string | undefined) => { - editTable({ id: table.id, color: color ? color : null } as TableType); }, [table]); @@ -51,7 +50,11 @@ const TableAccordionBody: React.FC = ({ table }) => { useEffect(() => { setNote(table.note ? table.note : ""); - }, [table.note]) + }, [table.note]) ; + + + + return (
= ({ table }) => {
} > - + ( @@ -178,4 +181,4 @@ const TableAccordionBody: React.FC = ({ table }) => { } -export default TableAccordionBody; \ No newline at end of file +export default React.memo(TableAccordionBody); \ No newline at end of file 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 a851676..ffc6262 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 @@ -2,7 +2,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/to import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react"; import { Check, ChevronRight, Copy, EllipsisVertical, FileKey, FileType, Focus, Pencil, Trash } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { TableInsertType, TableType } from "@/lib/schemas/table-schema"; @@ -10,6 +10,7 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr import { v4 } from "uuid"; import { getNextSequence } from "@/utils/field"; import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider"; +import hash from "object-hash"; export interface TableAccordionHeaderProps { table: TableType, @@ -52,6 +53,10 @@ const TableAccordionHeader: React.FC = ({ table, isOp }) } + + + + return (
= ({ table, isOp } -export default TableAccordionHeader; \ No newline at end of file +export default React.memo(TableAccordionHeader, (prevState, newState) => { + return hash(prevState) == hash(newState); +}); \ No newline at end of file diff --git a/src/pages/database/table/table.tsx b/src/pages/database/table/table.tsx index 44f4779..f3cbc78 100644 --- a/src/pages/database/table/table.tsx +++ b/src/pages/database/table/table.tsx @@ -69,10 +69,7 @@ const Table: React.FC> = ({ selected, data: { table, overl }) }, [table.fields, selected, highlightedEdges]); - - return ( - > = ({ selected, data: { table, overl overlapping ? 'ring-2 dark:ring-offset-default-900 ring-danger ring-offset-1 scale-105 shadow-danger ' : '', - !pulsing - ? 'scale-100' - : '', - pulsing + !pulsing && overlapping ? 'scale-105' : '', + pulsing && overlapping + ? 'scale-110' + : '', )} shadow="sm" diff --git a/src/providers/database-provider/database-context.ts b/src/providers/database-provider/database-context.ts index f882403..b2e5493 100644 --- a/src/providers/database-provider/database-context.ts +++ b/src/providers/database-provider/database-context.ts @@ -13,15 +13,15 @@ import { createContext } from "react"; interface DatabaseDataContextType { - data_types: DataType[], database: DatabaseType, - isLoading: boolean, + isLoading: boolean, getField: (tableId: string, id: string) => FieldType | undefined, } interface DatabaseOperationsContextType { + data_types: DataType[], createTable: (table: TableInsertType) => Promise, editTable: (table: TableInsertType) => Promise, diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index b66e3fd..a3bcc35 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -233,6 +233,7 @@ const DatabaseProvider: React.FC = ({ children }) => { deleteRelationship, deleteMultiRelationships, executeDbDiffOps, + data_types }), [ createTable, editTable, @@ -248,11 +249,12 @@ const DatabaseProvider: React.FC = ({ children }) => { deleteRelationship, deleteMultiRelationships, executeDbDiffOps, + data_types ]); return (