diff --git a/src/hooks/use-table-to-node.tsx b/src/hooks/use-table-to-node.tsx index b4a06e9..d584291 100644 --- a/src/hooks/use-table-to-node.tsx +++ b/src/hooks/use-table-to-node.tsx @@ -1,8 +1,7 @@ import { TableType } from "@/lib/schemas/table-schema"; import { Node, useReactFlow } from "@xyflow/react"; -import { useEffect } from "react"; -import hash from 'object-hash'; +import { useEffect } from "react"; import { getDefaultTableOverlapping } from "@/utils/tables"; export const useTableToNode = (tables: TableType[]): void => { const { setNodes } = useReactFlow(); @@ -10,20 +9,19 @@ export const useTableToNode = (tables: TableType[]): void => { useEffect(() => { - const tableNodes = tables.map((table: TableType) => { + const nodes = tables.map((table: TableType) => { return { id: table.id, type: "table", - position: { x: table.posX, y: table.posY }, data: { table, - overlapping : getDefaultTableOverlapping(table , tables) , - pulsing : false , - highlightedEdges : [] + overlapping: getDefaultTableOverlapping(table, tables), + pulsing: false, + highlightedEdges: [] }, style: { width: 224 @@ -31,7 +29,7 @@ export const useTableToNode = (tables: TableType[]): void => { } as Node }) - setNodes (tableNodes) ; + setNodes(nodes); }, [tables]) } \ No newline at end of file diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index 85333e5..f35fc50 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -38,10 +38,15 @@ const DatabasePage: React.FC = () => { const [isTableOverlappingPulsing, setIsTableOverlappingPulsing] = useState(false); const { tables, relationships } = database; const { fitView } = useReactFlow(); - const nodeTypes = useMemo(() => ({ table: Table }), []); const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []); + + + useTableToNode(tables); + useRelationshipToEdge(relationships); + + 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, ""); @@ -72,7 +77,8 @@ const DatabasePage: React.FC = () => { }, [database]); - const handleNodesChanges: OnNodesChange = useCallback((changes: NodeChange[]) => { + const handleNodesChanges: OnNodesChange = useCallback(async (changes: NodeChange[]) => { + const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) => change.type == "position" && @@ -82,7 +88,7 @@ const DatabasePage: React.FC = () => { const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove"); if (nodePositionChanges.length > 0) - updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({ + await updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({ id: change.id, posX: change.position?.x, posY: change.position?.y @@ -95,6 +101,8 @@ const DatabasePage: React.FC = () => { }, [onNodesChange]); + + const handleEdgeChanges: OnEdgesChange = useCallback((changes: EdgeChange[]) => { const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[]; @@ -158,8 +166,7 @@ const DatabasePage: React.FC = () => { - useTableToNode(tables); - useRelationshipToEdge(relationships); + const adjustPositions = useCallback(async () => { @@ -168,7 +175,7 @@ const DatabasePage: React.FC = () => { fitView({ duration: 500 }) - }, 500) + }, 300) }, [relationships, nodes]); @@ -239,7 +246,7 @@ const DatabasePage: React.FC = () => { defaultEdgeOptions={{ type: 'relationship-edge', }} - //onlyRenderVisibleElements + onlyRenderVisibleElements panOnDrag={true} zoomOnScroll={true} nodeTypes={nodeTypes} @@ -262,7 +269,7 @@ const DatabasePage: React.FC = () => { /> - +
= ({ children }) => { const udpateDbFlag = useRef(false); const { database } = useDatabase(); - const { executeDbDiffOps} = useDatabaseOperations() ; - + const { executeDbDiffOps } = useDatabaseOperations(); + const [isProcessing, setIsProcessing] = useState(false); + const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo(database); useEffect(() => { @@ -27,28 +28,44 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { const undo = useCallback(() => { - udpateDbFlag.current = true; - undoChanges(); - }, [undoChanges, udpateDbFlag]); + if (!isProcessing) { + udpateDbFlag.current = true; + undoChanges(); + } + }, [undoChanges, udpateDbFlag, isProcessing]); const redo = useCallback(() => { - udpateDbFlag.current = true; - redoChanges(); - }, [redoChanges, udpateDbFlag]); + if (!isProcessing) { + udpateDbFlag.current = true; + redoChanges(); + } + }, [redoChanges, udpateDbFlag, isProcessing]); useEffect(() => { if (!udpateDbFlag.current) { udpateDbFlag.current = true; return; } - const normalizedDatabase = normalizeDatabase(database); const normalizedPresent = normalizeDatabase(datatbaseState.present); const differences = compare(normalizedDatabase, normalizedPresent); if (differences && differences.length > 0) { + setIsProcessing(true); + const operations: DBDiffOperation[] = mapDiffToDBDiffOperation(differences); - executeDbDiffOps(operations) + + (async () => { + try { + await executeDbDiffOps(operations) + setIsProcessing(false); + } + catch (error) { + set(database); + setIsProcessing(false); + } + })() + } }, [datatbaseState.present]); @@ -59,6 +76,7 @@ const DatabaseHistoryProvider: React.FC = ({ children }) => { redo, canUndo, canRedo, + isProcessing, present: datatbaseState.present }} > diff --git a/src/providers/database-provider/database-provider.tsx b/src/providers/database-provider/database-provider.tsx index 89abd9b..0c59f60 100644 --- a/src/providers/database-provider/database-provider.tsx +++ b/src/providers/database-provider/database-provider.tsx @@ -14,6 +14,7 @@ import { DBDiffOperation } from "@/utils/database"; import { DatabaseType } from "@/lib/schemas/database-schema"; import { getTimestamp } from "@/utils/utils"; + interface Props { children: React.ReactNode } const DatabaseProvider: React.FC = ({ children }) => { @@ -121,6 +122,7 @@ const DatabaseProvider: React.FC = ({ children }) => { }, [database]) const orderTableFields = useCallback(async (fieldsList: FieldType[]): Promise => { + const caseStatements = fieldsList .map((field, index) => `WHEN '${field.id}' THEN ${index}`) .join('\n '); @@ -134,6 +136,7 @@ const DatabaseProvider: React.FC = ({ children }) => { ${ids} );`; return await powerSyncDb.execute(sql); + }, [powerSyncDb]); const createRelationship = useCallback(async (relationship: RelationshipInsertType): Promise => { @@ -160,6 +163,7 @@ const DatabaseProvider: React.FC = ({ children }) => { }, [db]); const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise => { + const posXCases = tableList .map(table => `WHEN '${table.id}' THEN ${table.posX}`) .join('\n '); @@ -180,48 +184,52 @@ const DatabaseProvider: React.FC = ({ children }) => { ${ids} );`; return await powerSyncDb.execute(sql); + }, [powerSyncDb]); const executeDbDiffOps = useCallback(async (operations: DBDiffOperation[]) => { + try { + await db.transaction(async (tx) => { - await db.transaction(async (tx) => { + for (const operation of operations) { - for (const operation of operations) { + if (operation.type == "CREATE_TABLE") { + await tx.insert(tables).values(operation.table); - if (operation.type == "CREATE_TABLE") { - await tx.insert(tables).values(operation.table); + if (operation.table.fields && Object.values(operation.table.fields).length > 0) + await tx.insert(fields).values(Object.values(operation.table.fields)); + } + else if (operation.type === "UPDATE_TABLE") { - if (operation.table.fields && Object.values(operation.table.fields).length > 0) - await tx.insert(fields).values(Object.values(operation.table.fields)); + await tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId)); + + } else if (operation.type === "DELETE_TABLE") { + await tx.delete(tables).where(eq(tables.id, operation.tableId)); + + } else if (operation.type === "CREATE_FIELD") { + await tx.insert(fields).values(operation.field); + + } else if (operation.type === "DELETE_FIELD") { + await tx.delete(relationships).where(or(eq(relationships.sourceFieldId, operation.fieldId), eq(relationships.targetFieldId, operation.fieldId))); + await tx.delete(fields).where(eq(fields.id, operation.fieldId)); + + } else if (operation.type === "UPDATE_FIELD") { + await tx.update(fields).set(operation.changes).where(eq(fields.id, operation.fieldId)); + + } else if (operation.type === "CREATE_RELATIONSHIP") { + await tx.insert(relationships).values(operation.relationship); + + } else if (operation.type === "DELETE_RELATIONSHIP") { + await tx.delete(relationships).where(eq(relationships.id, operation.relationshipId)); + + } else if (operation.type === "UPDATE_RELATIONSHIP") { + await tx.update(relationships).set(operation.changes).where(eq(relationships.id, operation.relationshipId)); + } } - 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") { - await tx.delete(tables).where(eq(tables.id, operation.tableId)); - - } else if (operation.type === "CREATE_FIELD") { - await tx.insert(fields).values(operation.field); - - } else if (operation.type === "DELETE_FIELD") { - await tx.delete(relationships).where(or(eq(relationships.sourceFieldId, operation.fieldId), eq(relationships.targetFieldId, operation.fieldId))); - await tx.delete(fields).where(eq(fields.id, operation.fieldId)); - - } else if (operation.type === "UPDATE_FIELD") { - await tx.update(fields).set(operation.changes).where(eq(fields.id, operation.fieldId)); - - } else if (operation.type === "CREATE_RELATIONSHIP") { - await tx.insert(relationships).values(operation.relationship); - - } else if (operation.type === "DELETE_RELATIONSHIP") { - await tx.delete(relationships).where(eq(relationships.id, operation.relationshipId)); - - } else if (operation.type === "UPDATE_RELATIONSHIP") { - await tx.update(relationships).set(operation.changes).where(eq(relationships.id, operation.relationshipId)); - } - } - }) + }) + } catch (error) { + throw error + } }, [db]); diff --git a/src/styles/globals.css b/src/styles/globals.css index 03545e2..4f1c4bc 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,89 +1,119 @@ .react-flow__attribution { - display: none; + display: none; } .text-editable { - @apply dark:group-hover:bg-default-50 group-hover:bg-slate-100 group-hover:ring-[0.5px] group-hover:ring-primary-700 dark:group-hover:ring-white rounded-md cursor-pointer ; + @apply dark:group-hover:bg-default-50 group-hover:bg-slate-100 group-hover:ring-[0.5px] group-hover:ring-primary-700 dark:group-hover:ring-white rounded-md cursor-pointer; } .react-flow__node.selected { - transition: box-shadow 0.3s ease; - /* Add transition */ - /* box-shadow: 0px 0px 15px 0px hsl(var(--heroui-secondary-50)), 0px 2px 30px 0px hsl(var(--heroui-secondary-50)), 0px 0px 1px 0px hsl(var(--heroui-secondary-50)); + transition: box-shadow 0.3s ease; + /* Add transition */ + /* box-shadow: 0px 0px 15px 0px hsl(var(--heroui-secondary-50)), 0px 2px 30px 0px hsl(var(--heroui-secondary-50)), 0px 0px 1px 0px hsl(var(--heroui-secondary-50)); */ } + .noselect { - user-select: none; - -webkit-user-select: none; - /* Safari */ - -moz-user-select: none; - /* Firefox */ - -ms-user-select: none; - /* IE10+/Edge */ + user-select: none; + -webkit-user-select: none; + /* Safari */ + -moz-user-select: none; + /* Firefox */ + -ms-user-select: none; + /* IE10+/Edge */ } +html, +body { + user-select: none; + -webkit-user-select: none; + /* Safari */ + -moz-user-select: none; + /* Firefox */ + -ms-user-select: none; + +} .react-flow__renderer { - cursor: default !important; /* or pointer, grab, etc. */ + cursor: default !important; + /* or pointer, grab, etc. */ } .sidebar-item[data-active="true"] svg { - color : hsl(var(--heroui-primary-900)); + color: hsl(var(--heroui-primary-900)); } div[data-slot="input-wrapper"] { - outline : none ; - border-width: 1px; - + outline: none; + border-width: 1px; + } div[data-slot="mainWrapper"] button[data-slot="trigger"] { - - outline : none ; - border-width: 1px; + + outline: none; + border-width: 1px; } -div[data-slot="content"] hr[role="separator"] { - display: none ; +div[data-slot="content"] hr[role="separator"] { + display: none; } - + @keyframes dash { to { stroke-dashoffset: -10; } } - + /* Target the scrollbar */ ::-webkit-scrollbar { - width: 4px; /* Width of the scrollbar */ - + width: 4px; + /* Width of the scrollbar */ + } /* Track (background of the scrollbar) */ - + /* Handle (the scroll thumb) */ ::-webkit-scrollbar-thumb { - background: hsl(var(--heroui-default-100)); /* Darker gray */ - border-radius: 6px; /* Rounded corners */ - + background: hsl(var(--heroui-default-100)); + /* Darker gray */ + border-radius: 6px; + /* Rounded corners */ + } - + /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { - background: hsl(var(--heroui-default-300)); /* Darker on hover */ + background: hsl(var(--heroui-default-300)); + /* Darker on hover */ } + + + +.react-flow__node { + cursor: grab !important; +} + +.react-flow__node:active { + cursor: grabbing !important; +} + +.react-flow__pane { + cursor: default !important; +} \ No newline at end of file diff --git a/src/utils/stackrender-connector.ts b/src/utils/stackrender-connector.ts index 5fb838a..231b227 100644 --- a/src/utils/stackrender-connector.ts +++ b/src/utils/stackrender-connector.ts @@ -1,6 +1,6 @@ import { v4 as uuid } from 'uuid'; -import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, UpdateType } from '@powersync/web'; +import { AbstractPowerSyncDatabase, PowerSyncBackendConnector, UpdateType } from '@powersync/web'; export type DemoConfig = { backendUrl: string; @@ -67,9 +67,9 @@ export class StackRenderConnector implements PowerSyncBackendConnector { let batch: any[] = []; for (let operation of transaction.crud) { - if (operation.op != UpdateType.DELETE && Object.keys(operation.opData as any).length == 0) + if (operation.op != UpdateType.DELETE && Object.keys(operation.opData as any).length == 0) continue - + let payload = { op: operation.op, table: operation.table, @@ -92,6 +92,7 @@ export class StackRenderConnector implements PowerSyncBackendConnector { throw new Error(`Received ${response.status} from /api/data: ${await response.text()}`); } } + await transaction.complete( import.meta.env.VITE_CHECKPOINT_MODE == CheckpointMode.CUSTOM ? await this.getCheckpoint(this._clientId) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 98b7bd5..e062430 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,10 +1,10 @@ const areArraysEqual = (a: string[], b: string[]): boolean => { - if (a.length !== b.length) return false; + if (a.length !== b.length) return false; - const sortedA = [...a].sort(); - const sortedB = [...b].sort(); + const sortedA = [...a].sort(); + const sortedB = [...b].sort(); - return sortedA.every((val, index) => val === sortedB[index]); + return sortedA.every((val, index) => val === sortedB[index]); }; @@ -52,9 +52,11 @@ function excludeFields( } + + export { - areArraysEqual , - getTimestamp , + areArraysEqual, + getTimestamp, excludeFields } \ No newline at end of file