diff --git a/apps/design-system/content/docs/components/switch.mdx b/apps/design-system/content/docs/components/switch.mdx index 3dcf332821f99..6e7ec2d1f7153 100644 --- a/apps/design-system/content/docs/components/switch.mdx +++ b/apps/design-system/content/docs/components/switch.mdx @@ -1,6 +1,6 @@ --- title: Switch -description: A control that allows the user to toggle between checked and not checked. +description: A control for toggling between unchecked and checked. component: true links: doc: https://www.radix-ui.com/docs/primitives/components/switch @@ -60,6 +60,26 @@ import { Switch } from '@/components/ui/switch' ``` +### Relative positioning + +When using Switch in custom flex layouts (especially with height constraints like `h-full`), you may need to add `relative` positioning to the parent container to ensure proper rendering of focus rings, form validation messages, and overflow. + +```tsx +
+ ( + + + + )} + /> +
+``` + +You don’t need to add `relative` manually when using FormItemLayout as it provides the necessary positioning context automatically. + ## Examples ### Form diff --git a/apps/docs/content/_partials/social_provider_setup.mdx b/apps/docs/content/_partials/social_provider_setup.mdx index 5af559543aef7..60b4a3dca689e 100644 --- a/apps/docs/content/_partials/social_provider_setup.mdx +++ b/apps/docs/content/_partials/social_provider_setup.mdx @@ -2,7 +2,7 @@ The next step requires a callback URL, which looks like this: `https:// diff --git a/apps/docs/content/guides/auth/social-login/auth-github.mdx b/apps/docs/content/guides/auth/social-login/auth-github.mdx index 8a5d35e971e29..cdb85b1bc1842 100644 --- a/apps/docs/content/guides/auth/social-login/auth-github.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-github.mdx @@ -10,7 +10,7 @@ To enable GitHub Auth for your project, you need to set up a GitHub OAuth applic Setting up GitHub logins for your application consists of 3 parts: -- Create and configure a GitHub OAuth App on [GitHub](https://github.com) +- Create and configure a GitHub OAuth App on [GitHub](https://github.com/settings/applications/new) - Add your GitHub OAuth keys to your [Supabase Project](/dashboard) - Add the login code to your [Supabase JS Client App](https://github.com/supabase/supabase-js) diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx index 26ddf458cbb89..24fde2379c740 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx @@ -120,14 +120,14 @@ export const EmailTemplates = () => { )} {isSuccess && ( -
+ <> {builtInSMTP ? ( -
+
) : null} {isSecurityNotificationsEnabled ? ( -
+
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 (
@@ -254,6 +250,8 @@ export const TemplateEditor = ({ template }: TemplateEditorProps) => { name={x} render={({ field }) => ( { {messageProperty && ( <> - - Message body - - - - - Source - - - - Preview - - - - { - setBodyValue(e ?? '') - if (bodyValue !== e) setHasUnsavedChanges(true) - }} - options={{ wordWrap: 'on', contextmenu: false, padding: { top: 16 } }} - value={bodyValue} - editorRef={editorRef} - /> + +
+ Body + setActiveView(option)} + borderOverride="border-muted" + /> +
+ {activeView === 'source' ? ( + <> +
+ { + setBodyValue(e ?? '') + if (bodyValue !== e) setHasUnsavedChanges(true) + }} + options={{ wordWrap: 'on', contextmenu: false, padding: { top: 16 } }} + value={bodyValue} + editorRef={editorRef} + /> +
{messageVariables.length > 0 && ( -
-
- {messageVariables.map(({ variable, description }) => ( - - - - - -

{description || 'Variable description not available'}

-
-
- ))} -
+
+ {messageVariables.map(({ variable, description }) => ( + + + + + +

{description || 'Variable description not available'}

+
+
+ ))}
)} - - + + ) : ( + <>