diff --git a/cypress/e2e/workflows/changeDetection.cy.ts b/cypress/e2e/workflows/changeDetection.cy.ts index 4e9b09d0..96838a7b 100644 --- a/cypress/e2e/workflows/changeDetection.cy.ts +++ b/cypress/e2e/workflows/changeDetection.cy.ts @@ -59,3 +59,29 @@ it('detects no changes when the workflow is saved', () => { }).click(); cy.findByTestId('saveRedDot').should('not.exist'); }); + +it('prompts a warning when leaving an unsaved workflow', () => { + cy.get('@node').click(); + cy.findByRole('textbox', { name: 'Edit label' }) + .click() + .type('Always and forever...'); + + cy.get('@node').click(); + + cy.findByRole('button', { + name: 'Save workflow to server: changes pending', + }).should('exist'); + + cy.window().then((win) => cy.spy(win, 'confirm').as('confirm')); + + cy.findByRole('link', { name: 'Monitor' }).click(); + cy.get('@confirm').should('have.been.calledOnce'); + + // We should be able to navigate back without a second warning prompt + cy.findAllByRole('link', { name: 'Edit' }).first().click(); + cy.location().should((loc) => { + expect(loc.pathname).to.eq('/edit'); + expect(loc.search).to.eq('?workflow=tutorial_Graph'); + }); + cy.get('@confirm').should('have.been.calledOnce'); +}); diff --git a/src/App.tsx b/src/App.tsx index 48132590..7a58c645 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import SimpleSnackbar from './general/Snackbar'; import MonitorRoute from './MonitorRoute'; import NavBar from './navbar/NavBar'; import SocketClientProvider from './SocketClientProvider'; +import { useLocationWithWarningPrompt } from './edition/hooks'; const queryClient = new QueryClient(); @@ -14,7 +15,10 @@ export default function App() { return ( - + diff --git a/src/edition/hooks.ts b/src/edition/hooks.ts index c79b222f..7240666e 100644 --- a/src/edition/hooks.ts +++ b/src/edition/hooks.ts @@ -2,12 +2,46 @@ import { useReactFlow } from '@xyflow/react'; import type { RefCallback } from 'react'; import { useCallback, useState } from 'react'; -import { useNodesIds } from '../store/graph-hooks'; +import { useNodesIds, useWorkflowHasChanges } from '../store/graph-hooks'; import useNodeDataStore from '../store/useNodeDataStore'; import type { RFNode } from '../types'; import { getNodeData } from '../utils'; import { assertDefined, assertNodeDataDefined } from '../utils/typeGuards'; import { generateNewNodeId } from './utils'; +import { type navigate, useBrowserLocation } from 'wouter/use-browser-location'; +import { type Path } from 'wouter'; +import { useEventListener } from '@react-hookz/web'; + +export function useLocationWithWarningPrompt(): [Path, typeof navigate] { + const [location, setLocation] = useBrowserLocation(); + const workflowHasChanges = useWorkflowHasChanges(); + + const displayWarning = location === '/edit' && workflowHasChanges; + + useEventListener(window, 'beforeunload', (event: BeforeUnloadEvent) => { + if (displayWarning) { + event.preventDefault(); + } + }); + + return [ + location, + (newLocation, options) => { + if (!displayWarning) { + setLocation(newLocation, options); + return; + } + // eslint-disable-next-line no-alert + const perfomNavigation = window.confirm( + 'There are unsaved changes that will be lost when navigating to another page. Do you want to continue?', + ); + + if (perfomNavigation) { + setLocation(newLocation, options); + } + }, + ]; +} export function useCloneNode() { const rfInstance = useReactFlow();