Merge branch 'dev' of https://github.com/logspace-ai/langflow into streaming
This commit is contained in:
commit
92aadaf934
2 changed files with 116 additions and 2 deletions
109
src/frontend/src/pages/FlowPage/hooks/useUndoRedo.ts
Normal file
109
src/frontend/src/pages/FlowPage/hooks/useUndoRedo.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Edge, Node, useReactFlow } from 'reactflow';
|
||||
|
||||
type UseUndoRedoOptions = {
|
||||
maxHistorySize: number;
|
||||
enableShortcuts: boolean;
|
||||
};
|
||||
|
||||
type UseUndoRedo = (options?: UseUndoRedoOptions) => {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
takeSnapshot: () => void;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
};
|
||||
|
||||
type HistoryItem = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
const defaultOptions: UseUndoRedoOptions = {
|
||||
maxHistorySize: 100,
|
||||
enableShortcuts: true,
|
||||
};
|
||||
|
||||
// https://redux.js.org/usage/implementing-undo-history
|
||||
export const useUndoRedo: UseUndoRedo = ({
|
||||
maxHistorySize = defaultOptions.maxHistorySize,
|
||||
enableShortcuts = defaultOptions.enableShortcuts,
|
||||
} = defaultOptions) => {
|
||||
// the past and future arrays store the states that we can jump to
|
||||
const [past, setPast] = useState<HistoryItem[]>([]);
|
||||
const [future, setFuture] = useState<HistoryItem[]>([]);
|
||||
|
||||
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
|
||||
|
||||
const takeSnapshot = useCallback(() => {
|
||||
// push the current graph to the past state
|
||||
setPast((past) => [
|
||||
...past.slice(past.length - maxHistorySize + 1, past.length),
|
||||
{ nodes: getNodes(), edges: getEdges() },
|
||||
]);
|
||||
|
||||
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
|
||||
setFuture([]);
|
||||
}, [getNodes, getEdges, maxHistorySize]);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
// get the last state that we want to go back to
|
||||
const pastState = past[past.length - 1];
|
||||
|
||||
if (pastState) {
|
||||
// first we remove the state from the history
|
||||
setPast((past) => past.slice(0, past.length - 1));
|
||||
// we store the current graph for the redo operation
|
||||
setFuture((future) => [...future, { nodes: getNodes(), edges: getEdges() }]);
|
||||
// now we can set the graph to the past state
|
||||
setNodes(pastState.nodes);
|
||||
setEdges(pastState.edges);
|
||||
}
|
||||
}, [setNodes, setEdges, getNodes, getEdges, past]);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
const futureState = future[future.length - 1];
|
||||
|
||||
if (futureState) {
|
||||
setFuture((future) => future.slice(0, future.length - 1));
|
||||
setPast((past) => [...past, { nodes: getNodes(), edges: getEdges() }]);
|
||||
setNodes(futureState.nodes);
|
||||
setEdges(futureState.edges);
|
||||
}
|
||||
}, [setNodes, setEdges, getNodes, getEdges, future]);
|
||||
|
||||
useEffect(() => {
|
||||
// this effect is used to attach the global event handlers
|
||||
if (!enableShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'z' && (event.ctrlKey || event.metaKey) && event.shiftKey) {
|
||||
redo();
|
||||
}
|
||||
else if (event.key === 'y' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault(); // prevent the default action
|
||||
redo();
|
||||
} else if (event.key === 'z' && (event.ctrlKey || event.metaKey)) {
|
||||
undo();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keyDownHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler);
|
||||
};
|
||||
}, [undo, redo, enableShortcuts]);
|
||||
|
||||
return {
|
||||
undo,
|
||||
redo,
|
||||
takeSnapshot,
|
||||
canUndo: !!past.length,
|
||||
canRedo: !!future.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUndoRedo;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useContext, useEffect, useRef } from "react";
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
|
|
@ -22,6 +22,7 @@ import ConnectionLineComponent from "./components/ConnectionLineComponent";
|
|||
import { FlowType, NodeType } from "../../types/flow";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import { isValidConnection } from "../../utils";
|
||||
import useUndoRedo from "./hooks/useUndoRedo";
|
||||
|
||||
const nodeTypes = {
|
||||
genericNode: GenericNode,
|
||||
|
|
@ -185,7 +186,7 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full" ref={reactFlowWrapper}>
|
||||
<div className="w-full h-full" onMouseMove={handleMouseMove} ref={reactFlowWrapper}>
|
||||
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
|
||||
<>
|
||||
<ReactFlow
|
||||
|
|
@ -196,6 +197,7 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
|
|||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChangeMod}
|
||||
onKeyDown={(e) => onKeyDown(e)}
|
||||
onConnect={onConnect}
|
||||
onLoad={setReactFlowInstance}
|
||||
onInit={setReactFlowInstance}
|
||||
|
|
@ -203,6 +205,9 @@ export default function FlowPage({ flow }: { flow: FlowType }) {
|
|||
onEdgeUpdate={onEdgeUpdate}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onSelectionDragStart={onSelectionDragStart}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
connectionLineComponent={ConnectionLineComponent}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue