Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/admin-x-framework/src/api/automated-emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export interface AutomatedEmailsVerifyResponseType extends AutomatedEmailsRespon
meta?: Meta & {email_verified: string};
}

export type AutomatedEmailPreview = {
html: string;
plaintext: string;
subject: string;
}

export interface AutomatedEmailsPreviewResponseType {
automated_emails: AutomatedEmailPreview[];
}

const dataType = 'AutomatedEmailsResponseType';

export const useBrowseAutomatedEmails = createQuery<AutomatedEmailsResponseType>({
Expand Down Expand Up @@ -90,3 +100,9 @@ export const useSendTestWelcomeEmail = createMutation<unknown, {id: string; emai
path: ({id}) => `/automated_emails/${id}/test/`,
body: ({email, subject, lexical}) => ({email, subject, lexical})
});

export const usePreviewWelcomeEmail = createMutation<AutomatedEmailsPreviewResponseType, {id: string; subject: string; lexical: string}>({
method: 'POST',
path: ({id}) => `/automated_emails/${id}/preview/`,
body: ({subject, lexical}) => ({subject, lexical})
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,26 @@ import React from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';

import MemberEmailEditor from './member-email-editor';
import WelcomeEmailPreviewFrame from './welcome-email-preview-frame';
import {Hint, Button as LegacyButton, Modal, TextField} from '@tryghost/admin-x-design-system';
import {confirmIfDirty} from '@tryghost/admin-x-design-system';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {getWelcomeEmailValidationErrors, useWelcomeEmailPreview} from '../../../../hooks/use-welcome-email-preview';
import {useBrowseAutomatedEmails, useEditAutomatedEmail, usePreviewWelcomeEmail} from '@tryghost/admin-x-framework/api/automated-emails';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useWelcomeEmailSenderDetails} from '../../../../hooks/use-welcome-email-sender-details';

import TestEmailDropdown from './test-email-dropdown';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useBrowseAutomatedEmails, useEditAutomatedEmail} from '@tryghost/admin-x-framework/api/automated-emails';
import {useGlobalData} from '../../../../components/providers/global-data-provider';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import type {AutomatedEmail} from '@tryghost/admin-x-framework/api/automated-emails';

import {Button} from '@tryghost/shade/components';
import {Button, Tabs, TabsList, TabsTrigger} from '@tryghost/shade/components';
import {cn} from '@tryghost/shade/utils';

interface EmailPreviewModalContentProps {
title: string;
centeredHeaderContent?: React.ReactNode;
headerActions?: React.ReactNode;
children: React.ReactNode;
className?: string;
Expand All @@ -28,7 +31,7 @@ interface EmailPreviewModalContentProps {
const EmailPreviewModalContent = React.forwardRef<
HTMLDivElement,
EmailPreviewModalContentProps
>(({title, headerActions, children, className}, ref) => (
>(({title, centeredHeaderContent, headerActions, children, className}, ref) => (
<div
ref={ref}
className={cn(
Expand All @@ -37,15 +40,18 @@ const EmailPreviewModalContent = React.forwardRef<
className
)}
>
<div className="sticky top-0 flex shrink-0 items-center justify-between border-b border-gray-200 bg-white px-5 py-3 dark:border-gray-900 dark:bg-gray-975">
<h3 className="text-xl font-semibold">
<div className="sticky top-0 grid shrink-0 grid-cols-[1fr_auto_1fr] items-center border-b border-gray-200 bg-white px-5 py-3 dark:border-gray-900 dark:bg-gray-975">
<h3 className="justify-self-start text-xl font-semibold">
{title}
</h3>
<div className="flex items-center gap-2">
<div className="justify-self-center">
{centeredHeaderContent}
</div>
<div className="flex items-center gap-2 justify-self-end">
{headerActions}
</div>
</div>
<div className="flex h-[clamp(0px,calc(100dvh-320px),82vh)] min-h-0 grow flex-col overflow-y-auto">
<div className="flex h-[clamp(0px,calc(100dvh-320px),82vh)] min-h-0 grow flex-col overflow-y-auto [scrollbar-gutter:stable]">
{children}
</div>
</div>
Expand Down Expand Up @@ -85,37 +91,16 @@ interface WelcomeEmailModalProps {
automatedEmail: AutomatedEmail;
}

const isEmptyLexical = (lexical: string | null | undefined): boolean => {
if (!lexical) {
return true;
}

try {
const parsed = JSON.parse(lexical);
const children = parsed?.root?.children;

// Empty if no children or only an empty paragraph
if (!children || children.length === 0) {
return true;
}
if (children.length === 1 &&
children[0].type === 'paragraph' &&
(!children[0].children || children[0].children.length === 0)) {
return true;
}

return false;
} catch {
return true;
}
};
type PreviewMode = 'edit' | 'preview';

const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType = 'free', automatedEmail}) => {
const modal = useModal();
const {updateRoute} = useRouting();
const {mutateAsync: editAutomatedEmail} = useEditAutomatedEmail();
const {mutateAsync: previewWelcomeEmail} = usePreviewWelcomeEmail();
const {data: automatedEmailsData} = useBrowseAutomatedEmails();
const [showTestDropdown, setShowTestDropdown] = useState(false);
const [mode, setMode] = useState<PreviewMode>('edit');
const dropdownRef = useRef<HTMLDivElement>(null);
const normalizedLexical = useRef<string>(automatedEmail?.lexical || '');
const hasEditorBeenFocused = useRef(false);
Expand All @@ -127,7 +112,7 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
const emailTypeLabel = emailType === 'paid' ? 'Paid' : 'Free';
const modalTitle = `${emailTypeLabel} members welcome email`;

const {formState, saveState, updateForm, setFormState, handleSave, okProps, errors, validate} = useForm({
const {formState, saveState, updateForm, setFormState, setErrors, handleSave, okProps, errors, validate} = useForm({
initialState: {
subject: automatedEmail?.subject || 'Welcome',
lexical: automatedEmail?.lexical || ''
Expand All @@ -137,21 +122,14 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
await editAutomatedEmail({...automatedEmail, ...state});
},
onSaveError: handleError,
onValidate: (state) => {
const newErrors: Record<string, string> = {};

if (!state.subject?.trim()) {
newErrors.subject = 'A subject is required';
}

if (isEmptyLexical(state.lexical)) {
newErrors.lexical = 'Email content is required';
}

return newErrors;
}
onValidate: getWelcomeEmailValidationErrors
});
const saveButtonLabel = okProps.label || 'Save';
const {previewFrameState, previewState, enterPreview, exitPreview} = useWelcomeEmailPreview({
automatedEmailId: automatedEmail.id,
previewWelcomeEmail,
setErrors
});

const isDirty = saveState === 'unsaved';

Expand Down Expand Up @@ -196,6 +174,16 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
};
}, []);

const handleModeChange = useCallback((nextMode: PreviewMode) => {
setMode(nextMode);

if (nextMode === 'preview') {
enterPreview(formState);
} else {
exitPreview();
}
}, [enterPreview, exitPreview, formState]);

// The editor normalizes content on mount (e.g., processing {name} templates),
// which triggers onChange even without user edits. We track whether the editor
// has ever been focused - normalization happens before focus is possible, so any
Expand Down Expand Up @@ -234,6 +222,19 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
width='full'
>
<EmailPreviewModalContent
centeredHeaderContent={
<Tabs
data-testid='welcome-email-mode-toggle'
value={mode}
variant='segmented-sm'
onValueChange={value => value && handleModeChange(value as PreviewMode)}
>
<TabsList className='bg-gray-100 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.04)]'>
<TabsTrigger data-testid='welcome-email-mode-edit' value='edit'>Edit</TabsTrigger>
<TabsTrigger data-testid='welcome-email-mode-preview' value='preview'>Preview</TabsTrigger>
</TabsList>
</Tabs>
}
className='dark:bg-[#151719]'
headerActions={
<>
Expand Down Expand Up @@ -283,22 +284,36 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
<div className='flex items-center'>
<div className='w-20 shrink-0 text-sm font-semibold'>Subject:</div>
<div className='grow'>
<TextField
className='w-full'
error={Boolean(errors.subject)}
hint={errors.subject || ''}
maxLength={300}
placeholder={`Welcome to ${siteTitle}`}
value={formState.subject}
onChange={e => updateForm(state => ({...state, subject: e.target.value}))}
/>
{mode === 'edit' ? (
<TextField
className='w-full'
error={Boolean(errors.subject)}
hint={errors.subject || ''}
maxLength={300}
placeholder={`Welcome to ${siteTitle}`}
value={formState.subject}
onChange={e => updateForm(state => ({...state, subject: e.target.value}))}
/>
) : (
<TextField
className='w-full cursor-default caret-transparent'
data-testid='welcome-email-preview-subject'
tabIndex={-1}
value={previewState.status === 'success' ? previewState.preview.subject : formState.subject}
readOnly
onFocus={e => e.currentTarget.blur()}
/>
)}
</div>
</div>
</div>
</EmailPreviewEmailHeader>
<EmailPreviewBody className={errors.lexical ? 'border border-red-500' : ''}>
<EmailPreviewBody className={mode === 'edit' && errors.lexical ? 'border border-red-500' : ''}>
<div
className='mx-auto w-full max-w-[600px] pt-10 pb-8 transition-[max-width,padding] duration-300 ease-out motion-reduce:transition-none'
className={cn(
'mx-auto w-full max-w-[600px] pt-10 pb-8 transition-[max-width,padding] duration-300 ease-out motion-reduce:transition-none',
mode === 'preview' && 'hidden'
)}
data-testid='welcome-email-editor'
onFocus={() => {
hasEditorBeenFocused.current = true;
Expand All @@ -308,13 +323,15 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
key={automatedEmail?.id || 'new'}
className='welcome-email-editor'
placeholder='Write your welcome email content...'

value={automatedEmail?.lexical || ''}
value={formState.lexical}
onChange={handleEditorChange}
/>
</div>
{mode === 'preview' && (
<WelcomeEmailPreviewFrame previewState={previewFrameState} />
)}
</EmailPreviewBody>
{errors.lexical && <Hint className='mt-2 max-w-[740px]' color='red'>{errors.lexical}</Hint>}
{mode === 'edit' && errors.lexical && <Hint className='mt-2 max-w-[740px]' color='red'>{errors.lexical}</Hint>}
</div>
</EmailPreviewModalContent>
</Modal>
Expand Down
Loading
Loading