Files
supabase/apps/studio/hooks/ui/usePreventNavigationOnUnsavedChanges.ts
Gildas Garcia d571c7404a chore: refactor <PreventNavigationOnUnsavedChanges> by extracting a usePreventNavigationOnUnsavedChanges hook (#43900)
## Problem

We want `ui-patterns` to contain only visual components but
https://github.com/supabase/supabase/pull/43577 introduced a component
with logic about navigation

## Solution

Extract the logic part into a hook and use it in the component.
Next step will be to remove this component and use
`<DiscardChangesConfirmationDialog>` directly along the hook where
needed

## How to test

Check that the fixes from #43577 still work:

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-20 09:27:20 +01:00

92 lines
2.7 KiB
TypeScript

import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { useStaticEffectEvent } from '../useStaticEffectEvent'
import { BASE_PATH } from '@/lib/constants'
interface UsePreventNavigationOnUnsavedChangesOptions {
/*
* Boolean indicating whether there are changes that would be lost if users navigate to another
* page or close the browser tab
*/
hasChanges: boolean
}
interface UsePreventNavigationOnUnsavedChangesReturn {
/*
* Cancel the navigation and keep the changes
*/
handleCancel: () => void
/*
* Confirm the navigation and lose the changes
*/
handleConfirm: () => void
/*
* Boolean indicating whether UI to request users confirmation for the navigation should be
* displayed
*/
shouldConfirm: boolean
}
/*
* Hook that prevents navigation when users could lose their changes.
* It prevents both NextJS and browser navigation (such as when closing the tab)
*/
export const usePreventNavigationOnUnsavedChanges = ({
hasChanges,
}: UsePreventNavigationOnUnsavedChangesOptions): UsePreventNavigationOnUnsavedChangesReturn => {
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 = useStaticEffectEvent(() => {
setNavigateUrl(undefined)
})
const handleConfirm = useStaticEffectEvent(() => {
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 useMemo(
() => ({
handleCancel,
handleConfirm,
shouldConfirm: !!navigateUrl,
}),
[navigateUrl, handleCancel, handleConfirm]
)
}