mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 20:35:07 +03:00
## 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>
67 lines
1.4 KiB
TypeScript
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]
|
|
)
|
|
}
|