Files
supabase/apps/studio/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog.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

79 lines
2.0 KiB
TypeScript

'use client'
import { useCallback, useEffect, useRef, type ReactNode } from 'react'
import { type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from 'ui'
interface DiscardChangesConfirmationDialogProps extends ConfirmOnCloseModalProps {
title?: ReactNode
description?: ReactNode
confirmLabel?: ReactNode
cancelLabel?: ReactNode
}
export const DiscardChangesConfirmationDialog = ({
visible,
onClose,
onCancel,
title = 'Discard changes?',
description = 'Are you sure you want to discard your changes? Any unsaved changes will be lost.',
confirmLabel = 'Discard changes',
cancelLabel = 'Keep editing',
}: DiscardChangesConfirmationDialogProps) => {
const isConfirmingRef = useRef(false)
useEffect(() => {
if (visible) {
isConfirmingRef.current = false
}
}, [visible])
const handleConfirm = useCallback(() => {
isConfirmingRef.current = true
onClose()
}, [onClose])
const handleOpenChange = useCallback(
(open: boolean) => {
if (open) return
if (isConfirmingRef.current) {
isConfirmingRef.current = false
return
}
onCancel()
},
[onCancel]
)
return (
<AlertDialog open={visible} onOpenChange={handleOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
{description !== undefined && description !== null && (
<AlertDialogDescription>{description}</AlertDialogDescription>
)}
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{cancelLabel}</AlertDialogCancel>
<AlertDialogAction variant="danger" onClick={handleConfirm}>
{confirmLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}