Skip to content

Commit 5972a0e

Browse files
committed
Add basic undo/redo capability
1 parent 2499852 commit 5972a0e

File tree

3 files changed

+137
-1
lines changed

3 files changed

+137
-1
lines changed

app/src/components/WithGraph.tsx

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { memo, ReactNode, Suspense, useCallback, useState } from "react";
1+
import {
2+
memo,
3+
ReactNode,
4+
Suspense,
5+
useCallback,
6+
useEffect,
7+
useState,
8+
} from "react";
29

310
import { useHasProAccess, useFullscreen } from "../lib/hooks";
411
import { useUnmountStore } from "../lib/useUnmountStore";
@@ -10,6 +17,7 @@ import styles from "./WithGraph.module.css";
1017
import TabPane from "./TabPane";
1118
import { useMobileStore } from "../lib/useMobileStore";
1219
import { useTabsStore } from "../lib/useTabsStore";
20+
import { redo, undo } from "../lib/undoStack";
1321

1422
type MainProps = {
1523
children?: ReactNode;
@@ -25,6 +33,27 @@ const WithGraph = memo(({ children }: MainProps) => {
2533
const tab = useMobileStore((state) => state.tab);
2634
const selectedTab = useTabsStore((state) => state.selectedTab);
2735

36+
useEffect(() => {
37+
const handleKeyDown = (event: KeyboardEvent) => {
38+
if (event.metaKey || event.ctrlKey) {
39+
if (event.key === "z") {
40+
if (event.shiftKey) {
41+
redo();
42+
} else {
43+
undo();
44+
}
45+
event.preventDefault();
46+
} else if (event.key === "y") {
47+
redo();
48+
event.preventDefault();
49+
}
50+
}
51+
};
52+
53+
window.addEventListener("keydown", handleKeyDown);
54+
return () => window.removeEventListener("keydown", handleKeyDown);
55+
}, []);
56+
2857
return (
2958
<div
3059
className="relative grid grid-rows-[[main]_minmax(0,1fr)_auto] grid-cols-[[main]_minmax(0,1fr)] md:flex md:shadow-xl"

app/src/lib/alignNodes.ts

+70
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NodePositions } from "../components/getNodePositionsFromCy";
22
import { useDoc } from "./useDoc";
3+
import { addToUndoStack } from "./undoStack";
34

45
/**
56
* This function tries to align nodes vertical and horiontal on their center
@@ -15,6 +16,9 @@ export function alignNodes() {
1516
const threshold = 40; // Adjust this value to change the alignment sensitivity
1617
const alignedPositions: NodePositions = {};
1718

19+
// Store the original positions for undo
20+
const originalPositions = { ...nodePositions };
21+
1822
// Iterate through all nodes
1923
Object.entries(nodePositions).forEach(([nodeId, position]) => {
2024
let closestHorizontal: cytoscape.Position | null = null;
@@ -56,6 +60,26 @@ export function alignNodes() {
5660
nodePositions: alignedPositions,
5761
},
5862
}));
63+
64+
// Add the action to the undo stack
65+
addToUndoStack({
66+
undo: () => {
67+
useDoc.setState((state) => ({
68+
meta: {
69+
...state.meta,
70+
nodePositions: originalPositions,
71+
},
72+
}));
73+
},
74+
redo: () => {
75+
useDoc.setState((state) => ({
76+
meta: {
77+
...state.meta,
78+
nodePositions: alignedPositions,
79+
},
80+
}));
81+
},
82+
});
5983
}
6084

6185
/**
@@ -68,6 +92,9 @@ export function alignNodesHorizontally(nodeIds: string[]) {
6892
const nodePositions = meta.nodePositions as NodePositions;
6993
if (!nodePositions) return;
7094

95+
// Store the original positions for undo
96+
const originalPositions = { ...nodePositions };
97+
7198
// Calculate the average x position
7299
let sumX = 0;
73100
let count = 0;
@@ -99,6 +126,26 @@ export function alignNodesHorizontally(nodeIds: string[]) {
99126
nodePositions: alignedPositions,
100127
},
101128
}));
129+
130+
// Add the action to the undo stack
131+
addToUndoStack({
132+
undo: () => {
133+
useDoc.setState((state) => ({
134+
meta: {
135+
...state.meta,
136+
nodePositions: originalPositions,
137+
},
138+
}));
139+
},
140+
redo: () => {
141+
useDoc.setState((state) => ({
142+
meta: {
143+
...state.meta,
144+
nodePositions: alignedPositions,
145+
},
146+
}));
147+
},
148+
});
102149
}
103150

104151
/**
@@ -111,6 +158,9 @@ export function alignNodesVertically(nodeIds: string[]) {
111158
const nodePositions = meta.nodePositions as NodePositions;
112159
if (!nodePositions) return;
113160

161+
// Store the original positions for undo
162+
const originalPositions = { ...nodePositions };
163+
114164
// Calculate the average y position
115165
let sumY = 0;
116166
let count = 0;
@@ -142,4 +192,24 @@ export function alignNodesVertically(nodeIds: string[]) {
142192
nodePositions: alignedPositions,
143193
},
144194
}));
195+
196+
// Add the action to the undo stack
197+
addToUndoStack({
198+
undo: () => {
199+
useDoc.setState((state) => ({
200+
meta: {
201+
...state.meta,
202+
nodePositions: originalPositions,
203+
},
204+
}));
205+
},
206+
redo: () => {
207+
useDoc.setState((state) => ({
208+
meta: {
209+
...state.meta,
210+
nodePositions: alignedPositions,
211+
},
212+
}));
213+
},
214+
});
145215
}

app/src/lib/undoStack.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
interface UndoAction {
2+
undo: () => void;
3+
redo: () => void;
4+
}
5+
6+
let undoStack: UndoAction[] = [];
7+
let redoStack: UndoAction[] = [];
8+
9+
export function addToUndoStack(action: UndoAction) {
10+
undoStack.push(action);
11+
redoStack = []; // Clear redo stack when a new action is performed
12+
}
13+
14+
export function undo() {
15+
const action = undoStack.pop();
16+
if (action) {
17+
action.undo();
18+
redoStack.push(action);
19+
}
20+
}
21+
22+
export function redo() {
23+
const action = redoStack.pop();
24+
if (action) {
25+
action.redo();
26+
undoStack.push(action);
27+
}
28+
}
29+
30+
// Optional: Add a function to check if undo/redo is available
31+
export function canUndo(): boolean {
32+
return undoStack.length > 0;
33+
}
34+
35+
export function canRedo(): boolean {
36+
return redoStack.length > 0;
37+
}

0 commit comments

Comments
 (0)