Phase 7: Undo/Redo

- History tracking with 50-state limit in useDesignEditor hook
- Undo: Ctrl/Cmd + Z keyboard shortcut
- Redo: Ctrl/Cmd + Shift + Z or Ctrl/Cmd + Y
- Undo/Redo buttons in canvas header
- History saves state after add, update, delete, reorder operations
- Disabled button states when history is exhausted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Khalid A
2026-04-21 01:27:51 -05:00
parent 72495fec3e
commit 537cfd572d
3 changed files with 178 additions and 15 deletions

View File

@@ -1,31 +1,70 @@
import { useState, useCallback } from 'react';
import { useState, useCallback, useRef } from 'react';
const MAX_HISTORY = 50;
export function useDesignEditor() {
const [elements, setElements] = useState([]);
const [selectedId, setSelectedId] = useState(null);
// History for undo/redo
const historyRef = useRef([]);
const historyIndexRef = useRef(-1);
const saveToHistory = useCallback((newElements) => {
// Remove any future history if we're in the middle of the stack
if (historyIndexRef.current < historyRef.current.length - 1) {
historyRef.current = historyRef.current.slice(0, historyIndexRef.current + 1);
}
// Add new state to history
historyRef.current.push(JSON.stringify(newElements));
// Limit history size
if (historyRef.current.length > MAX_HISTORY) {
historyRef.current.shift();
} else {
historyIndexRef.current++;
}
}, []);
const canUndo = historyIndexRef.current > 0;
const canRedo = historyIndexRef.current < historyRef.current.length - 1;
const addElement = useCallback((element) => {
const newElement = {
...element,
id: `element-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
};
setElements((prev) => [...prev, newElement]);
setElements((prev) => {
const newElements = [...prev, newElement];
saveToHistory(newElements);
return newElements;
});
setSelectedId(newElement.id);
return newElement.id;
}, []);
}, [saveToHistory]);
const updateElement = useCallback((id, attrs) => {
setElements((prev) =>
prev.map((el) => (el.id === id ? { ...el, ...attrs } : el))
);
}, []);
setElements((prev) => {
const newElements = prev.map((el) => (el.id === id ? { ...el, ...attrs } : el));
saveToHistory(newElements);
return newElements;
});
}, [saveToHistory]);
const deleteElement = useCallback((id) => {
setElements((prev) => prev.filter((el) => el.id !== id));
setElements((prev) => {
const newElements = prev.filter((el) => el.id !== id);
saveToHistory(newElements);
return newElements;
});
if (selectedId === id) {
setSelectedId(null);
}
}, [selectedId]);
}, [selectedId, saveToHistory]);
const selectElement = useCallback((id) => {
setSelectedId(id);
@@ -39,11 +78,38 @@ export function useDesignEditor() {
setElements((prev) => {
const index = prev.findIndex((el) => el.id === id);
if (index === -1 || index === newOrder) return prev;
const newElements = [...prev];
const [removed] = newElements.splice(index, 1);
newElements.splice(newOrder, 0, removed);
saveToHistory(newElements);
return newElements;
});
}, [saveToHistory]);
const undo = useCallback(() => {
if (historyIndexRef.current > 0) {
historyIndexRef.current--;
const prevState = JSON.parse(historyRef.current[historyIndexRef.current]);
setElements(prevState);
setSelectedId(null);
}
}, []);
const redo = useCallback(() => {
if (historyIndexRef.current < historyRef.current.length - 1) {
historyIndexRef.current++;
const nextState = JSON.parse(historyRef.current[historyIndexRef.current]);
setElements(nextState);
setSelectedId(null);
}
}, []);
// Initialize history with empty state
const initializeHistory = useCallback(() => {
historyRef.current = [JSON.stringify([])];
historyIndexRef.current = 0;
}, []);
return {
@@ -55,5 +121,10 @@ export function useDesignEditor() {
selectElement,
deselectAll,
reorderElement,
undo,
redo,
canUndo,
canRedo,
initializeHistory,
};
}