Files
supabase/apps/studio/components/ui-patterns/Dialogs/PreventNavigationOnUnsavedChanges.tsx
Gildas Garcia 87581e02cb fix: Prevent users from loosing changes when navigating away (#43577)
## Problem

When editing email templates or edge functions, users may navigate away
from the page without a warning indicating they may loose their changes.

This is because we only handle the `beforeunload` event when we should
also handle NextJS routing events.
This is actually done for the observability reports.

## Solution

Extract the logic from the observability reports into a reusable
component and use it where needed

## How to test

On staging, for each case:
- Authentication email templates
- Observability reports
- Edge functions creation
- Edge functions edition

Test:
- Modify the template/report/function
- Navigate away using either the sidebar link, the browser back button
or closing the tab
- Cancel navigation in the confirmation dialog
- Navigation should be prevented and you should not loose your changes

Test:
- Modify the template/report/function
- Navigate away using either the sidebar link, the browser back button
or closing the tab
- Confirm navigation in the confirmation dialog
- Navigation should not be prevented and you should have lost your
changes
2026-03-10 15:10:47 +01:00

71 lines
2.0 KiB
TypeScript

import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { BASE_PATH } from 'lib/constants'
import {
DiscardChangesConfirmationDialog,
type DiscardChangesConfirmationDialogProps,
} from './DiscardChangesConfirmationDialog'
export const PreventNavigationOnUnsavedChanges = ({
hasChanges,
...props
}: { hasChanges: boolean } & Omit<
DiscardChangesConfirmationDialogProps,
'visible' | 'onClose' | 'onCancel'
>) => {
const router = useRouter()
const [navigateUrl, setNavigateUrl] = useState<string>()
const [confirmNavigate, setConfirmNavigate] = useState(false)
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasChanges) {
e.preventDefault()
e.returnValue = '' // deprecated, but older browsers still require this
}
}
const handleBrowseAway = (url: string) => {
if (hasChanges && !confirmNavigate) {
setNavigateUrl(url)
throw 'Route change declined' // Just to prevent the route change
return
}
setNavigateUrl(undefined)
}
window.addEventListener('beforeunload', handleBeforeUnload)
router.events.on('routeChangeStart', handleBrowseAway)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload)
router.events.off('routeChangeStart', handleBrowseAway)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [confirmNavigate, hasChanges])
const handleCancel = () => {
setNavigateUrl(undefined)
}
const handleClose = () => {
setConfirmNavigate(true)
let urlToNavigate = navigateUrl ?? '/'
if (BASE_PATH && urlToNavigate.startsWith(BASE_PATH)) {
urlToNavigate = urlToNavigate.slice(BASE_PATH.length) || '/'
}
if (!urlToNavigate.startsWith('/')) urlToNavigate = `/${urlToNavigate}`
setNavigateUrl(undefined)
router.push(urlToNavigate)
}
return (
<DiscardChangesConfirmationDialog
visible={!!navigateUrl}
onCancel={handleCancel}
onClose={handleClose}
{...props}
/>
)
}