Home / Function/ TemplateEditor() — supabase Function Reference

TemplateEditor() — supabase Function Reference

Architecture documentation for the TemplateEditor() function in TemplateEditor.tsx from the supabase codebase.

Entity Profile

Relationship Graph

Source Code

apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx lines 37–381

export const TemplateEditor = ({ template }: TemplateEditorProps) => {
  const { ref: projectRef } = useParams()
  const { can: canUpdateConfig } = useAsyncCheckPermissions(
    PermissionAction.UPDATE,
    'custom_config_gotrue'
  )
  const editorRef = useRef<editor.IStandaloneCodeEditor>()

  // [Joshen] Error state is handled in the parent
  const { data: authConfig, isSuccess } = useAuthConfigQuery({ projectRef })

  const { mutate: validateSpam } = useValidateSpamMutation()

  const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation({
    onError: (error) => {
      setIsSavingTemplate(false)
      toast.error(`Failed to update email templates: ${error.message}`)
    },
  })

  const { id, properties } = template

  const messageSlug = `MAILER_TEMPLATES_${id}_CONTENT` as keyof typeof authConfig
  const messageProperty = properties[messageSlug]
  const builtInSMTP =
    isSuccess &&
    authConfig &&
    (!authConfig.SMTP_HOST || !authConfig.SMTP_USER || !authConfig.SMTP_PASS)

  const [validationResult, setValidationResult] = useState<ValidateSpamResponse>()
  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 INITIAL_VALUES = useMemo(() => {
    const result: { [x: string]: string } = {}
    Object.keys(properties).forEach((key) => {
      result[key] = ((authConfig && authConfig[key as keyof typeof authConfig]) ?? '') as string
    })
    return result
  }, [authConfig, properties])

  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
    // its state is kept separately from the form state, hence why we manually inject it here
    delete payload[messageSlug]
    if (messageProperty) payload[messageSlug] = bodyValue

    const [subjectKey] = Object.keys(properties)

    validateSpam(
      {
        projectRef,
        template: {
          subject: payload[subjectKey],
          content: payload[messageSlug],
        },
      },
      {
        onSuccess: (res) => {
          setValidationResult(res)
          const spamRules = (res?.rules ?? []).filter((rule) => rule.score > 0)
          const preventSaveFromSpamCheck = builtInSMTP && spamRules.length > 0

          if (preventSaveFromSpamCheck) {
            setIsSavingTemplate(false)
            toast.error(
              'Please rectify all spam warnings before saving while using the built-in email service'
            )
          } else {
            updateAuthConfig(
              { projectRef: projectRef, config: payload },
              {
                onSuccess: () => {
                  setIsSavingTemplate(false)
                  setHasUnsavedChanges(false) // Reset the unsaved changes state
                  toast.success('Successfully updated email template')
                },
              }
            )
          }
        },
        onError: () => setIsSavingTemplate(false),
      }
    )
  }

  // Single useMemo hook to parse and prepare message variables
  const messageVariables = useMemo(() => {
    if (!messageProperty?.description) return []

    // Parse bullet point format: - `{{ .Variable }}` : Description
    const lines = messageProperty.description.split('\n')
    const variables: { variable: string; description: string }[] = []

    for (const line of lines) {
      // Match lines that start with a bullet point followed by a variable in the format {{ .Variable }}
      // Handle variations in formatting (with or without backticks, different spacing)
      const match = line.match(/-\s*`?({{\s*\.\w+\s*}})`?\s*(?::|-)?\s*(.+)/)
      if (match && match[1] && match[2]) {
        variables.push({
          variable: match[1].replace(/`/g, '').trim(),
          description: match[2].trim(),
        })
      }
    }

    return variables
  }, [messageProperty?.description])

  // Check if form values have changed
  const formValues = form.watch()
  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) => {
    if (!editorRef.current) return

    const editor = editorRef.current
    const selection = editor.getSelection()

    if (selection) {
      const range = {
        startLineNumber: selection.startLineNumber,
        startColumn: selection.startColumn,
        endLineNumber: selection.endLineNumber,
        endColumn: selection.endColumn,
      }

      editor.executeEdits('insert-variable', [
        {
          range,
          text,
          forceMoveMarkers: true,
        },
      ])

      // Focus the editor after insertion
      editor.focus()
    }
  }

  // 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 (
    <Form_Shadcn_ {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <CardContent>
          {Object.keys(properties).map((x: string) => {
            const property = properties[x]
            if (property.type === 'string' && x !== messageSlug) {
              return (
                <FormField_Shadcn_
                  key={x}
                  control={form.control}
                  name={x}
                  render={({ field }) => (
                    <FormItemLayout
                      className="gap-y-3"
                      layout="vertical"
                      label={property.title}
                      description={
                        property.description ? (
                          <ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
                            {property.description}
                          </ReactMarkdown>
                        ) : null
                      }
                      labelOptional={
                        property.descriptionOptional ? (
                          <ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
                            {property.descriptionOptional}
                          </ReactMarkdown>
                        ) : null
                      }
                    >
                      <FormControl_Shadcn_>
                        <Input_Shadcn_ id={x} {...field} disabled={!canUpdateConfig} />
                      </FormControl_Shadcn_>
                    </FormItemLayout>
                  )}
                />
              )
            }
            return null
          })}
        </CardContent>

        {messageProperty && (
          <>
            <CardContent className="flex flex-col gap-4">
              <div className="flex items-center justify-between gap-2">
                <Label_Shadcn_>Body</Label_Shadcn_>
                <TwoOptionToggle
                  width={60}
                  options={['preview', 'source']}
                  activeOption={activeView}
                  onClickOption={(option: 'source' | 'preview') => setActiveView(option)}
                  borderOverride="border-muted"
                />
              </div>
              {activeView === 'source' ? (
                <>
                  <div className="overflow-hidden rounded-md border dark:border-control overflow-hidden [&_.monaco-editor]:outline-0 [&_.monaco-editor-background]:!bg-surface-200/30 [&_.monaco-editor_.margin]:!bg-surface-200/30 dark:[&_.monaco-editor-background]:!bg-surface-300 dark:[&_.monaco-editor_.margin]:!bg-surface-300">
                    <CodeEditor
                      id="code-id"
                      language="html"
                      isReadOnly={!canUpdateConfig}
                      className="!mb-0 relative h-96 outline-none outline-offset-0 outline-width-0 outline-0"
                      onInputChange={(e: string | undefined) => {
                        setBodyValue(e ?? '')
                        if (bodyValue !== e) setHasUnsavedChanges(true)
                      }}
                      options={{ wordWrap: 'on', contextmenu: false, padding: { top: 16 } }}
                      value={bodyValue}
                      editorRef={editorRef}
                    />
                  </div>
                  {messageVariables.length > 0 && (
                    <div className="flex flex-wrap gap-1">
                      {messageVariables.map(({ variable, description }) => (
                        <Tooltip key={variable}>
                          <TooltipTrigger asChild>
                            <Button
                              type="outline"
                              size="tiny"
                              className="rounded-full"
                              onClick={() => insertTextAtCursor(variable)}
                            >
                              {variable}
                            </Button>
                          </TooltipTrigger>
                          <TooltipContent side="top">
                            <p>{description || 'Variable description not available'}</p>
                          </TooltipContent>
                        </Tooltip>
                      ))}
                    </div>
                  )}
                </>
              ) : (
                <>
                  <iframe
                    className="!mb-0 mt-0 overflow-hidden h-96 w-full rounded-md border bg-white"
                    title={id}
                    srcDoc={bodyValue}
                    sandbox="allow-scripts allow-forms"
                  />
                  <Admonition
                    type="default"
                    title="Email rendering may differ"
                    description="The preview shown here may differ slightly from how your email appears in the recipient’s email client."
                  />
                </>
              )}
            </CardContent>

            <SpamValidation spamRules={spamRules} />

            <CardFooter className="flex flex-row justify-end gap-2">
              {hasChanges && (
                <Button
                  type="default"
                  onClick={() => {
                    form.reset(INITIAL_VALUES)
                    setBodyValue((authConfig && authConfig[messageSlug]) ?? '')
                    setHasUnsavedChanges(false)
                  }}
                >
                  Cancel
                </Button>
              )}
              <Button
                type="primary"
                htmlType="submit"
                disabled={!canUpdateConfig || isSavingTemplate || !hasChanges}
                loading={isSavingTemplate}
              >
                Save changes
              </Button>
            </CardFooter>
          </>
        )}
      </form>
    </Form_Shadcn_>
  )
}

Subdomains

Analyze Your Own Codebase

Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.

Try Supermodel Free