fix(studio): migrate email template reset dialog to async AlertDialog (#47705)

## What kind of change does this PR introduce?

Bug fix / refactor. Resolves DEPR-573.

## What is the current behavior?

`ResetTemplateDialog` (added in #45572) confirms the Auth email template
reset using the old `AlertDialog` workaround: an `AlertDialogAction`
with `asChild` + `event.preventDefault()` and a manual loading `Button`,
driven by `mutate` plus inline callbacks. Reset failures are only
reported via a toast from the mutation's default `onError`, so the error
disappears from the dialog context.

This predates #45960, which added first-class async handling to
`AlertDialogAction` (promise-returning handlers, controlled `loading`,
and `AlertDialogBody` for inline feedback). #45960 explicitly flagged
`ResetTemplateDialog` as needing this follow-up migration.

## What is the new behavior?

`ResetTemplateDialog` now uses the async `AlertDialogAction` pattern:

- The confirm handler uses `mutateAsync` and returns the reset promise,
so the dialog stays open with a loading state while the mutation is
pending and closes only after it succeeds.
- Reset failures surface inline via a destructive `Admonition` inside
`AlertDialogBody`, and the mutation's toast-only error path is
suppressed (`onError: () => {}`). The inline error clears when the
dialog closes.
- `Cancel` is disabled while the reset is in flight.
- The `asChild` + `preventDefault()` workaround and the manual loading
`Button` are removed; `loading={isResetting}` is retained for
parent-controlled loading.

This matches the established usage in `DisablePipelinesDialog` /
`JitDbAccessDeleteDialog` and the design-system
`alert-dialog-async-error` example.

## To test

- [ ] Customise an Auth email template, click **Reset template**,
confirm the dialog shows loading until the reset succeeds and then
closes with the editor refreshed to the default subject/body.
- [ ] In DevTools → Network, block `*/templates/*/reset`, click
**Reset**, and confirm the dialog stays open with an inline destructive
admonition and no toast.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved email template reset error handling by showing reset failures
inline in the confirmation dialog (with a destructive alert message).
* The dialog remains open on reset failure so users can review the error
and retry.
* “Cancel” is disabled while resetting; success behavior and existing
success toast behavior remain unchanged.
* **Tests**
* Updated reset mutation mock to use async behavior and added coverage
for reset failure UI/error handling (including that error toasts are not
triggered).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-07-08 14:43:31 +08:00
1 parent 511a7806de
commit fa20667ec1
2 files changed
+62 -17

No files matched your search

@@ -5,6 +5,7 @@ import { toast } from 'sonner'
import {
AlertDialog,
AlertDialogAction,
AlertDialogBody,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
@@ -14,6 +15,7 @@ import {
AlertDialogTrigger,
Button,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { type AuthTemplate } from './EmailTemplates.types'
import { getAuthTemplateType } from './EmailTemplates.utils'
@@ -32,6 +34,7 @@ export const ResetTemplateDialog = ({
}) => {
const { ref: projectRef } = useParams()
const [open, setOpen] = useState(false)
const [error, setError] = useState<string | null>(null)
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
@@ -40,25 +43,40 @@ export const ResetTemplateDialog = ({
const { id } = template
const templateType = getAuthTemplateType(id)
const { mutate: resetAuthTemplate, isPending: isResetting } = useAuthTemplateResetMutation()
const { mutateAsync: resetAuthTemplate, isPending: isResetting } = useAuthTemplateResetMutation({
onSuccess: (config) => {
toast.success('Email template reset to default')
onResetSuccess(config)
},
onError: () => {},
})
const resetTemplateToDefault = async () => {
if (!projectRef) throw new Error('Project ref is required')
if (!templateType) throw new Error('Template type is required')
resetAuthTemplate(
{ projectRef, template: templateType },
{
onSuccess: (config) => {
toast.success('Email template reset to default')
onResetSuccess(config)
},
}
)
setError(null)
try {
await resetAuthTemplate({ projectRef, template: templateType })
} catch (error) {
setError(
error instanceof Error
? error.message
: 'An unknown error occurred while resetting the template'
)
throw error
}
}
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialog
open={open}
onOpenChange={(next) => {
setOpen(next)
if (!next) setError(null)
}}
>
<AlertDialogTrigger asChild>
<Button variant="default" type="button" disabled={!canUpdateConfig}>
Reset template
@@ -73,15 +91,21 @@ export const ResetTemplateDialog = ({
: 'This will remove your custom subject line and email body content. The default values will be used instead.'}
</AlertDialogDescription>
</AlertDialogHeader>
{error && (
<AlertDialogBody>
<Admonition
type="destructive"
title="Unable to reset email template"
description={error}
/>
</AlertDialogBody>
)}
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isResetting}>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="warning"
loading={isResetting}
onClick={(e) => {
e.preventDefault()
resetTemplateToDefault()
}}
onClick={resetTemplateToDefault}
>
Reset
</AlertDialogAction>
@@ -124,7 +124,10 @@ const renderTemplateEditor = ({
})
useAsyncCheckPermissionsMock.mockReturnValue({ can: canUpdateConfig })
useAuthConfigUpdateMutationMock.mockReturnValue({ mutate: updateAuthConfigMock })
useAuthTemplateResetMutationMock.mockReturnValue({ mutate: resetTemplateMock, isPending: false })
useAuthTemplateResetMutationMock.mockImplementation((options = {}) => ({
mutateAsync: (vars: unknown) => resetTemplateMock(vars, options),
isPending: false,
}))
return render(<TemplateEditor template={confirmationTemplate} />)
}
@@ -253,4 +256,22 @@ describe('TemplateEditor reset to default', () => {
expect(screen.getByRole('button', { name: 'Reset template' })).toBeDisabled()
})
it('keeps the dialog open and shows an inline error when reset fails', async () => {
const user = userEvent.setup()
resetTemplateMock.mockImplementation(async () => {
throw new Error('Reset endpoint unavailable')
})
renderTemplateEditor({ hasCustomBody: true })
await user.click(screen.getByRole('button', { name: 'Reset template' }))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'Reset' }))
expect(await within(dialog).findByText('Reset endpoint unavailable')).toBeInTheDocument()
expect(within(dialog).getByText('Unable to reset email template')).toBeInTheDocument()
expect(within(dialog).getByRole('button', { name: 'Reset' })).toBeInTheDocument()
expect(toast.error).not.toHaveBeenCalled()
})
})