Fix Undo/Redo craches whenever the user click on shortcuts very fast

This commit is contained in:
KarimTamani
2025-05-23 22:58:39 +01:00
parent 54b724921e
commit 94ff6ddf33
8 changed files with 168 additions and 103 deletions
+6 -8
View File
@@ -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])
}
+15 -8
View File
@@ -38,10 +38,15 @@ const DatabasePage: React.FC<never> = () => {
const [isTableOverlappingPulsing, setIsTableOverlappingPulsing] = useState<boolean>(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<never> = () => {
}, [database]);
const handleNodesChanges: OnNodesChange<never> = useCallback((changes: NodeChange<never>[]) => {
const handleNodesChanges: OnNodesChange<never> = useCallback(async (changes: NodeChange<never>[]) => {
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) =>
change.type == "position" &&
@@ -82,7 +88,7 @@ const DatabasePage: React.FC<never> = () => {
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<never> = () => {
}, [onNodesChange]);
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
@@ -158,8 +166,7 @@ const DatabasePage: React.FC<never> = () => {
useTableToNode(tables);
useRelationshipToEdge(relationships);
const adjustPositions = useCallback(async () => {
@@ -168,7 +175,7 @@ const DatabasePage: React.FC<never> = () => {
fitView({
duration: 500
})
}, 500)
}, 300)
}, [relationships, nodes]);
@@ -239,7 +246,7 @@ const DatabasePage: React.FC<never> = () => {
defaultEdgeOptions={{
type: 'relationship-edge',
}}
//onlyRenderVisibleElements
onlyRenderVisibleElements
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
@@ -262,7 +269,7 @@ const DatabasePage: React.FC<never> = () => {
/>
</Controls >
<Background className="bg-default/40 dark:bg-black"/>
<Background className="bg-default/40 dark:bg-black" />
</ReactFlow>
<div
className="absolute left-[12px] top-[64px] "
@@ -12,6 +12,7 @@ export interface DatabaseHistoryContextType {
canUndo : boolean ;
canRedo : boolean ;
isProcessing : boolean ;
present : DatabaseType ;
@@ -1,11 +1,11 @@
import { useCallback, useContext, useEffect, useRef } from "react";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import DatabaseHistoryContext from "./database-history-context";
import useUndo from 'use-undo';
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';
import { DatabaseType } from "@/lib/schemas/database-schema";
import { DatabaseType } from "@/lib/schemas/database-schema";
interface Props { children: React.ReactNode };
@@ -13,8 +13,9 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const udpateDbFlag = useRef(false);
const { database } = useDatabase();
const { executeDbDiffOps} = useDatabaseOperations() ;
const { executeDbDiffOps } = useDatabaseOperations();
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo<DatabaseType>(database);
useEffect(() => {
@@ -27,28 +28,44 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ 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<Props> = ({ children }) => {
redo,
canUndo,
canRedo,
isProcessing,
present: datatbaseState.present
}}
>
@@ -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<Props> = ({ children }) => {
@@ -121,6 +122,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}, [database])
const orderTableFields = useCallback(async (fieldsList: FieldType[]): Promise<QueryResult> => {
const caseStatements = fieldsList
.map((field, index) => `WHEN '${field.id}' THEN ${index}`)
.join('\n ');
@@ -134,6 +136,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
${ids}
);`;
return await powerSyncDb.execute(sql);
}, [powerSyncDb]);
const createRelationship = useCallback(async (relationship: RelationshipInsertType): Promise<QueryResult> => {
@@ -160,6 +163,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}, [db]);
const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise<QueryResult> => {
const posXCases = tableList
.map(table => `WHEN '${table.id}' THEN ${table.posX}`)
.join('\n ');
@@ -180,48 +184,52 @@ const DatabaseProvider: React.FC<Props> = ({ 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]);
+62 -32
View File
@@ -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;
}
+4 -3
View File
@@ -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)
+8 -6
View File
@@ -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
}