diff --git a/DoodleBUGS/package-lock.json b/DoodleBUGS/package-lock.json
index 603ed73a8..e06ad630f 100644
--- a/DoodleBUGS/package-lock.json
+++ b/DoodleBUGS/package-lock.json
@@ -20,6 +20,7 @@
"cytoscape-no-overlap": "^1.0.1",
"cytoscape-snap-to-grid": "^2.0.0",
"cytoscape-svg": "^0.4.0",
+ "cytoscape-undo-redo": "^1.3.3",
"pinia": "^3.0.2",
"vue": "^3.5.13"
},
@@ -2731,6 +2732,15 @@
"cytoscape": "^3.2.0"
}
},
+ "node_modules/cytoscape-undo-redo": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/cytoscape-undo-redo/-/cytoscape-undo-redo-1.3.3.tgz",
+ "integrity": "sha512-BjTXe0Oiytj4+UtYkkwYHG3BILi6MZD1MuZ+06kopgEtiyumcMEqX53zHMhY90kqJMPwLfpcrlmKmJtNAa2v2Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "cytoscape": "^3.3.0"
+ }
+ },
"node_modules/d3-dispatch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
diff --git a/DoodleBUGS/package.json b/DoodleBUGS/package.json
index 3a9154fd2..071c04ca4 100644
--- a/DoodleBUGS/package.json
+++ b/DoodleBUGS/package.json
@@ -24,6 +24,7 @@
"cytoscape-no-overlap": "^1.0.1",
"cytoscape-snap-to-grid": "^2.0.0",
"cytoscape-svg": "^0.4.0",
+ "cytoscape-undo-redo": "^1.3.3",
"pinia": "^3.0.2",
"vue": "^3.5.13"
},
diff --git a/DoodleBUGS/src/components/canvas/CanvasToolbar.vue b/DoodleBUGS/src/components/canvas/CanvasToolbar.vue
index 6b34c8d81..9f7625e2c 100644
--- a/DoodleBUGS/src/components/canvas/CanvasToolbar.vue
+++ b/DoodleBUGS/src/components/canvas/CanvasToolbar.vue
@@ -1,5 +1,6 @@
+
+
\ No newline at end of file
diff --git a/DoodleBUGS/src/composables/useGraphElements.ts b/DoodleBUGS/src/composables/useGraphElements.ts
index 7d795a3ff..df662424f 100644
--- a/DoodleBUGS/src/composables/useGraphElements.ts
+++ b/DoodleBUGS/src/composables/useGraphElements.ts
@@ -1,9 +1,11 @@
import { computed, ref } from 'vue';
import { useGraphStore } from '../stores/graphStore';
-import type { GraphElement } from '../types';
+import { useGraphInstance } from './useGraphInstance';
+import type { GraphElement, GraphNode } from '../types';
export function useGraphElements() {
const graphStore = useGraphStore();
+ const { getCyInstance, getUndoRedoInstance } = useGraphInstance();
const selectedElement = ref(null);
@@ -17,8 +19,32 @@ export function useGraphElements() {
});
const addElement = (newElement: GraphElement) => {
+ // Add to Vue store first for immediate UI update
elements.value = [...elements.value, newElement];
selectedElement.value = newElement;
+
+ // Add to cytoscape - let cytoscape-undo-redo handle the tracking
+ const cy = getCyInstance();
+ const ur = getUndoRedoInstance();
+
+ if (cy) {
+ try {
+ const cyElement = cy.add({
+ group: newElement.type === 'node' ? 'nodes' : 'edges',
+ data: newElement,
+ position: newElement.type === 'node' ? (newElement as GraphNode).position : undefined
+ });
+
+ console.log('Element added to cytoscape:', newElement.id);
+
+ // Manually trigger undo-redo tracking if needed
+ if (ur) {
+ console.log('Undo-redo instance available for tracking');
+ }
+ } catch (error) {
+ console.warn('Failed to add element to cytoscape:', error);
+ }
+ }
};
const updateElement = (updatedElement: GraphElement) => {
@@ -30,7 +56,40 @@ export function useGraphElements() {
}
};
- const deleteElement = (elementId: string) => {
+ const deleteElement = (elementId: string, visited = new Set()) => {
+ const elementToDelete = elements.value.find(el => el.id === elementId);
+ if (!elementToDelete) return;
+
+ // Remove from Vue store for immediate UI update
+ deleteElementManually(elementId, visited);
+
+ // Remove from cytoscape - let cytoscape-undo-redo handle the tracking
+ const cy = getCyInstance();
+ const ur = getUndoRedoInstance();
+
+ if (cy) {
+ try {
+ const element = cy.getElementById(elementId);
+ if (element.length > 0) {
+ element.remove();
+ console.log('Element removed from cytoscape:', elementId);
+
+ if (ur) {
+ console.log('Undo-redo instance available for tracking removal');
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to remove element from cytoscape:', error);
+ }
+ }
+ };
+
+ const deleteElementManually = (elementId: string, visited = new Set()) => {
+ if (visited.has(elementId)) {
+ return;
+ }
+ visited.add(elementId);
+
const elementToDelete = elements.value.find(el => el.id === elementId);
if (!elementToDelete) return;
diff --git a/DoodleBUGS/src/composables/useGraphInstance.ts b/DoodleBUGS/src/composables/useGraphInstance.ts
index 32e8dae52..69c05389e 100644
--- a/DoodleBUGS/src/composables/useGraphInstance.ts
+++ b/DoodleBUGS/src/composables/useGraphInstance.ts
@@ -10,6 +10,8 @@ import cola from 'cytoscape-cola';
import klay from 'cytoscape-klay';
import { useCompoundDragDrop } from './useCompoundDragDrop';
import svg from 'cytoscape-svg';
+// @ts-ignore
+import undoRedo from 'cytoscape-undo-redo';
// NOTE: Do NOT register gridGuide or contextMenus - they break iPad/mobile touch events
cytoscape.use(dagre);
@@ -17,8 +19,11 @@ cytoscape.use(fcose);
cytoscape.use(cola);
cytoscape.use(klay);
cytoscape.use(svg);
+// @ts-ignore
+cytoscape.use(undoRedo);
let cyInstance: Core | null = null;
+let undoRedoInstance: any = null;
export function useGraphInstance() {
const initCytoscape = (container: HTMLElement, initialElements: ElementDefinition[]): Core => {
@@ -173,6 +178,22 @@ export function useGraphInstance() {
});
*/
+ // Initialize undo-redo functionality safely
+ try {
+ undoRedoInstance = (cyInstance as any).undoRedo({
+ isDebug: true, // Set to true for debugging
+ undoableDrag: true, // Allow dragging to be undoable
+ stackSizeLimit: undefined, // Unlimited stack size
+ ready: () => {
+ // This function is called when undo-redo is ready
+ console.log('Undo-redo functionality initialized successfully');
+ }
+ });
+ } catch (error) {
+ console.warn('Undo-redo initialization failed:', error);
+ undoRedoInstance = null;
+ }
+
return cyInstance;
};
@@ -180,10 +201,57 @@ export function useGraphInstance() {
if (cy) {
cy.destroy();
cyInstance = null;
+ undoRedoInstance = null;
}
};
const getCyInstance = (): Core | null => cyInstance;
- return { initCytoscape, destroyCytoscape, getCyInstance };
+ const getUndoRedoInstance = (): any => undoRedoInstance;
+
+ const undo = (): boolean => {
+ if (undoRedoInstance) {
+ return undoRedoInstance.undo();
+ }
+ return false;
+ };
+
+ const redo = (): boolean => {
+ if (undoRedoInstance) {
+ return undoRedoInstance.redo();
+ }
+ return false;
+ };
+
+ const isUndoStackEmpty = (): boolean => {
+ if (undoRedoInstance) {
+ return undoRedoInstance.isUndoStackEmpty();
+ }
+ return true;
+ };
+
+ const isRedoStackEmpty = (): boolean => {
+ if (undoRedoInstance) {
+ return undoRedoInstance.isRedoStackEmpty();
+ }
+ return true;
+ };
+
+ const resetUndoRedoStack = (): void => {
+ if (undoRedoInstance) {
+ undoRedoInstance.reset();
+ }
+ };
+
+ return {
+ initCytoscape,
+ destroyCytoscape,
+ getCyInstance,
+ getUndoRedoInstance,
+ undo,
+ redo,
+ isUndoStackEmpty,
+ isRedoStackEmpty,
+ resetUndoRedoStack
+ };
}
diff --git a/DoodleBUGS/src/composables/useUndoRedo.ts b/DoodleBUGS/src/composables/useUndoRedo.ts
new file mode 100644
index 000000000..9a8c0263b
--- /dev/null
+++ b/DoodleBUGS/src/composables/useUndoRedo.ts
@@ -0,0 +1,103 @@
+import { ref, computed } from 'vue';
+import { useGraphInstance } from './useGraphInstance';
+import { useGraphStore } from '../stores/graphStore';
+import type { GraphElement, GraphNode, GraphEdge } from '../types';
+
+export function useUndoRedo() {
+ const { undo, redo, isUndoStackEmpty, isRedoStackEmpty, resetUndoRedoStack, getCyInstance, getUndoRedoInstance } = useGraphInstance();
+ const graphStore = useGraphStore();
+
+ // Reactive state for UI updates
+ const canUndo = ref(false);
+ const canRedo = ref(false);
+
+ // Update the state by checking the actual undo-redo instance
+ const updateUndoRedoState = () => {
+ try {
+ const newCanUndo = !isUndoStackEmpty();
+ const newCanRedo = !isRedoStackEmpty();
+
+ if (newCanUndo !== canUndo.value || newCanRedo !== canRedo.value) {
+ canUndo.value = newCanUndo;
+ canRedo.value = newCanRedo;
+ console.log(`Undo/Redo state updated: undo=${newCanUndo}, redo=${newCanRedo}`);
+ }
+ } catch (error) {
+ console.warn('Failed to update undo/redo state:', error);
+ canUndo.value = false;
+ canRedo.value = false;
+ }
+ };
+
+ // Perform undo operation
+ const performUndo = (): boolean => {
+ console.log('Attempting undo...');
+ try {
+ const result = undo();
+ console.log(`Undo result: ${result}`);
+ updateUndoRedoState();
+ return result;
+ } catch (error) {
+ console.error('Undo failed:', error);
+ return false;
+ }
+ };
+
+ // Perform redo operation
+ const performRedo = (): boolean => {
+ console.log('Attempting redo...');
+ try {
+ const result = redo();
+ console.log(`Redo result: ${result}`);
+ updateUndoRedoState();
+ return result;
+ } catch (error) {
+ console.error('Redo failed:', error);
+ return false;
+ }
+ };
+
+ // Reset undo/redo stacks
+ const resetStacks = (): void => {
+ try {
+ resetUndoRedoStack();
+ updateUndoRedoState();
+ console.log('Reset undo/redo stacks');
+ } catch (error) {
+ console.warn('Failed to reset stacks:', error);
+ }
+ };
+
+ // Keyboard shortcuts handler
+ const handleKeyboardShortcuts = (event: KeyboardEvent): void => {
+ // Only handle shortcuts if not typing in an input field
+ const target = event.target as HTMLElement;
+ if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true') {
+ return;
+ }
+
+ // Ctrl+Z for undo
+ if (event.ctrlKey && event.key === 'z' && !event.shiftKey) {
+ event.preventDefault();
+ console.log('Keyboard shortcut: Ctrl+Z (Undo)');
+ performUndo();
+ }
+ // Ctrl+Shift+Z or Ctrl+Y for redo
+ else if ((event.ctrlKey && event.shiftKey && event.key === 'Z') ||
+ (event.ctrlKey && event.key === 'y')) {
+ event.preventDefault();
+ console.log('Keyboard shortcut: Ctrl+Shift+Z or Ctrl+Y (Redo)');
+ performRedo();
+ }
+ };
+
+ return {
+ canUndo: computed(() => canUndo.value),
+ canRedo: computed(() => canRedo.value),
+ performUndo,
+ performRedo,
+ resetStacks,
+ updateUndoRedoState,
+ handleKeyboardShortcuts
+ };
+}
\ No newline at end of file
diff --git a/DoodleBUGS/src/types/cytoscape-undo-redo.d.ts b/DoodleBUGS/src/types/cytoscape-undo-redo.d.ts
new file mode 100644
index 000000000..8d9acf61e
--- /dev/null
+++ b/DoodleBUGS/src/types/cytoscape-undo-redo.d.ts
@@ -0,0 +1,31 @@
+declare module 'cytoscape-undo-redo' {
+ import { Core } from 'cytoscape';
+
+ interface UndoRedoOptions {
+ isDebug?: boolean;
+ actions?: {
+ [key: string]: {
+ undo: (arg: any) => any;
+ redo: (arg: any) => any;
+ };
+ };
+ undoableDrag?: boolean;
+ stackSizeLimit?: number;
+ ready?: () => void;
+ }
+
+ interface UndoRedoInstance {
+ do: (actionName: string, args: any) => any;
+ undo: () => boolean;
+ redo: () => boolean;
+ reset: () => void;
+ isUndoStackEmpty: () => boolean;
+ isRedoStackEmpty: () => boolean;
+ getUndoStackSize: () => number;
+ getRedoStackSize: () => number;
+ }
+
+ function undoRedo(options?: UndoRedoOptions): (cy: Core) => UndoRedoInstance;
+
+ export = undoRedo;
+}
\ No newline at end of file
diff --git a/DoodleBUGS/src/types/index.ts b/DoodleBUGS/src/types/index.ts
index c2755da09..f32b90b40 100644
--- a/DoodleBUGS/src/types/index.ts
+++ b/DoodleBUGS/src/types/index.ts
@@ -13,8 +13,8 @@ export interface GraphNode {
id: string;
name: string;
type: 'node';
- nodeType: NodeType;
- position: { x: number; y: number; };
+ nodeType?: string;
+ position?: { x: number; y: number };
parent?: string;
// Properties from definitions