mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 19:25:44 +00:00
Fix Undo/Redo craches whenever the user click on shortcuts very fast
This commit is contained in:
@@ -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])
|
||||
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user