Browse Source

feat(quick-010): wrap workflowStore with temporal middleware and wire keyboard shortcuts

- Wrap store creation with temporal() middleware
  - partialize: Only track nodes (binary-stripped), edges, edgeStyle, groups
  - limit: 20 undo steps max
  - equality: Fast referential check using undoStateEquality()
- Add keyboard shortcuts:
  - Ctrl/Cmd+Z: Undo last change
  - Ctrl/Cmd+Shift+Z: Redo
  - Ctrl/Cmd+Y: Redo (Windows alt)
- Add drag debouncing to prevent per-pixel snapshots:
  - onNodeDragStart: Pause temporal tracking
  - onNodeDragStop: Resume tracking and capture final position
- Add _nudgeForSnapshot() helper to force snapshot capture
- Mark workflow as unsaved after undo/redo
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
5af5e34527
  1. 43
      src/components/WorkflowCanvas.tsx
  2. 31
      src/store/workflowStore.ts

43
src/components/WorkflowCanvas.tsx

@ -281,6 +281,10 @@ export function WorkflowCanvas() {
// Check if a node was dropped into a group and add it to that group
const handleNodeDragStart = useCallback(() => {
useWorkflowStore.temporal.getState().pause();
}, []);
const handleNodeDragStop = useCallback(
(_event: React.MouseEvent, node: Node) => {
// Skip if it's a group node
@ -313,6 +317,11 @@ export function WorkflowCanvas() {
if (targetGroupId !== currentGroupId) {
setNodeGroupId(node.id, targetGroupId);
}
// Resume undo tracking after drag and capture final position
const temporal = useWorkflowStore.temporal.getState();
temporal.resume();
useWorkflowStore.getState()._nudgeForSnapshot();
},
[groups, nodes, setNodeGroupId]
);
@ -1081,6 +1090,39 @@ export function WorkflowCanvas() {
return;
}
// Handle undo (Ctrl/Cmd + Z, but NOT Ctrl/Cmd + Shift + Z)
if ((event.ctrlKey || event.metaKey) && (event.key === "z" || event.key === "Z") && !event.shiftKey) {
event.preventDefault();
const { undo, pastStates } = useWorkflowStore.temporal.getState();
if (pastStates.length > 0) {
undo();
useWorkflowStore.setState({ hasUnsavedChanges: true });
}
return;
}
// Handle redo (Ctrl/Cmd + Shift + Z)
if ((event.ctrlKey || event.metaKey) && (event.key === "z" || event.key === "Z") && event.shiftKey) {
event.preventDefault();
const { redo, futureStates } = useWorkflowStore.temporal.getState();
if (futureStates.length > 0) {
redo();
useWorkflowStore.setState({ hasUnsavedChanges: true });
}
return;
}
// Handle redo alt (Ctrl + Y — Windows convention)
if ((event.ctrlKey || event.metaKey) && event.key === "y") {
event.preventDefault();
const { redo, futureStates } = useWorkflowStore.temporal.getState();
if (futureStates.length > 0) {
redo();
useWorkflowStore.setState({ hasUnsavedChanges: true });
}
return;
}
// Handle copy (Ctrl/Cmd + C)
if ((event.ctrlKey || event.metaKey) && event.key === "c") {
event.preventDefault();
@ -1643,6 +1685,7 @@ export function WorkflowCanvas() {
onEdgesChange={onEdgesChange}
onConnect={handleConnect}
onConnectEnd={handleConnectEnd}
onNodeDragStart={handleNodeDragStart}
onNodeDragStop={handleNodeDragStop}
onSelectionChange={handleSelectionChange}
nodeTypes={nodeTypes}

31
src/store/workflowStore.ts

@ -1,5 +1,6 @@
import { create } from "zustand";
import { create, StateCreator } from "zustand";
import { useShallow } from "zustand/shallow";
import { temporal, TemporalState } from "zundo";
import {
Connection,
EdgeChange,
@ -43,6 +44,7 @@ import {
getCanvasNavigationSettings,
saveCanvasNavigationSettings,
} from "./utils/localStorage";
import { partializeForUndo, undoStateEquality } from "./undoUtils";
import {
createDefaultNodeData,
defaultNodeDimensions,
@ -333,8 +335,14 @@ interface WorkflowStore {
// Canvas navigation settings actions
updateCanvasNavigationSettings: (settings: CanvasNavigationSettings) => void;
// Undo/Redo helper actions
_nudgeForSnapshot: () => void;
}
// Extend with TemporalState for undo/redo functionality
export type WorkflowStoreWithTemporal = WorkflowStore & TemporalState<WorkflowStore>;
let nodeIdCounter = 0;
let groupIdCounter = 0;
let autoSaveIntervalId: ReturnType<typeof setInterval> | null = null;
@ -369,7 +377,8 @@ async function waitForPendingImageSyncs(timeout: number = 60000): Promise<void>
export { generateWorkflowId, saveGenerateImageDefaults, saveNanoBananaDefaults } from "./utils/localStorage";
export { GROUP_COLORS } from "./utils/nodeDefaults";
export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
// Store implementation with temporal middleware for undo/redo
const workflowStoreImpl: StateCreator<WorkflowStore, [["temporal", unknown]], []> = (set, get) => ({
nodes: [],
edges: [],
edgeStyle: "curved" as EdgeStyle,
@ -2111,7 +2120,23 @@ export const useWorkflowStore = create<WorkflowStore>((set, get) => ({
set({ canvasNavigationSettings: settings });
saveCanvasNavigationSettings(settings);
},
}));
// Undo/Redo helper actions
_nudgeForSnapshot: () => {
set((state) => ({ nodes: state.nodes }));
},
});
export const useWorkflowStore = create<WorkflowStore>()(
temporal(
workflowStoreImpl,
{
partialize: partializeForUndo as any,
limit: 20,
equality: undoStateEquality as any,
}
)
);
/**
* Stable hook for provider API keys.

Loading…
Cancel
Save