Files
supabase/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx
T
Danny WhiteandJoshen Lim f45a6085f4 feat(studio): add auth email template reset flow (#45572)
## What kind of change does this PR introduce?

Feature. Resolves AUTH-879.

## What is the current behavior?

Studio does not provide a way to reset an Auth email template back to
the default subject and body once it has been customised.

## What is the new behavior?

Studio shows a `Reset template` action when Platform reports that the
selected Auth email template subject or body has been customised. The
action opens a confirmation dialog, calls the dedicated Platform reset
endpoint, and refreshes the editor with the default subject and body
returned by the API.

The Auth config save/reset mutations now run their user-facing success
handling before refreshing Auth lint data, so the success toast and
local editor cleanup are not delayed by lint refetches.

## Additional context

Depends on supabase/platform#32417.



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

* **New Features**
* Reset email templates to defaults via a confirmation dialog; button
appears when custom content is detected and respects update permissions.
* Typed email-template definitions and improved template variable
display and descriptions.

* **Tests**
* Added tests covering template reset visibility, confirmation flow,
state updates, permission handling, and toast notifications.

* **Documentation**
* Example email template placeholders updated for internationalization
and provider snippets.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45572)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-05-18 14:33:26 +10:00

216 lines
7.4 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { groupBy } from 'lodash'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Card,
CardContent,
CardFooter,
Form,
FormControl,
FormField,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
PageSection,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import z from 'zod'
import { useProfileIdentitiesQuery } from '@/data/profile/profile-identities-query'
import { useProfileUpdateMutation } from '@/data/profile/profile-update-mutation'
import { useProfile } from '@/lib/profile'
const FormSchema = z.object({
first_name: z.string().optional(),
last_name: z.string().optional(),
username: z.string().optional(),
primary_email: z.string().email().optional(),
})
const formId = 'profile-information-form'
export const ProfileInformation = () => {
const { profile } = useProfile()
const {
data: identityData,
isPending: isIdentitiesLoading,
isSuccess: isIdentitiesSuccess,
} = useProfileIdentitiesQuery()
const identities = (identityData?.identities ?? []).filter((x) => x.identity_data?.email !== null)
const dedupedIdentityEmails = Object.keys(groupBy(identities, 'identity_data.email'))
const defaultValues = {
first_name: profile?.first_name ?? '',
last_name: profile?.last_name ?? '',
username: profile?.username ?? '',
primary_email: profile?.primary_email ?? '',
}
const form = useForm({
resolver: zodResolver(FormSchema),
defaultValues,
values: defaultValues,
})
const { mutate: updateProfile, isPending: isUpdatingProfile } = useProfileUpdateMutation({
onSuccess: (data) => {
toast.success('Successfully saved profile')
const { first_name, last_name, username, primary_email } = data
form.reset({
first_name: first_name ?? undefined,
last_name: last_name ?? undefined,
username,
primary_email,
})
},
onError: (error) => toast.error(`Failed to update profile: ${error.message}`),
})
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => {
updateProfile({
firstName: data.first_name || '',
lastName: data.last_name || '',
username: data.username || '',
primaryEmail: data.primary_email || '',
})
}
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Profile information</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...form}>
<form id={formId} className="space-y-6 w-full" onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardContent>
<FormField
control={form.control}
name="first_name"
render={({ field }) => (
<FormItemLayout label="First name" layout="flex-row-reverse">
<FormControl className="col-span-8">
<Input {...field} placeholder="First name" className="w-full" />
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={form.control}
name="last_name"
render={({ field }) => (
<FormItemLayout label="Last name" layout="flex-row-reverse">
<FormControl className="col-span-8">
<Input {...field} placeholder="Last name" className="w-full" />
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={form.control}
name="primary_email"
render={({ field }) => (
<FormItemLayout
label="Primary email"
description={
profile?.is_sso_user
? 'Managed by your SSO provider and cannot be changed here'
: 'Used for account notifications'
}
layout="flex-row-reverse"
>
<FormControl className="col-span-8">
<div className="flex flex-col gap-1">
<Select
value={field.value}
onValueChange={field.onChange}
disabled={profile?.is_sso_user}
>
<SelectTrigger className="col-span-8 w-full">
<SelectValue placeholder="Select primary email" />
</SelectTrigger>
<SelectContent className="col-span-8">
{isIdentitiesSuccess &&
dedupedIdentityEmails.map((email) => (
<SelectItem key={email} value={email}>
{email}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItemLayout
label="Username"
description={
profile?.is_sso_user
? 'Managed by your SSO provider and cannot be changed here'
: 'Display name used across dashboard'
}
layout="flex-row-reverse"
>
<FormControl className="col-span-8">
<div className="flex flex-col gap-1">
<Input
{...field}
className="w-full"
placeholder="Username"
disabled={profile?.is_sso_user}
/>
</div>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
loading={isUpdatingProfile || isIdentitiesLoading}
disabled={!form.formState.isDirty}
>
Save
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
)
}