added useUndoRedo to context and added Undo and Redo buttons to header
This commit is contained in:
parent
1af07ec453
commit
1452e9c7a2
7 changed files with 245 additions and 199 deletions
|
|
@ -40,8 +40,6 @@ export default function App() {
|
||||||
setSuccessOpen,
|
setSuccessOpen,
|
||||||
} = useContext(alertContext);
|
} = useContext(alertContext);
|
||||||
|
|
||||||
const { flows, addFlow } = useContext(TabsContext);
|
|
||||||
|
|
||||||
// Initialize state variable for the list of alerts
|
// Initialize state variable for the list of alerts
|
||||||
const [alertsList, setAlertsList] = useState<
|
const [alertsList, setAlertsList] = useState<
|
||||||
Array<{
|
Array<{
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,13 @@ import ApiModal from "../../../../modals/ApiModal";
|
||||||
import { alertContext } from "../../../../contexts/alertContext";
|
import { alertContext } from "../../../../contexts/alertContext";
|
||||||
import { updateFlowInDatabase } from "../../../../controllers/API";
|
import { updateFlowInDatabase } from "../../../../controllers/API";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||||
|
|
||||||
export const MenuBar = ({ setRename, rename, flows, tabId }) => {
|
export const MenuBar = ({ setRename, rename, flows, tabId }) => {
|
||||||
const { updateFlow, setTabId, addFlow } = useContext(TabsContext);
|
const { updateFlow, setTabId, addFlow } = useContext(TabsContext);
|
||||||
const { setErrorData } = useContext(alertContext);
|
const { setErrorData } = useContext(alertContext);
|
||||||
const { openPopUp } = useContext(PopUpContext);
|
const { openPopUp } = useContext(PopUpContext);
|
||||||
|
const { undo, redo } = useContext(undoRedoContext);
|
||||||
|
|
||||||
function handleSaveFlow(flow) {
|
function handleSaveFlow(flow) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -121,6 +123,22 @@ export const MenuBar = ({ setRename, rename, flows, tabId }) => {
|
||||||
<Edit className="w-4 h-4 mr-2" />
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
Rename
|
Rename
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
undo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
|
Undo
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
redo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
|
Redo
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuLabel>Flows</DropdownMenuLabel>
|
<DropdownMenuLabel>Flows</DropdownMenuLabel>
|
||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import PopUpProvider from "./popUpContext";
|
||||||
import { TabsProvider } from "./tabsContext";
|
import { TabsProvider } from "./tabsContext";
|
||||||
import { TypesProvider } from "./typesContext";
|
import { TypesProvider } from "./typesContext";
|
||||||
import { ReactFlowProvider } from "reactflow";
|
import { ReactFlowProvider } from "reactflow";
|
||||||
|
import { UndoRedoProvider } from "./undoRedoContext";
|
||||||
|
|
||||||
export default function ContextWrapper({ children }: { children: ReactNode }) {
|
export default function ContextWrapper({ children }: { children: ReactNode }) {
|
||||||
//element to wrap all context
|
//element to wrap all context
|
||||||
|
|
@ -17,7 +18,9 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
|
||||||
<LocationProvider>
|
<LocationProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
<TabsProvider>
|
<TabsProvider>
|
||||||
|
<UndoRedoProvider>
|
||||||
<PopUpProvider>{children}</PopUpProvider>
|
<PopUpProvider>{children}</PopUpProvider>
|
||||||
|
</UndoRedoProvider>
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</LocationProvider>
|
</LocationProvider>
|
||||||
|
|
|
||||||
194
src/frontend/src/contexts/undoRedoContext.tsx
Normal file
194
src/frontend/src/contexts/undoRedoContext.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
||||||
|
import { Edge, Node, useReactFlow } from "reactflow";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { TabsContext } from "./tabsContext";
|
||||||
|
|
||||||
|
type undoRedoContextType = {
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
takeSnapshot: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 initialValue = {
|
||||||
|
undo: () => {},
|
||||||
|
redo: () => {},
|
||||||
|
takeSnapshot: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions: UseUndoRedoOptions = {
|
||||||
|
maxHistorySize: 100,
|
||||||
|
enableShortcuts: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
|
||||||
|
|
||||||
|
export function UndoRedoProvider({ children }) {
|
||||||
|
const { tabId, flows } = useContext(TabsContext);
|
||||||
|
|
||||||
|
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||||
|
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||||
|
const [tabIndex, setTabIndex] = useState(flows.findIndex((f) => f.id === tabId));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// whenever the flows variable changes, we need to add one array to the past and future states
|
||||||
|
setPast((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
||||||
|
setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
||||||
|
setTabIndex(flows.findIndex((f) => f.id === tabId));
|
||||||
|
|
||||||
|
}, [flows, tabId]);
|
||||||
|
|
||||||
|
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
|
||||||
|
|
||||||
|
const takeSnapshot = useCallback(() => {
|
||||||
|
// push the current graph to the past state
|
||||||
|
console.log(past);
|
||||||
|
console.log(tabIndex);
|
||||||
|
setPast((old) => {
|
||||||
|
let newPast = cloneDeep(old);
|
||||||
|
newPast[tabIndex] = old[tabIndex].slice(
|
||||||
|
old[tabIndex].length - defaultOptions.maxHistorySize + 1,
|
||||||
|
old[tabIndex].length
|
||||||
|
);
|
||||||
|
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
||||||
|
return newPast;
|
||||||
|
});
|
||||||
|
|
||||||
|
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
|
||||||
|
setFuture((old) => {
|
||||||
|
let newFuture = cloneDeep(old);
|
||||||
|
newFuture[tabIndex] = [];
|
||||||
|
return newFuture;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
getNodes,
|
||||||
|
getEdges,
|
||||||
|
past,
|
||||||
|
future,
|
||||||
|
flows,
|
||||||
|
tabId,
|
||||||
|
setPast,
|
||||||
|
setFuture,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const undo = useCallback(() => {
|
||||||
|
// get the last state that we want to go back to
|
||||||
|
const pastState = past[tabIndex][past[tabIndex].length - 1];
|
||||||
|
|
||||||
|
if (pastState) {
|
||||||
|
// first we remove the state from the history
|
||||||
|
setPast((old) => {
|
||||||
|
let newPast = cloneDeep(old);
|
||||||
|
newPast[tabIndex] = old[tabIndex].slice(0, old[tabIndex].length - 1);
|
||||||
|
return newPast;
|
||||||
|
});
|
||||||
|
// we store the current graph for the redo operation
|
||||||
|
setFuture((old) => {
|
||||||
|
let newFuture = cloneDeep(old);
|
||||||
|
newFuture[tabIndex] = old[tabIndex];
|
||||||
|
newFuture[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
||||||
|
return newFuture;
|
||||||
|
});
|
||||||
|
// now we can set the graph to the past state
|
||||||
|
setNodes(pastState.nodes);
|
||||||
|
setEdges(pastState.edges);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
getNodes,
|
||||||
|
getEdges,
|
||||||
|
future,
|
||||||
|
past,
|
||||||
|
setFuture,
|
||||||
|
setPast,
|
||||||
|
tabIndex,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const redo = useCallback(() => {
|
||||||
|
const futureState = future[tabIndex][future[tabIndex].length - 1];
|
||||||
|
|
||||||
|
if (futureState) {
|
||||||
|
setFuture((old) => {
|
||||||
|
let newFuture = cloneDeep(old);
|
||||||
|
newFuture[tabIndex] = old[tabIndex].slice(0, old[tabIndex].length - 1);
|
||||||
|
return newFuture;
|
||||||
|
});
|
||||||
|
setPast((old) => {
|
||||||
|
let newPast = cloneDeep(old);
|
||||||
|
newPast[tabIndex] = old[tabIndex];
|
||||||
|
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
||||||
|
return newPast;
|
||||||
|
});
|
||||||
|
setNodes(futureState.nodes);
|
||||||
|
setEdges(futureState.edges);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
future,
|
||||||
|
past,
|
||||||
|
setFuture,
|
||||||
|
setPast,
|
||||||
|
setNodes,
|
||||||
|
setEdges,
|
||||||
|
getNodes,
|
||||||
|
getEdges,
|
||||||
|
future,
|
||||||
|
tabIndex,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// this effect is used to attach the global event handlers
|
||||||
|
if (!defaultOptions.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]);
|
||||||
|
return (
|
||||||
|
<undoRedoContext.Provider
|
||||||
|
value={{
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
takeSnapshot,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</undoRedoContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -25,9 +25,9 @@ import { typesContext } from "../../../../contexts/typesContext";
|
||||||
import { APIClassType } from "../../../../types/api";
|
import { APIClassType } from "../../../../types/api";
|
||||||
import { FlowType, NodeType } from "../../../../types/flow";
|
import { FlowType, NodeType } from "../../../../types/flow";
|
||||||
import { isValidConnection } from "../../../../utils";
|
import { isValidConnection } from "../../../../utils";
|
||||||
import useUndoRedo from "../../hooks/useUndoRedo";
|
|
||||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||||
import ExtraSidebar from "../extraSidebarComponent";
|
import ExtraSidebar from "../extraSidebarComponent";
|
||||||
|
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
genericNode: GenericNode,
|
genericNode: GenericNode,
|
||||||
|
|
@ -47,7 +47,7 @@ export default function Page({ flow }: { flow: FlowType }) {
|
||||||
useContext(typesContext);
|
useContext(typesContext);
|
||||||
const reactFlowWrapper = useRef(null);
|
const reactFlowWrapper = useRef(null);
|
||||||
|
|
||||||
const { takeSnapshot } = useUndoRedo();
|
const { takeSnapshot } = useContext(undoRedoContext);
|
||||||
|
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [lastSelection, setLastSelection] =
|
const [lastSelection, setLastSelection] =
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,16 @@ import { APIClassType, APIObjectType } from "../../../../types/api";
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||||
import { Code, Code2, FileDown, FileUp, Import, Save } from "lucide-react";
|
import { Code, Code2, FileDown, FileUp, Import, Save } from "lucide-react";
|
||||||
|
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||||
|
import ImportModal from "../../../../modals/importModal";
|
||||||
|
import ExportModal from "../../../../modals/exportModal";
|
||||||
|
import ApiModal from "../../../../modals/ApiModal";
|
||||||
|
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||||
|
|
||||||
export default function ExtraSidebar() {
|
export default function ExtraSidebar() {
|
||||||
const { data } = useContext(typesContext);
|
const { data } = useContext(typesContext);
|
||||||
|
const { openPopUp } = useContext(PopUpContext);
|
||||||
|
const { flows, tabId } = useContext(TabsContext);
|
||||||
const [dataFilter, setFilterData] = useState(data);
|
const [dataFilter, setFilterData] = useState(data);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
|
@ -49,6 +56,10 @@ export default function ExtraSidebar() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSaveFlow(current_flow: any) {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col overflow-auto scrollbar-hide h-full border-r">
|
<div className="flex flex-col overflow-auto scrollbar-hide h-full border-r">
|
||||||
<div className="mt-2 w-full flex gap-2 justify-between px-2 items-center">
|
<div className="mt-2 w-full flex gap-2 justify-between px-2 items-center">
|
||||||
|
|
@ -56,9 +67,10 @@ export default function ExtraSidebar() {
|
||||||
<button
|
<button
|
||||||
className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center rounded-md bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center rounded-md bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
openPopUp(<ImportModal />);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FileDown className="w-5 h-5 dark:text-gray-300"></FileDown>
|
<FileUp className="w-5 h-5 dark:text-gray-300"></FileUp>
|
||||||
</button>
|
</button>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
|
|
||||||
|
|
@ -67,10 +79,10 @@ export default function ExtraSidebar() {
|
||||||
className={classNames("hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
className={classNames("hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
openPopUp(<ExportModal />);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FileUp className="w-5 h-5 dark:text-gray-300"></FileUp>
|
<FileDown className="w-5 h-5 dark:text-gray-300"></FileDown>
|
||||||
</button>
|
</button>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
<ShadTooltip delayDuration={1000} content="Code" side="top">
|
<ShadTooltip delayDuration={1000} content="Code" side="top">
|
||||||
|
|
@ -78,7 +90,7 @@ export default function ExtraSidebar() {
|
||||||
className={classNames("hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
className={classNames("hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
openPopUp(<ApiModal flow={flows.find((f) => f.id === tabId)} />);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Code2 className="w-5 h-5 dark:text-gray-300"></Code2>
|
<Code2 className="w-5 h-5 dark:text-gray-300"></Code2>
|
||||||
|
|
@ -88,7 +100,9 @@ export default function ExtraSidebar() {
|
||||||
<ShadTooltip delayDuration={1000} content="Save" side="top">
|
<ShadTooltip delayDuration={1000} content="Save" side="top">
|
||||||
<button
|
<button
|
||||||
className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center transition-all shadow-sm duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center transition-all shadow-sm duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
||||||
|
onClick={(event) => {
|
||||||
|
handleSaveFlow(flows.find((f) => f.id === tabId));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Save className="w-5 h-5 dark:text-gray-300"></Save>
|
<Save className="w-5 h-5 dark:text-gray-300"></Save>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
|
||||||
import { Edge, Node, useReactFlow } from "reactflow";
|
|
||||||
import { TabsContext } from "../../../contexts/tabsContext";
|
|
||||||
import { cloneDeep } from "lodash";
|
|
||||||
|
|
||||||
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 { tabId, flows } = useContext(TabsContext);
|
|
||||||
|
|
||||||
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
|
||||||
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// whenever the flows variable changes, we need to add one array to the past and future states
|
|
||||||
setPast((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
|
||||||
setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
|
||||||
}, [flows]);
|
|
||||||
|
|
||||||
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
|
|
||||||
|
|
||||||
const takeSnapshot = useCallback(() => {
|
|
||||||
// push the current graph to the past state
|
|
||||||
setPast((old) => {
|
|
||||||
let newPast = cloneDeep(old);
|
|
||||||
newPast[tabIndex] = old[tabIndex].slice(
|
|
||||||
old[tabIndex].length - maxHistorySize + 1,
|
|
||||||
old[tabIndex].length
|
|
||||||
);
|
|
||||||
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
|
||||||
return newPast;
|
|
||||||
});
|
|
||||||
|
|
||||||
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
|
|
||||||
setFuture((old) => {
|
|
||||||
let newFuture = cloneDeep(old);
|
|
||||||
newFuture[tabIndex] = [];
|
|
||||||
return newFuture;
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
getNodes,
|
|
||||||
getEdges,
|
|
||||||
past,
|
|
||||||
future,
|
|
||||||
tabId,
|
|
||||||
setPast,
|
|
||||||
setFuture,
|
|
||||||
maxHistorySize,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tabIndex = flows.findIndex((f) => f.id === tabId);
|
|
||||||
|
|
||||||
const undo = useCallback(() => {
|
|
||||||
// get the last state that we want to go back to
|
|
||||||
const pastState = past[tabIndex][past[tabIndex].length - 1];
|
|
||||||
|
|
||||||
if (pastState) {
|
|
||||||
// first we remove the state from the history
|
|
||||||
setPast((old) => {
|
|
||||||
let newPast = cloneDeep(old);
|
|
||||||
newPast[tabIndex] = old[tabIndex].slice(0, old[tabIndex].length - 1);
|
|
||||||
return newPast;
|
|
||||||
});
|
|
||||||
// we store the current graph for the redo operation
|
|
||||||
setFuture((old) => {
|
|
||||||
let newFuture = cloneDeep(old);
|
|
||||||
newFuture[tabIndex] = old[tabIndex];
|
|
||||||
newFuture[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
|
||||||
return newFuture;
|
|
||||||
});
|
|
||||||
// now we can set the graph to the past state
|
|
||||||
setNodes(pastState.nodes);
|
|
||||||
setEdges(pastState.edges);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
setNodes,
|
|
||||||
setEdges,
|
|
||||||
getNodes,
|
|
||||||
getEdges,
|
|
||||||
future,
|
|
||||||
past,
|
|
||||||
setFuture,
|
|
||||||
setPast,
|
|
||||||
tabIndex,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const redo = useCallback(() => {
|
|
||||||
const futureState = future[tabIndex][future[tabIndex].length - 1];
|
|
||||||
|
|
||||||
if (futureState) {
|
|
||||||
setFuture((old) => {
|
|
||||||
let newFuture = cloneDeep(old);
|
|
||||||
newFuture[tabIndex] = old[tabIndex].slice(0, old[tabIndex].length - 1);
|
|
||||||
return newFuture;
|
|
||||||
});
|
|
||||||
setPast((old) => {
|
|
||||||
let newPast = cloneDeep(old);
|
|
||||||
newPast[tabIndex] = old[tabIndex];
|
|
||||||
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
|
||||||
return newPast;
|
|
||||||
});
|
|
||||||
setNodes(futureState.nodes);
|
|
||||||
setEdges(futureState.edges);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
future,
|
|
||||||
past,
|
|
||||||
setFuture,
|
|
||||||
setPast,
|
|
||||||
setNodes,
|
|
||||||
setEdges,
|
|
||||||
getNodes,
|
|
||||||
getEdges,
|
|
||||||
future,
|
|
||||||
tabIndex,
|
|
||||||
]);
|
|
||||||
|
|
||||||
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;
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue