Files
supabase/apps/studio/hooks/ui/useConfirmOnClose.tsx
Danny WhiteandJoshen Lim aef1d70351 chore(studio): standardise discard changes behaviour (#43201)
## What kind of change does this PR introduce?

UX consistency improvement. Updates DEPR-355.

## What is the current behavior?

Discard-confirm close behavioir is implemented inconsistently across
Studio forms:

- some sheets/dialogs used `useConfirmOnClose`
- some duplicated local `CloseConfirmationModal` components
- some (e.g. `CreateHookSheet`) closed unconditionally and could lose
unsaved changes

## What is the new behavior?

Extracts and validates a reusable discard-close pattern for
dialogs/sheets
- enhances `useConfirmOnClose` with `handleOpenChange(open)` for
`Dialog`/`Sheet` `onOpenChange`
- adds shared `DiscardChangesConfirmationDialog` (`AlertDialog`-based,
override-able copy)
- migrates:
	- `InviteMemberButton`
	- `CreateHookSheet`
	- `EditSecretSheet`

This standardizes close-guard behavior for
backdrop/escape/close-button/cancel-button flows without trying to block
route changes or arbitrary unmounts.

## Additional context

`CreateHookSheet` now also marks the generated secret action as dirty
(`setValue(..., { shouldDirty: true })`) so the discard guard behaves
correctly.

- Added tests for `useConfirmOnClose` covering:
	- clean vs dirty close
	- handleOpenChange(true|false)
	- confirm/cancel behavior
	- latest callback ref behavior

A follow-up PR is needed to migrate remaining duplicated
`CloseConfirmationModal` usages and older `useConfirmOnClose` call sites
to the shared `DiscardChangesConfirmationDialog` + `handleOpenChange`
pattern.

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-03-05 11:32:39 +11:00

67 lines
1.4 KiB
TypeScript

import { useCallback, useMemo, useState } from 'react'
import useLatest from '../misc/useLatest'
export interface ConfirmOnCloseModalProps {
visible: boolean
onClose: () => void
onCancel: () => void
}
interface UseConfirmOnCloseProps {
checkIsDirty: () => boolean
onClose: () => void
}
export const useConfirmOnClose = ({ checkIsDirty, onClose }: UseConfirmOnCloseProps) => {
const [visible, setVisible] = useState(false)
const checkIsDirtyRef = useLatest(checkIsDirty)
const onCloseRef = useLatest(onClose)
const confirmOnClose = useCallback(() => {
if (checkIsDirtyRef.current()) {
setVisible(true)
} else {
onCloseRef.current()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleOpenChange = useCallback(
(open: boolean) => {
if (!open) {
confirmOnClose()
}
},
[confirmOnClose]
)
const onConfirm = useCallback(() => {
setVisible(false)
onCloseRef.current()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onCancel = useCallback(() => {
setVisible(false)
}, [])
const modalProps: ConfirmOnCloseModalProps = useMemo(
() => ({
visible,
onClose: onConfirm,
onCancel,
}),
[visible, onConfirm, onCancel]
)
return useMemo(
() => ({
confirmOnClose,
handleOpenChange,
modalProps,
}),
[confirmOnClose, handleOpenChange, modalProps]
)
}