From 7415c55ce76f1118dd58ebac0c0ee94adc76ef85 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 18 Jan 2023 16:43:11 -0500 Subject: [PATCH] do not navigate away from diagram editor page if there are changes w/ burnettk --- spiffworkflow-frontend/package-lock.json | 1 + spiffworkflow-frontend/package.json | 1 + .../src/components/ReactDiagramEditor.tsx | 8 +++ .../src/hooks/UsePrompt.tsx | 58 +++++++++++++++++++ .../src/routes/ProcessModelEditDiagram.tsx | 10 ++++ 5 files changed, 78 insertions(+) create mode 100644 spiffworkflow-frontend/src/hooks/UsePrompt.tsx diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index 8466f7a8..af05da5b 100644 --- a/spiffworkflow-frontend/package-lock.json +++ b/spiffworkflow-frontend/package-lock.json @@ -57,6 +57,7 @@ "react-icons": "^4.4.0", "react-jsonschema-form": "^1.8.1", "react-markdown": "^8.0.3", + "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "remark-gfm": "^3.0.1", diff --git a/spiffworkflow-frontend/package.json b/spiffworkflow-frontend/package.json index 515d191a..0b2d3805 100644 --- a/spiffworkflow-frontend/package.json +++ b/spiffworkflow-frontend/package.json @@ -53,6 +53,7 @@ "react-jsonschema-form": "^1.8.1", "react-markdown": "^8.0.3", "react-router-dom": "^6.3.0", + "react-router": "^6.3.0", "react-scripts": "^5.0.1", "remark-gfm": "^3.0.1", "serve": "^14.0.0", diff --git a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx index 54d63eb9..8e947fdc 100644 --- a/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx +++ b/spiffworkflow-frontend/src/components/ReactDiagramEditor.tsx @@ -84,6 +84,7 @@ type OwnProps = { onJsonFilesRequested?: (..._args: any[]) => any; onDmnFilesRequested?: (..._args: any[]) => any; onSearchProcessModels?: (..._args: any[]) => any; + onElementsChanged?: (..._args: any[]) => any; url?: string; }; @@ -109,6 +110,7 @@ export default function ReactDiagramEditor({ onJsonFilesRequested, onDmnFilesRequested, onSearchProcessModels, + onElementsChanged, url, }: OwnProps) { const [diagramXMLString, setDiagramXMLString] = useState(''); @@ -291,6 +293,11 @@ export default function ReactDiagramEditor({ diagramModeler.on('element.click', (element: any) => { handleElementClick(element); }); + diagramModeler.on('elements.changed', (event: any) => { + if (onElementsChanged) { + onElementsChanged(event); + } + }); diagramModeler.on('spiff.service_tasks.requested', (event: any) => { handleServiceTasksRequested(event); @@ -330,6 +337,7 @@ export default function ReactDiagramEditor({ onJsonFilesRequested, onDmnFilesRequested, onSearchProcessModels, + onElementsChanged, ]); useEffect(() => { diff --git a/spiffworkflow-frontend/src/hooks/UsePrompt.tsx b/spiffworkflow-frontend/src/hooks/UsePrompt.tsx new file mode 100644 index 00000000..63b87361 --- /dev/null +++ b/spiffworkflow-frontend/src/hooks/UsePrompt.tsx @@ -0,0 +1,58 @@ +/** + * These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'. + * Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315 + * Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381 + */ + +import { useCallback, useContext, useEffect } from 'react'; +import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'; + +/** + * Blocks all navigation attempts. This is useful for preventing the page from + * changing until some condition is met, like saving form data. + * + * @param blocker + * @param when + * @see https://reactrouter.com/api/useBlocker + */ +export function useBlocker(blocker: any, when: any = true) { + const { navigator } = useContext(NavigationContext); + + useEffect(() => { + if (!when) return null; + + const unblock = (navigator as any).block((tx: any) => { + const autoUnblockingTx = { + ...tx, + retry() { + // Automatically unblock the transition so it can play all the way + // through before retrying it. TODO: Figure out how to re-enable + // this block if the transition is cancelled for some reason. + unblock(); + tx.retry(); + }, + }; + + blocker(autoUnblockingTx); + }); + + return unblock; + }, [navigator, blocker, when]); +} +/** + * Prompts the user with an Alert before they leave the current screen. + * + * @param message + * @param when + */ +export function usePrompt(message: any, when: any = true) { + const blocker = useCallback( + (tx: any) => { + // eslint-disable-next-line no-alert + if (window.confirm(message)) tx.retry(); + }, + [message] + ); + + useBlocker(blocker, when); +} diff --git a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx index f6af667e..af38d8bc 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelEditDiagram.tsx @@ -35,11 +35,13 @@ import { } from '../interfaces'; import ProcessSearch from '../components/ProcessSearch'; import { Notification } from '../components/Notification'; +import { usePrompt } from '../hooks/UsePrompt'; export default function ProcessModelEditDiagram() { const [showFileNameEditor, setShowFileNameEditor] = useState(false); const handleShowFileNameEditor = () => setShowFileNameEditor(true); const [processModel, setProcessModel] = useState(null); + const [diagramHasChanges, setDiagramHasChanges] = useState(false); const [scriptText, setScriptText] = useState(''); const [scriptType, setScriptType] = useState(''); @@ -112,6 +114,8 @@ export default function ProcessModelEditDiagram() { const processModelPath = `process-models/${modifiedProcessModelId}`; + usePrompt('Changes you made may not be saved.', diagramHasChanges); + useEffect(() => { // Grab all available process models in case we need to search for them. // Taken from the Process Group List @@ -206,6 +210,11 @@ export default function ProcessModelEditDiagram() { // after saving the file, make sure we null out newFileName // so it does not get used over the params setNewFileName(''); + setDiagramHasChanges(false); + }; + + const onElementsChanged = () => { + setDiagramHasChanges(true); }; const onDeleteFile = (fileName = params.file_name) => { @@ -922,6 +931,7 @@ export default function ProcessModelEditDiagram() { onLaunchDmnEditor={onLaunchDmnEditor} onDmnFilesRequested={onDmnFilesRequested} onSearchProcessModels={onSearchProcessModels} + onElementsChanged={onElementsChanged} /> ); };