From 1452e9c7a219170abe00a41b76f86da486c0adcb Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 13 Jun 2023 11:41:03 -0300 Subject: [PATCH] added useUndoRedo to context and added Undo and Redo buttons to header --- src/frontend/src/App.tsx | 2 - .../components/menuBar/index.tsx | 18 ++ src/frontend/src/contexts/index.tsx | 21 +- src/frontend/src/contexts/undoRedoContext.tsx | 194 ++++++++++++++++++ .../components/PageComponent/index.tsx | 4 +- .../extraSidebarComponent/index.tsx | 24 ++- .../src/pages/FlowPage/hooks/useUndoRedo.ts | 181 ---------------- 7 files changed, 245 insertions(+), 199 deletions(-) create mode 100644 src/frontend/src/contexts/undoRedoContext.tsx delete mode 100644 src/frontend/src/pages/FlowPage/hooks/useUndoRedo.ts diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 22cd82280..60fa6d065 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -40,8 +40,6 @@ export default function App() { setSuccessOpen, } = useContext(alertContext); - const { flows, addFlow } = useContext(TabsContext); - // Initialize state variable for the list of alerts const [alertsList, setAlertsList] = useState< Array<{ diff --git a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx index 1c7e73a94..ccd6a1ccd 100644 --- a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx +++ b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx @@ -30,11 +30,13 @@ import ApiModal from "../../../../modals/ApiModal"; import { alertContext } from "../../../../contexts/alertContext"; import { updateFlowInDatabase } from "../../../../controllers/API"; import { Link } from "react-router-dom"; +import { undoRedoContext } from "../../../../contexts/undoRedoContext"; export const MenuBar = ({ setRename, rename, flows, tabId }) => { const { updateFlow, setTabId, addFlow } = useContext(TabsContext); const { setErrorData } = useContext(alertContext); const { openPopUp } = useContext(PopUpContext); + const { undo, redo } = useContext(undoRedoContext); function handleSaveFlow(flow) { try { @@ -121,6 +123,22 @@ export const MenuBar = ({ setRename, rename, flows, tabId }) => { Rename + { + undo(); + }} + > + + Undo + + { + redo(); + }} + > + + Redo + Flows - - - - - {children} - - - - + + + + + + {children} + + + + + diff --git a/src/frontend/src/contexts/undoRedoContext.tsx b/src/frontend/src/contexts/undoRedoContext.tsx new file mode 100644 index 000000000..573a260e0 --- /dev/null +++ b/src/frontend/src/contexts/undoRedoContext.tsx @@ -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(initialValue); + +export function UndoRedoProvider({ children }) { + const { tabId, flows } = useContext(TabsContext); + + const [past, setPast] = useState(flows.map(() => [])); + const [future, setFuture] = useState(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 ( + + {children} + + ); +} diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 7da4a4c16..a9012ca68 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -25,9 +25,9 @@ import { typesContext } from "../../../../contexts/typesContext"; import { APIClassType } from "../../../../types/api"; import { FlowType, NodeType } from "../../../../types/flow"; import { isValidConnection } from "../../../../utils"; -import useUndoRedo from "../../hooks/useUndoRedo"; import ConnectionLineComponent from "../ConnectionLineComponent"; import ExtraSidebar from "../extraSidebarComponent"; +import { undoRedoContext } from "../../../../contexts/undoRedoContext"; const nodeTypes = { genericNode: GenericNode, @@ -47,7 +47,7 @@ export default function Page({ flow }: { flow: FlowType }) { useContext(typesContext); const reactFlowWrapper = useRef(null); - const { takeSnapshot } = useUndoRedo(); + const { takeSnapshot } = useContext(undoRedoContext); const [position, setPosition] = useState({ x: 0, y: 0 }); const [lastSelection, setLastSelection] = diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index f5a067947..8e76b8ec6 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -12,9 +12,16 @@ import { APIClassType, APIObjectType } from "../../../../types/api"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import ShadTooltip from "../../../../components/ShadTooltipComponent"; 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() { const { data } = useContext(typesContext); + const { openPopUp } = useContext(PopUpContext); + const { flows, tabId } = useContext(TabsContext); const [dataFilter, setFilterData] = useState(data); const [search, setSearch] = useState(""); @@ -49,6 +56,10 @@ export default function ExtraSidebar() { }); } + function handleSaveFlow(current_flow: any) { + throw new Error("Function not implemented."); + } + return (
@@ -56,9 +67,10 @@ export default function ExtraSidebar() { @@ -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" )} onClick={(event) => { - + openPopUp(); }} > - + @@ -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" )} onClick={(event) => { - + openPopUp( f.id === tabId)} />); }} > @@ -88,7 +100,9 @@ export default function ExtraSidebar() { diff --git a/src/frontend/src/pages/FlowPage/hooks/useUndoRedo.ts b/src/frontend/src/pages/FlowPage/hooks/useUndoRedo.ts deleted file mode 100644 index 4d2a2f529..000000000 --- a/src/frontend/src/pages/FlowPage/hooks/useUndoRedo.ts +++ /dev/null @@ -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(flows.map(() => [])); - const [future, setFuture] = useState(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;