From 9e6c236b1755c8ae24f4098b8bca5c268b9d8540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Kl=C3=A9ri?= <4449-balintbrews@users.noreply.drupalcode.org> Date: Mon, 10 Mar 2025 13:21:59 +0000 Subject: [PATCH] Issue #3511888 by balintbrews, jessebaker, wim leers: Recover from server-side errors that may happen during rendering preview --- dictionary.txt | 1 + ui/src/components/UndoRedo.tsx | 34 +++++------------------- ui/src/features/canvas/Canvas.tsx | 4 +++ ui/src/hooks/useUndoRedo.ts | 44 +++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 27 deletions(-) create mode 100644 ui/src/hooks/useUndoRedo.ts diff --git a/dictionary.txt b/dictionary.txt index 1a32fec5e9..dea2f73aea 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 313fbcec3a..c640bb417f 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 0f6137184f..914fc70d46 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 0000000000..d92ab6c0b4 --- /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, + }; +} -- GitLab