Authentication
@@ -149,11 +149,8 @@ export const EmailTemplates = () => {
)}
-
-
+
+
@@ -191,7 +188,7 @@ export const EmailTemplates = () => {
)}
-
+
{
href={`/project/${projectRef}/auth/templates/${templateSlug}`}
className="py-6 pr-6"
>
-
+
@@ -271,7 +265,7 @@ export const EmailTemplates = () => {
)}
-
+ >
)}
)
diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx
index 89d69e61ed8f1..7853be1b2d7e0 100644
--- a/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx
+++ b/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx
@@ -1,66 +1,66 @@
-import { Check, MailWarning } from 'lucide-react'
+import { AnimatePresence, motion } from 'framer-motion'
import { Markdown } from 'components/interfaces/Markdown'
import { ValidateSpamResponse } from 'data/auth/validate-spam-mutation'
-import { Separator, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
+import { CardContent, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
+import { Admonition } from 'ui-patterns'
interface SpamValidationProps {
- validationResult?: ValidateSpamResponse
+ spamRules?: ValidateSpamResponse['rules']
}
// [Joshen] According to API, we label as a spam risk as long as there are spam
// rules identified with scores above 0. Scores are irrelevant in our context and
// are hence not visualized in the UI
-export const SpamValidation = ({ validationResult }: SpamValidationProps) => {
- const spamRules = (validationResult?.rules ?? []).filter((rule) => rule.score >= 0)
- const hasSpamWarning = spamRules.length > 0
+export const SpamValidation = ({ spamRules = [] }: SpamValidationProps) => {
+ const rules = spamRules.filter((rule) => rule.score >= 0)
return (
-
-
- {hasSpamWarning ? (
-
- ) : (
-
- )}
-
-
-
- {hasSpamWarning
- ? 'Email has a high probability of being marked as spam - review issues below to improve deliverability.'
- : 'Email content is unlikely to be marked as spam'}
-
- {hasSpamWarning && (
- <>
-
-
-
-
- Warning
- Description
-
-
-
- {spamRules.map((rule) => (
-
-
- {rule.name}
-
- {rule.desc}
+
+ {rules.length > 0 && (
+
+
+
+
+
+
+
+
+
+ Warning
+ Description
- ))}
-
-
+
+
+ {rules.map((rule) => (
+
+ {rule.name}
+ {rule.desc}
+
+ ))}
+
+
+
+
-
-
- >
- )}
-
-
+
+
+ )}
+
)
}
diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx
index 47334435239c0..045bb4f370448 100644
--- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx
+++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx
@@ -1,5 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { Code, Monitor } from 'lucide-react'
import { editor } from 'monaco-editor'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useForm } from 'react-hook-form'
@@ -8,6 +7,7 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
+import TwoOptionToggle from 'components/ui/TwoOptionToggle'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useValidateSpamMutation, ValidateSpamResponse } from 'data/auth/validate-spam-mutation'
@@ -22,10 +22,6 @@ import {
FormField_Shadcn_,
Input_Shadcn_,
Label_Shadcn_,
- Tabs_Shadcn_,
- TabsContent_Shadcn_,
- TabsList_Shadcn_,
- TabsTrigger_Shadcn_,
Tooltip,
TooltipContent,
TooltipTrigger,
@@ -44,16 +40,12 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
PermissionAction.UPDATE,
'custom_config_gotrue'
)
-
- // Add a ref to the code editor
const editorRef = useRef
()
// [Joshen] Error state is handled in the parent
const { data: authConfig, isSuccess } = useAuthConfigQuery({ projectRef })
- const { mutate: validateSpam } = useValidateSpamMutation({
- onSuccess: (res) => setValidationResult(res),
- })
+ const { mutate: validateSpam } = useValidateSpamMutation()
const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation({
onError: (error) => {
@@ -75,11 +67,10 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
const [bodyValue, setBodyValue] = useState((authConfig && authConfig[messageSlug]) ?? '')
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const [isSavingTemplate, setIsSavingTemplate] = useState(false)
+ const [activeView, setActiveView] = useState<'source' | 'preview'>('source')
const spamRules = (validationResult?.rules ?? []).filter((rule) => rule.score > 0)
- const preventSaveFromSpamCheck = builtInSMTP && spamRules.length > 0
- // Create form values
const INITIAL_VALUES = useMemo(() => {
const result: { [x: string]: string } = {}
Object.keys(properties).forEach((key) => {
@@ -88,27 +79,13 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
return result
}, [authConfig, properties])
- // Setup React Hook Form
- const form = useForm({
- defaultValues: INITIAL_VALUES,
- })
-
- // Update form values when authConfig changes
- useEffect(() => {
- if (authConfig) {
- const values: { [key: string]: string } = {}
- Object.keys(properties).forEach((key) => {
- values[key] = ((authConfig && authConfig[key as keyof typeof authConfig]) ?? '') as string
- })
- form.reset(values)
- setBodyValue((authConfig && authConfig[messageSlug]) ?? '')
- }
- }, [authConfig, properties, messageSlug, form])
+ const form = useForm({ defaultValues: INITIAL_VALUES })
const onSubmit = (values: any) => {
if (!projectRef) return console.error('Project ref is required')
setIsSavingTemplate(true)
+
const payload = { ...values }
// Because the template content uses the code editor which is not a form component
@@ -128,6 +105,7 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
},
{
onSuccess: (res) => {
+ setValidationResult(res)
const spamRules = (res?.rules ?? []).filter((rule) => rule.score > 0)
const preventSaveFromSpamCheck = builtInSMTP && spamRules.length > 0
@@ -142,8 +120,8 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
{
onSuccess: () => {
setIsSavingTemplate(false)
- toast.success('Successfully updated settings')
setHasUnsavedChanges(false) // Reset the unsaved changes state
+ toast.success('Successfully updated email template')
},
}
)
@@ -154,36 +132,6 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
)
}
- useEffect(() => {
- const handleBeforeUnload = (e: BeforeUnloadEvent) => {
- if (hasUnsavedChanges) {
- e.preventDefault()
- e.returnValue = '' // deprecated, but older browsers still require this
- }
- }
-
- window.addEventListener('beforeunload', handleBeforeUnload)
-
- return () => {
- window.removeEventListener('beforeunload', handleBeforeUnload)
- }
- }, [hasUnsavedChanges])
-
- useEffect(() => {
- if (projectRef && id && !!authConfig) {
- const [subjectKey] = Object.keys(properties)
-
- validateSpam({
- projectRef,
- template: {
- subject: authConfig[subjectKey as keyof typeof authConfig] as string,
- content: authConfig[messageSlug],
- },
- })
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [id])
-
// Single useMemo hook to parse and prepare message variables
const messageVariables = useMemo(() => {
if (!messageProperty?.description) return []
@@ -209,8 +157,10 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
// Check if form values have changed
const formValues = form.watch()
- const hasFormChanges = JSON.stringify(formValues) !== JSON.stringify(INITIAL_VALUES)
- const hasChanges = hasFormChanges || ((authConfig && authConfig[messageSlug]) ?? '') !== bodyValue
+ const baselineValues = INITIAL_VALUES
+ const baselineBodyValue = (authConfig && authConfig[messageSlug]) ?? ''
+ const hasFormChanges = JSON.stringify(formValues) !== JSON.stringify(baselineValues)
+ const hasChanges = hasFormChanges || baselineBodyValue !== bodyValue
// Function to insert text at cursor position
const insertTextAtCursor = (text: string) => {
@@ -240,6 +190,52 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => {
}
}
+ // Update form values when authConfig changes
+ useEffect(() => {
+ if (authConfig) {
+ const values: { [key: string]: string } = {}
+ Object.keys(properties).forEach((key) => {
+ values[key] = ((authConfig && authConfig[key as keyof typeof authConfig]) ?? '') as string
+ })
+ form.reset(values)
+ setBodyValue((authConfig && authConfig[messageSlug]) ?? '')
+ }
+ }, [authConfig, properties, messageSlug, form])
+
+ useEffect(() => {
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ if (hasUnsavedChanges) {
+ e.preventDefault()
+ e.returnValue = '' // deprecated, but older browsers still require this
+ }
+ }
+
+ window.addEventListener('beforeunload', handleBeforeUnload)
+
+ return () => {
+ window.removeEventListener('beforeunload', handleBeforeUnload)
+ }
+ }, [hasUnsavedChanges])
+
+ useEffect(() => {
+ if (projectRef && id && !!authConfig) {
+ const [subjectKey] = Object.keys(properties)
+
+ validateSpam({
+ projectRef,
+ template: {
+ subject: authConfig[subjectKey as keyof typeof authConfig] as string,
+ content: authConfig[messageSlug],
+ },
+ })
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [id])
+
+ useEffect(() => {
+ if (!hasChanges) setValidationResult(undefined)
+ }, [hasChanges])
+
return (