diff --git a/dictionary.txt b/dictionary.txt index 1a32fec5e984f4fcaff2c252cb4d0915c9167e05..dea2f73aeade74df8f21041bfd438364f952de0c 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -42,6 +42,7 @@ Postel propless propsify Pryce's +redoable reduxjs renderify rightbar diff --git a/ui/src/components/UndoRedo.tsx b/ui/src/components/UndoRedo.tsx index 313fbcec3a6109e61c34ecc0d385f06ff909d94e..c640bb417f3f2b300ee424a40078d497ecb0cafc 100644 --- a/ui/src/components/UndoRedo.tsx +++ b/ui/src/components/UndoRedo.tsx @@ -1,33 +1,13 @@ -// cspell:ignore redoable +import { useEffect } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { Button } from '@radix-ui/themes'; import { ResetIcon } from '@radix-ui/react-icons'; -import { useAppDispatch, useAppSelector } from '@/app/hooks'; -import { selectLayoutHistory } from '@/features/layout/layoutModelSlice'; -import { selectPageDataHistory } from '@/features/pageData/pageDataSlice'; -import { useHotkeys } from 'react-hotkeys-hook'; -import { useEffect } from 'react'; -import { UndoRedoActionCreators } from '@/features/ui/uiSlice'; -import { selectUndoType, selectRedoType } from '@/features/ui/uiSlice'; -import clsx from 'clsx'; import styles from '@/components/topbar/Topbar.module.css'; +import { useUndoRedo } from '@/hooks/useUndoRedo'; const UndoRedo = () => { - const dispatch = useAppDispatch(); - const layoutModel = useAppSelector(selectLayoutHistory); - const pageData = useAppSelector(selectPageDataHistory); - const undoType = useAppSelector(selectUndoType); - const redoType = useAppSelector(selectRedoType); - const isUndoable = layoutModel.past.length > 1 || pageData.past.length > 1; - const isRedoable = - layoutModel.future.length > 0 || pageData.future.length > 0; - const dispatchUndo = () => - isUndoable && undoType - ? dispatch(UndoRedoActionCreators.undo(undoType)) - : null; - const dispatchRedo = () => - isRedoable && redoType - ? dispatch(UndoRedoActionCreators.redo(redoType)) - : null; + const { isUndoable, isRedoable, dispatchUndo, dispatchRedo } = useUndoRedo(); + // The useHotKeys hook listens to the parent document. useHotkeys('mod+z', () => dispatchUndo()); // 'mod' listens for cmd on Mac and ctrl on Windows. useHotkeys(['meta+shift+z', 'ctrl+y'], () => dispatchRedo()); // Mac redo is cmd+shift+z, Windows redo is ctrl+y. @@ -55,7 +35,7 @@ const UndoRedo = () => { variant="ghost" color="gray" size="2" - className={clsx(styles.topBarButton)} + className={styles.topBarButton} onClick={() => dispatchUndo()} disabled={!isUndoable} aria-label="Undo" @@ -66,7 +46,7 @@ const UndoRedo = () => { variant="ghost" color="gray" size="2" - className={clsx(styles.topBarButton)} + className={styles.topBarButton} onClick={() => dispatchRedo()} disabled={!isRedoable} aria-label="Redo" diff --git a/ui/src/features/canvas/Canvas.tsx b/ui/src/features/canvas/Canvas.tsx index 0f6137184f2f9ac5d8b83d9af28a5f1ab64fa801..914fc70d46d57f43f5ef7eb734aeb4f6c77b34cb 100644 --- a/ui/src/features/canvas/Canvas.tsx +++ b/ui/src/features/canvas/Canvas.tsx @@ -23,6 +23,7 @@ import { deleteNode } from '../layout/layoutModelSlice'; import useCopyPasteComponents from '@/hooks/useCopyPasteComponents'; import { useNavigationUtils } from '@/hooks/useNavigationUtils'; import useXbParams from '@/hooks/useXbParams'; +import { useUndoRedo } from '@/hooks/useUndoRedo'; const Canvas = () => { const dispatch = useAppDispatch(); @@ -44,6 +45,7 @@ const Canvas = () => { const middleMouseDownRef = useRef(middleMouseDown); const { copySelectedComponent, pasteAfterSelectedComponent } = useCopyPasteComponents(); + const { isUndoable, dispatchUndo } = useUndoRedo(); useHotkeys(['NumpadAdd', 'Equal'], () => dispatch(canvasViewPortZoomIn())); useHotkeys(['Minus', 'NumpadSubtract'], () => @@ -257,6 +259,8 @@ const Canvas = () => { <ErrorBoundary title="An unexpected error has occurred while rendering preview." variant="alert" + onReset={isUndoable ? dispatchUndo : undefined} + resetButtonText={isUndoable ? 'Undo last action' : undefined} > <Preview /> </ErrorBoundary> diff --git a/ui/src/hooks/useUndoRedo.ts b/ui/src/hooks/useUndoRedo.ts new file mode 100644 index 0000000000000000000000000000000000000000..d92ab6c0b41b656fbfcc4360bcdb7b038e54a154 --- /dev/null +++ b/ui/src/hooks/useUndoRedo.ts @@ -0,0 +1,44 @@ +import { useAppDispatch, useAppSelector } from '@/app/hooks'; +import { + UndoRedoActionCreators, + selectUndoType, + selectRedoType, +} from '@/features/ui/uiSlice'; +import { selectLayoutHistory } from '@/features/layout/layoutModelSlice'; +import { selectPageDataHistory } from '@/features/pageData/pageDataSlice'; + +interface UndoRedoState { + isUndoable: boolean; + isRedoable: boolean; + dispatchUndo: () => void; + dispatchRedo: () => void; +} + +export function useUndoRedo(): UndoRedoState { + const dispatch = useAppDispatch(); + const layoutModel = useAppSelector(selectLayoutHistory); + const pageData = useAppSelector(selectPageDataHistory); + const undoType = useAppSelector(selectUndoType); + const redoType = useAppSelector(selectRedoType); + + const isUndoable = layoutModel.past.length > 1 || pageData.past.length > 1; + const isRedoable = + layoutModel.future.length > 0 || pageData.future.length > 0; + + const dispatchUndo = () => + isUndoable && undoType + ? dispatch(UndoRedoActionCreators.undo(undoType)) + : null; + + const dispatchRedo = () => + isRedoable && redoType + ? dispatch(UndoRedoActionCreators.redo(redoType)) + : null; + + return { + isUndoable, + isRedoable, + dispatchUndo, + dispatchRedo, + }; +}