split the database context into 2 data context and operatoins context to redeuce re-rendering

This commit is contained in:
KarimTamani
2025-05-21 19:53:33 +01:00
parent e0c31e0cb6
commit 2d5a082210
22 changed files with 481 additions and 226 deletions
+4 -2
View File
@@ -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 (
<NextUIProvider>
<HeroUIProvider>
<ToastProvider placement="bottom-right"/>
<NextThemesProvider
defaultTheme='system'
attribute='class'
@@ -35,7 +37,7 @@ function App() {
</ReactFlowProvider>
</SyncProvider>
</NextThemesProvider>
</NextUIProvider>
</HeroUIProvider>
);
}
+59
View File
@@ -0,0 +1,59 @@
import { TableType } from "@/lib/schemas/table-schema";
import { Node, useReactFlow } from "@xyflow/react";
import { useMemo } from "react";
const useGetOverlappingNodes = (): Set<string> => {
const { getNodes } = useReactFlow();
const nodes = getNodes();
const overlappingNodes: Set<string> = useMemo(() => {
const overlaps: Set<string> = 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;
+24
View File
@@ -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<Edge[]>([]);
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;
+20 -6
View File
@@ -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])
}
+23 -5
View File
@@ -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])
}
@@ -52,7 +52,7 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
}, []);
return (
<Navbar className="flex rounded-md border-1 border-default-200 bg-transparent dark:border-default-800" isBlurred
<Navbar className="flex rounded-md border-1 border-default-200 bg-transparent dark:border-default-800 " isBlurred
classNames={{
wrapper: "h-14 p-2 gap-1",
}}
+148 -48
View File
@@ -6,7 +6,7 @@ import '@xyflow/react/dist/style.css';
import Relationship from "./table/relationship";
import DBController from "./db-controller/db-controller";
import { TableInsertType } from "@/lib/schemas/table-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { useDatabase, useDatabaseOperations } 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";
@@ -16,42 +16,60 @@ import CardinalityMarker from "@/components/cardinality-marker/cardinality-marke
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 { addToast, Button, Image } from "@heroui/react";
import { AlertTriangle, LayoutGrid } from "lucide-react";
import { adjustTablesPositions } from "@/utils/tables";
import { useTheme } from "next-themes";
import DatabaseControlButtons from "./database-control-buttons";
import useGetOverlappingNodes from "@/hooks/use-get-overlapping-nodes";
import { FieldType } from "@/lib/schemas/field-schema";
const DatabasePage: React.FC<never> = () => {
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<string[]>([]);
const { tables, relationships } = database ;
const [isTableOverlappingPulsing, setIsTableOverlappingPulsing] = useState<boolean>(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<never> = useCallback((changes: NodeChange<never>[]) => {
@@ -124,6 +142,7 @@ const DatabasePage: React.FC<never> = () => {
useTableToNode(tables);
useRelationshipToEdge(relationships);
const adjustPositions = useCallback(async () => {
updateTablePositions(await adjustTablesPositions(nodes, relationships));
setTimeout(() => {
@@ -133,46 +152,127 @@ const DatabasePage: React.FC<never> = () => {
}, 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 (
<div className="w-full h-screen flex relative">
<DBController />
<ReactFlow
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default"
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
}}
// onlyRenderVisibleElements
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<Controls
position="bottom-center"
showFitView={false}
showZoom={false}
showInteractive={false}
className="shadow-none "
<DBController />
<div className="relative w-full h-full">
<ReactFlow
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default"
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
}}
//onlyRenderVisibleElements
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
<Controls
position="bottom-center"
showFitView={false}
showZoom={false}
showInteractive={false}
className="shadow-none "
>
</Controls >
<Background />
</ReactFlow>
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<Background />
</ReactFlow>
<div
className="absolute left-[12px] top-[64px] "
>
{
overlappingNodes.size > 0 &&
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="shadow"
size="sm"
isIconOnly
color="danger"
className="size-8 p-1 "
onPressEnd={pulsOverlappingTables}
>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
Overlapping Tables
</TooltipContent>
</Tooltip>
}
</div>
</div>
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
@@ -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<RelationshipAccordionBodyProps> = ({ relationship }) => {
const [cardinality, setCardinality] = useState(new Set([relationship.cardinality]));
const { editRelationship, deleteRelationship } = useDatabase();
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
@@ -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<RelationshipAccordionHeaderProps> =
const [editMode, setEditMode] = useState<boolean>(false);
const { name: defaultName } = useRelationshipName(relationship);
const { editRelationship, deleteRelationship } = useDatabase();
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
@@ -40,6 +40,11 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
deleteRelationship(relationship.id);
setPopOverOpen(false);
}
useEffect(() => {
setName(relationship.name ? relationship.name : defaultName) ;
} , [relationship.name])
return (
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
<div className={cn(
@@ -137,7 +142,6 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
</>
}
</div>
)
}
@@ -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<Props> = ({ }) => {
const [relationship, setRelationship] = useState<RelationshipInsertType | undefined>(undefined);
const [isValid, setIsValid] = useState<boolean>(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<Props> = ({ }) => {
createRelationship({
id: newRelationshipId,
...relationship,
...relationship,
} as RelationshipInsertType);
setSelectedRelationship(new Set([newRelationshipId]) as any);
@@ -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<Props> = ({ field }) => {
const [fieldName, setFieldName] = useState<string>(field.name);
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const { deleteField, editField, data_types } = useDatabase();
const { data_types } = useDatabase();
const {deleteField, editField} = useDatabaseOperations() ;
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
const { t } = useTranslation();
@@ -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<Props> = ({ table }) => {
const { t } = useTranslation();
const [fields, setFields] = useState<FieldType[]>(table.fields);
const { createField, orderTableFields } = useDatabase();
const { createField, orderTableFields } = useDatabaseOperations();
useEffect(() => {
setFields(table.fields)
@@ -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<TableAccordionBodyProps> = ({ table }) => {
const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"]));
const [note, setNote] = useState<string>(table.note ? table.note : "");
const { t } = useTranslation();
const { editTable, createField } = useDatabase();
const { editTable, createField } = useDatabaseOperations();
const onColorChange = useCallback((color: string | undefined) => {
@@ -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<TableAccordionHeaderProps> = ({ table, isOpen }) => {
const { editTable, deleteTable, createField } = useDatabase();
const { editTable, deleteTable, createField } = useDatabaseOperations();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
@@ -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<Props> = ({ }) => {
const { database, createTable } = useDatabase();
const { database } = useDatabase();
const {createTable} = useDatabaseOperations() ;
const { tables } = database ;
const { t } = useTranslation();
const [selectedTable, setSelectedTable] = useState(new Set([]));
+10 -13
View File
@@ -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<Props> = ({ field, showHandles, highlight }) => {
const [editMode, setEditMode] = useState<boolean>(false);
const { deleteField, editField } = useDatabase();
const { deleteField, editField } = useDatabaseOperations();
const [fieldName, setFieldName] = useState<string>(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<Props> = ({ field, showHandles, highlight }) => {
setEditMode(false);
}
// console.log("render field ", field.name)
return (
<div className={cn(
"group relative flex h-8 items-center justify-between gap-1 border-t border-default dark:border-default/5 px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-primary-500 transition-all duration-200 ease-in-out" ,
"group relative flex h-8 items-center justify-between gap-1 border-t border-default dark:border-default/5 px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-primary-500 transition-all duration-200 ease-in-out",
highlight ? "bg-primary/5" : ""
)}>
{
@@ -125,12 +122,12 @@ const Field: React.FC<Props> = ({ 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"
/>
<Handle
type="source"
position={Position.Right}
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"
id={RIGHT_PREFIX + field.id}
/>
</div>
@@ -113,6 +113,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
}
}, [data?.relationship.cardinality, selected]);
// console.log ("render relationship " , props.data?.relationship.sourceTable.name , props.data?.relationship.targetTable.name)
return (
<>
+96 -80
View File
@@ -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<NodeProps<TableProps>> = ({ selected, data: { table } }) => {
const Table: React.FC<NodeProps<TableProps>> = (props) => {
const { selected, data: { table, overlapping = false, pulsing = false } } = props;
const [editMode, setEditMode] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(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<NodeProps<TableProps>> = ({ 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<NodeProps<TableProps>> = ({ selected, data: { table } }) =
}, [table.fields, selected, highlightedEdges]);
useEffect(() => {
console.log("re-render ", table.name)
}, [useDatabase]) ;
return (
<Card className={cn(
"w-full h-full bg-background rounded-lg entity-card noselect overflow-visible dark:bg-default-900",
selected
? 'ring-2 ring-primary'
: '',
)}
shadow="sm"
>
<div className="px-[2px] ">
<div
className=" border-t-[4px] rounded-t-[6px] border-primary"
style={{ borderColor: table.color as string }}
></div>
</div>
<div className="group gap-2 flex h-9 items-center justify-between bg-default/50 px-2 dark:bg-black/20">
<Table2 className="size-4 shrink-0 text-icon dark:text-white" />
{
editMode && <>
<input
placeholder={table.name}
autoFocus
onChange={(event: any) => 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"
/>
<Card className={cn(
"w-full h-full bg-background rounded-lg noselect overflow-visible dark:bg-default-900",
selected
? 'ring-2 ring-primary'
: '',
overlapping
? 'ring-2 dark:ring-offset-default-900 ring-danger ring-offset-1 scale-105 shadow-danger '
: '',
!pulsing
? 'scale-100'
: '',
pulsing
? 'scale-105'
: '',
)}
shadow="sm"
>
<div className="px-[2px] ">
<div
className=" border-t-[4px] rounded-t-[6px] border-primary"
style={{ borderColor: table.color as string }}
></div>
</div>
<div className="group gap-2 flex h-9 items-center justify-between bg-default/50 px-2 dark:bg-black/20">
<Table2 className="size-4 shrink-0 text-icon dark:text-white" />
{
editMode && <>
<input
placeholder={table.name}
autoFocus
onChange={(event: any) => 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"
/>
<Button
variant="light"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-white"
size="sm"
onPressEnd={saveTableName}
isIconOnly
>
<Check className="size-3" />
</Button>
</>
}
{
!editMode &&
<>
<Tooltip>
<TooltipTrigger asChild>
<label
className=" w-full text-editable truncate px-2 py-0.5 text-sm font-bold dark:text-white dark:group-hover:bg-default-900"
onDoubleClick={() => setEditMode(true)}
>
{tableName}
</label>
</TooltipTrigger>
<TooltipContent className="dark:bg-default-900">
{t("table.double_click")}
</TooltipContent>
</Tooltip>
<div className="hidden shrink-0 flex-row group-hover:flex">
<Button
variant="light"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-white"
size="sm"
onPressEnd={saveTableName}
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:hover:bg-default-800 "
isIconOnly
onPressEnd={focus}
>
<Check className="size-3" />
<Focus className="size-4 text-icon dark:text-white" />
</Button>
</>
}
{
!editMode &&
<>
<Tooltip>
<TooltipTrigger asChild>
<label
className=" w-full text-editable truncate px-2 py-0.5 text-sm font-bold dark:text-white dark:group-hover:bg-default-900"
onDoubleClick={() => setEditMode(true)}
>
{tableName}
</label>
</TooltipTrigger>
<TooltipContent className="dark:bg-default-900">
{t("table.double_click")}
</TooltipContent>
</Tooltip>
</div>
</>
}
</div>
<div className="transition-[max-height] duration-200 ease-in-out">
{fields}
</div>
</Card>
<div className="hidden shrink-0 flex-row group-hover:flex">
<Button
variant="light"
size="sm"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:hover:bg-default-800 "
isIconOnly
onPressEnd={focus}
>
<Focus className="size-4 text-icon dark:text-white" />
</Button>
</div>
</>
}
</div>
<div className="transition-[max-height] duration-200 ease-in-out">
{fields}
</div>
</Card>
)
};
export default React.memo(Table)
@@ -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<Props> = ({ 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<DatabaseType>(database);
useEffect(() => {
@@ -21,8 +23,6 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const databaseHash: string = hash(database, { algorithm: 'sha1' });
if (presentHash != databaseHash)
set(database);
}, [database]);
@@ -47,15 +47,11 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const differences = compare(normalizedDatabase, normalizedPresent);
if (differences && differences.length > 0) {
const operations: DBDiffOperation[] = mapDiffToDBDiffOperation(differences);
executeDbDiffOps(operations)
}
}, [datatbaseState.present]);
return (
<DatabaseHistoryContext.Provider
value={{
@@ -11,34 +11,40 @@ import { createContext } from "react";
export interface DatabaseContextType {
interface DatabaseDataContextType {
data_types: DataType[],
database : DatabaseType ,
isLoading : boolean ,
database: DatabaseType,
isLoading: boolean,
// table operations
}
interface DatabaseOperationsContextType {
createTable: (table: TableInsertType) => Promise<void>,
editTable: (table: TableInsertType) => Promise<QueryResult>,
deleteTable: (id: string) => Promise<void>,
updateTablePositions: (tables: TableInsertType[]) => Promise<QueryResult> ,
updateTablePositions: (tables: TableInsertType[]) => Promise<QueryResult>,
deleteMultiTables: (ids: string[]) => Promise<QueryResult>
// field operations
createField: (field: FieldInsertType) => Promise<QueryResult>,
editField: (field: FieldInsertType) => Promise<QueryResult>,
deleteField: (id: string) => Promise<void>,
orderTableFields: (fields: FieldType[]) => Promise<QueryResult>,
getField: (tableId: string, id: string) => FieldType | undefined,
// relationship operations
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
deleteRelationship: (id: string) => Promise<QueryResult>,
deleteMultiRelationships: (ids: string[]) => Promise<QueryResult> ,
executeDbDiffOps : ( operations : DBDiffOperation[]) => void ,
deleteMultiRelationships: (ids: string[]) => Promise<QueryResult>,
// execute the diff operation whenver user click in undo or redo
executeDbDiffOps: (operations: DBDiffOperation[]) => void,
}
export const DatabaseDataContext = createContext<DatabaseDataContextType>({} as DatabaseDataContextType);
export const DatabaseOperationsContext = createContext<DatabaseOperationsContextType>({} as DatabaseOperationsContextType);
export default createContext<DatabaseContextType>({} as DatabaseContextType);
@@ -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<Props> = ({ 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<QueryResult> => {
const caseStatements = fieldsList
.map((field, index) => `WHEN '${field.id}' THEN ${index}`)
@@ -188,7 +195,7 @@ const DatabaseProvider: React.FC<Props> = ({ 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<Props> = ({ children }) => {
}
})
}, [db, currentDatabaseId])
}, [db, currentDatabaseId]);
return (
<DatabaseContext.Provider value={{
createTable,
editTable,
deleteTable,
updateTablePositions,
deleteMultiTables,
createField,
editField,
deleteField,
orderTableFields,
createRelationship,
editRelationship,
deleteRelationship,
deleteMultiRelationships,
<DatabaseDataContext.Provider value={{
data_types,
database: database as unknown as DatabaseType,
isLoading,
executeDbDiffOps
}}>
{
!isLoading && database &&
<DatabaseHistoryProvider>
{children}
</DatabaseHistoryProvider>
}
</DatabaseContext.Provider>
<DatabaseOperationsContext.Provider value={{
createTable,
editTable,
deleteTable,
updateTablePositions,
deleteMultiTables,
createField,
editField,
deleteField,
orderTableFields,
getField,
createRelationship,
editRelationship,
deleteRelationship,
deleteMultiRelationships,
executeDbDiffOps,
}}>
{
!isLoading && database &&
<DatabaseHistoryProvider>
{children}
</DatabaseHistoryProvider>
}
</DatabaseOperationsContext.Provider>
</DatabaseDataContext.Provider>
)
}
export const useDatabase = () => useContext(DatabaseContext);
export const useDatabase = () => useContext(DatabaseDataContext);
export const useDatabaseOperations = () => useContext(DatabaseOperationsContext);
export default DatabaseProvider;
+1 -1
View File
@@ -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",