[templateId].tsx285 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import Link from 'next/link'
5import { useRouter } from 'next/router'
6import { useEffect } from 'react'
7import { useForm } from 'react-hook-form'
8import { toast } from 'sonner'
9import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui'
10import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
11import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
12import { PageContainer } from 'ui-patterns/PageContainer'
13import {
14 PageHeader,
15 PageHeaderAside,
16 PageHeaderBreadcrumb,
17 PageHeaderDescription,
18 PageHeaderMeta,
19 PageHeaderSummary,
20 PageHeaderTitle,
21} from 'ui-patterns/PageHeader'
22import {
23 PageSection,
24 PageSectionContent,
25 PageSectionMeta,
26 PageSectionSummary,
27 PageSectionTitle,
28} from 'ui-patterns/PageSection'
29import {
30 BreadcrumbItem,
31 BreadcrumbLink,
32 BreadcrumbList,
33 BreadcrumbPage,
34 BreadcrumbSeparator,
35} from 'ui/src/components/shadcn/ui/breadcrumb'
36import * as z from 'zod'
37
38import { TEMPLATES_SCHEMAS } from '@/components/interfaces/Auth/EmailTemplates/AuthTemplatesValidation'
39import { slugifyTitle } from '@/components/interfaces/Auth/EmailTemplates/EmailTemplates.utils'
40import { TemplateEditor } from '@/components/interfaces/Auth/EmailTemplates/TemplateEditor'
41import AuthLayout from '@/components/layouts/AuthLayout/AuthLayout'
42import { DefaultLayout } from '@/components/layouts/DefaultLayout'
43import { DocsButton } from '@/components/ui/DocsButton'
44import { NoPermission } from '@/components/ui/NoPermission'
45import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
46import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
47import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
48import { DOCS_URL } from '@/lib/constants'
49import type { NextPageWithLayout } from '@/types'
50
51const TemplatePage: NextPageWithLayout = () => {
52 return <RedirectToTemplates />
53}
54
55const RedirectToTemplates = () => {
56 const router = useRouter()
57 const { templateId, ref } = router.query
58 const { ref: projectRef } = useParams()
59
60 const { can: canReadAuthSettings, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
61 PermissionAction.READ,
62 'custom_config_gotrue'
63 )
64
65 const { can: canUpdateConfig } = useAsyncCheckPermissions(
66 PermissionAction.UPDATE,
67 'custom_config_gotrue'
68 )
69
70 const { data: authConfig, isPending: isLoadingConfig } = useAuthConfigQuery({ projectRef })
71
72 const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({
73 onError: (error) => {
74 toast.error(`Failed to update settings: ${error?.message}`)
75 },
76 onSuccess: () => {
77 toast.success('Successfully updated settings')
78 },
79 })
80
81 // Find template whose slug matches the URL slug
82 const template =
83 templateId && typeof templateId === 'string'
84 ? TEMPLATES_SCHEMAS.find((template) => slugifyTitle(template.title) === templateId)
85 : null
86
87 // Convert templateId slug to one lowercase word to match docs anchor tag
88 const templateIdForDocs =
89 typeof templateId === 'string' ? templateId.replace(/-/g, '').toLowerCase() : ''
90
91 // Determine if this is a security notification template
92 const isSecurityTemplate = template?.misc?.emailTemplateType === 'security'
93
94 // Get the enabled key for security templates
95 const templateEnabledKey = isSecurityTemplate
96 ? (`MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED` as string)
97 : null
98
99 const showConfigurationSection = isSecurityTemplate && templateEnabledKey
100
101 // Create form schema for security templates
102 const TemplateFormSchema = templateEnabledKey
103 ? z.object({
104 [templateEnabledKey]: z.boolean(),
105 })
106 : z.object({})
107
108 const defaultValues = templateEnabledKey
109 ? {
110 [templateEnabledKey]: authConfig
111 ? Boolean(authConfig[templateEnabledKey as keyof typeof authConfig])
112 : false,
113 }
114 : {}
115
116 const templateForm = useForm<z.infer<typeof TemplateFormSchema>>({
117 resolver: zodResolver(TemplateFormSchema as any),
118 defaultValues,
119 })
120
121 const onSubmit = (values: z.infer<typeof TemplateFormSchema>) => {
122 if (!projectRef) return console.error('Project ref is required')
123 updateAuthConfig({ projectRef: projectRef, config: { ...values }, skipInvalidation: true })
124 }
125
126 useEffect(() => {
127 if (authConfig && templateEnabledKey) {
128 templateForm.reset({
129 [templateEnabledKey]: Boolean(authConfig[templateEnabledKey as keyof typeof authConfig]),
130 })
131 }
132 // eslint-disable-next-line react-hooks/exhaustive-deps
133 }, [authConfig, templateEnabledKey])
134
135 if (isPermissionsLoaded && !canReadAuthSettings) {
136 return <NoPermission isFullPage resourceText="access your project's email settings" />
137 }
138
139 if (!templateId) {
140 return null
141 }
142
143 // Show error if templateId is invalid or template is not found
144 if (!template) {
145 return (
146 <div className="flex h-full w-full items-center justify-center">
147 <Admonition
148 className="max-w-md"
149 type="default"
150 title="Unable to find template"
151 description={`${templateId ? `The template "${templateId}"` : 'This template'} doesn’t seem to exist.`}
152 >
153 <Button asChild type="default" className="mt-2">
154 <Link href={`/project/${ref}/auth/templates`}>Head back</Link>
155 </Button>
156 </Admonition>
157 </div>
158 )
159 }
160
161 return (
162 <>
163 <PageHeader size="default">
164 <PageHeaderBreadcrumb>
165 <BreadcrumbList>
166 <BreadcrumbItem>
167 <BreadcrumbLink asChild>
168 <Link href={`/project/${ref}/auth/templates`}>Emails</Link>
169 </BreadcrumbLink>
170 </BreadcrumbItem>
171 <BreadcrumbSeparator />
172 <BreadcrumbItem>
173 <BreadcrumbPage>{template.title}</BreadcrumbPage>
174 </BreadcrumbItem>
175 </BreadcrumbList>
176 </PageHeaderBreadcrumb>
177 <PageHeaderMeta>
178 <PageHeaderSummary>
179 <PageHeaderTitle>{template.title}</PageHeaderTitle>
180 <PageHeaderDescription>
181 {template.purpose || 'Configure and customize email templates.'}
182 </PageHeaderDescription>
183 </PageHeaderSummary>
184 <PageHeaderAside>
185 <DocsButton
186 href={`${DOCS_URL}/guides/local-development/customizing-email-templates#${isSecurityTemplate ? 'security' : 'auth'}emailtemplate${templateIdForDocs}`}
187 />
188 </PageHeaderAside>
189 </PageHeaderMeta>
190 </PageHeader>
191 <PageContainer size="default" className="pb-16">
192 {!isPermissionsLoaded || isLoadingConfig ? (
193 <PageSection>
194 <PageSectionContent>
195 <GenericSkeletonLoader />
196 </PageSectionContent>
197 </PageSection>
198 ) : (
199 <>
200 {showConfigurationSection && (
201 <PageSection>
202 <PageSectionMeta>
203 <PageSectionSummary>
204 <PageSectionTitle>Configuration</PageSectionTitle>
205 </PageSectionSummary>
206 </PageSectionMeta>
207 <PageSectionContent>
208 <Form {...templateForm}>
209 <form onSubmit={templateForm.handleSubmit(onSubmit)} className="space-y-4">
210 <Card>
211 <CardContent>
212 <FormField
213 control={templateForm.control}
214 name={templateEnabledKey as keyof z.infer<typeof TemplateFormSchema>}
215 render={({ field }) => (
216 <FormItemLayout
217 layout="flex-row-reverse"
218 label="Enable notification"
219 description="Send this email to users when triggered"
220 >
221 <FormControl>
222 <Switch
223 checked={field.value}
224 onCheckedChange={field.onChange}
225 disabled={!canUpdateConfig}
226 />
227 </FormControl>
228 </FormItemLayout>
229 )}
230 />
231 </CardContent>
232 <CardFooter className="justify-end space-x-2">
233 {templateForm.formState.isDirty && (
234 <Button type="default" onClick={() => templateForm.reset()}>
235 Cancel
236 </Button>
237 )}
238 <Button
239 type="primary"
240 htmlType="submit"
241 disabled={
242 !canUpdateConfig ||
243 isUpdatingConfig ||
244 !templateForm.formState.isDirty
245 }
246 loading={isUpdatingConfig}
247 >
248 Save changes
249 </Button>
250 </CardFooter>
251 </Card>
252 </form>
253 </Form>
254 </PageSectionContent>
255 </PageSection>
256 )}
257
258 <PageSection>
259 {showConfigurationSection && (
260 <PageSectionMeta>
261 <PageSectionSummary>
262 <PageSectionTitle>Content</PageSectionTitle>
263 </PageSectionSummary>
264 </PageSectionMeta>
265 )}
266 <PageSectionContent>
267 <Card>
268 <TemplateEditor template={template} />
269 </Card>
270 </PageSectionContent>
271 </PageSection>
272 </>
273 )}
274 </PageContainer>
275 </>
276 )
277}
278
279TemplatePage.getLayout = (page) => (
280 <DefaultLayout>
281 <AuthLayout title="Emails">{page}</AuthLayout>
282 </DefaultLayout>
283)
284
285export default TemplatePage