@@ -3,7 +3,7 @@ import NiceModal from '@ebay/nice-modal-react';
33import React from 'react' ;
44import TopLevelGroup from '../../top-level-group' ;
55import WelcomeEmailModal from './member-emails/welcome-email-modal' ;
6- import { Button , Separator , SettingGroupContent , Toggle , withErrorBoundary } from '@tryghost/admin-x-design-system' ;
6+ import { Separator , SettingGroupContent , Toggle , showToast , withErrorBoundary } from '@tryghost/admin-x-design-system' ;
77import { checkStripeEnabled , getSettingValues } from '@tryghost/admin-x-framework/api/settings' ;
88import { useAddAutomatedEmail , useBrowseAutomatedEmails , useEditAutomatedEmail } from '@tryghost/admin-x-framework/api/automated-emails' ;
99import { useGlobalData } from '../../providers/global-data-provider' ;
@@ -18,10 +18,18 @@ const DEFAULT_PAID_LEXICAL_CONTENT = '{"root":{"children":[{"children":[{"detail
1818
1919const EmailPreview : React . FC < {
2020 automatedEmail : AutomatedEmail ,
21- emailType : 'free' | 'paid'
21+ emailType : 'free' | 'paid' ,
22+ enabled : boolean ,
23+ isInitialLoading : boolean ,
24+ onEdit : ( ) => void ,
25+ onToggle : ( ) => void
2226} > = ( {
2327 automatedEmail,
24- emailType
28+ emailType,
29+ enabled,
30+ isInitialLoading,
31+ onEdit,
32+ onToggle
2533} ) => {
2634 const { settings} = useGlobalData ( ) ;
2735 const [ accentColor , icon , siteTitle ] = getSettingValues < string > ( settings , [ 'accent_color' , 'icon' , 'title' ] ) ;
@@ -30,34 +38,60 @@ const EmailPreview: React.FC<{
3038 const senderName = automatedEmail . sender_name || siteTitle || 'Your Site' ;
3139
3240 return (
33- < div className = 'mb-5 flex items-center justify-between gap-3 rounded-lg border border-grey-100 bg-grey-50 p-5 dark:border-grey-925 dark:bg-grey-975' >
34- < div className = 'flex items-start gap-3' >
35- { icon ?
36- < div className = 'size-10 min-h-10 min-w-10 rounded-sm bg-cover bg-center' style = { {
37- backgroundImage : `url(${ icon } )`
38- } } />
39- :
40- < div className = 'flex aspect-square size-10 items-center justify-center overflow-hidden rounded-full p-1 text-white' style = { {
41- backgroundColor : color
42- } } >
43- < img className = 'h-auto w-8' src = { FakeLogo } />
41+ < div
42+ className = 'relative flex w-full items-center justify-between gap-6 rounded-lg border border-grey-100 bg-grey-50 p-5 text-left transition-all hover:border-grey-200 hover:shadow-sm dark:border-grey-925 dark:bg-grey-975 dark:hover:border-grey-800'
43+ data-testid = { `${ emailType } -welcome-email-preview` }
44+ >
45+ < button
46+ className = 'flex w-full cursor-pointer items-center justify-between before:absolute before:inset-0 before:rounded-lg before:content-[""] focus-visible:outline-none focus-visible:before:ring-2 focus-visible:before:ring-green'
47+ type = 'button'
48+ onClick = { onEdit }
49+ >
50+ < div className = 'flex items-start gap-3' >
51+ { icon ?
52+ < div className = 'size-10 min-h-10 min-w-10 rounded-sm bg-cover bg-center' style = { {
53+ backgroundImage : `url(${ icon } )`
54+ } } />
55+ :
56+ < div className = 'flex aspect-square size-10 items-center justify-center overflow-hidden rounded-full p-1 text-white' style = { {
57+ backgroundColor : color
58+ } } >
59+ < img alt = "" className = 'h-auto w-8' src = { FakeLogo } />
60+ </ div >
61+ }
62+ < div className = 'text-left' >
63+ < div className = 'font-semibold' > { senderName } </ div >
64+ < div className = 'text-sm' > { automatedEmail . subject } </ div >
4465 </ div >
45- }
46- < div >
47- < div className = 'font-semibold' > { senderName } </ div >
48- < div className = 'text-sm' > { automatedEmail . subject } </ div >
4966 </ div >
67+ < div className = 'text-sm font-semibold opacity-100 transition-all hover:opacity-80' >
68+ Edit
69+ </ div >
70+ </ button >
71+ < div className = 'relative z-10 rounded-full has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-green' >
72+ { isInitialLoading ? (
73+ < div className = "h-4 w-7 rounded-full bg-grey-300 dark:bg-grey-800" />
74+ ) : (
75+ < Toggle
76+ checked = { enabled }
77+ onChange = { onToggle }
78+ />
79+ ) }
80+ </ div >
81+ </ div >
82+ ) ;
83+ } ;
84+
85+ const EmailSettingRow : React . FC < {
86+ title : string ,
87+ description : string
88+ } > = ( { title, description} ) => {
89+ return (
90+ < div className = 'flex items-center justify-between py-4' >
91+ < div >
92+ < div className = 'font-medium' > { title } </ div >
93+ < div className = 'text-sm text-grey-700 dark:text-grey-600' > { description } </ div >
5094 </ div >
51- < Button
52- className = 'border border-grey-200 font-semibold hover:border-grey-300 dark:border-grey-900 dark:hover:border-grey-800 dark:hover:bg-grey-950'
53- color = 'white'
54- data-testid = { `${ emailType } -welcome-email-edit-button` }
55- icon = 'pen'
56- label = 'Edit'
57- onClick = { ( ) => {
58- NiceModal . show ( WelcomeEmailModal , { emailType, automatedEmail} ) ;
59- } }
60- />
6195 </ div >
6296 ) ;
6397} ;
@@ -67,51 +101,101 @@ const MemberEmails: React.FC<{ keywords: string[] }> = ({keywords}) => {
67101 const [ siteTitle ] = getSettingValues < string > ( settings , [ 'title' ] ) ;
68102
69103 const { data : automatedEmailsData , isLoading} = useBrowseAutomatedEmails ( ) ;
70- const { mutateAsync : addAutomatedEmail } = useAddAutomatedEmail ( ) ;
71- const { mutateAsync : editAutomatedEmail } = useEditAutomatedEmail ( ) ;
104+ const { mutateAsync : addAutomatedEmail , isLoading : isAddingAutomatedEmail } = useAddAutomatedEmail ( ) ;
105+ const { mutateAsync : editAutomatedEmail , isLoading : isEditingAutomatedEmail } = useEditAutomatedEmail ( ) ;
72106 const handleError = useHandleError ( ) ;
73107
74108 const automatedEmails = automatedEmailsData ?. automated_emails || [ ] ;
109+ const isMutating = isAddingAutomatedEmail || isEditingAutomatedEmail ;
110+ const isBusy = isLoading || isMutating ;
75111
76112 const freeWelcomeEmail = automatedEmails . find ( email => email . slug === 'member-welcome-email-free' ) ;
77113 const paidWelcomeEmail = automatedEmails . find ( email => email . slug === 'member-welcome-email-paid' ) ;
78114
79115 const freeWelcomeEmailEnabled = freeWelcomeEmail ?. status === 'active' ;
80116 const paidWelcomeEmailEnabled = paidWelcomeEmail ?. status === 'active' ;
81117
118+ // Helper to get default values for an email type
119+ const getDefaultEmailValues = ( emailType : 'free' | 'paid' ) => ( {
120+ name : emailType === 'free' ? 'Welcome Email (Free)' : 'Welcome Email (Paid)' ,
121+ slug : `member-welcome-email-${ emailType } ` ,
122+ subject : emailType === 'free'
123+ ? `Welcome to ${ siteTitle || 'our site' } `
124+ : 'Welcome to your paid subscription' ,
125+ lexical : emailType === 'free' ? DEFAULT_FREE_LEXICAL_CONTENT : DEFAULT_PAID_LEXICAL_CONTENT
126+ } ) ;
127+
128+ // Create default email objects for display when no DB row exists
129+ const getDefaultEmail = ( emailType : 'free' | 'paid' ) : AutomatedEmail => ( {
130+ id : '' ,
131+ status : 'inactive' ,
132+ ...getDefaultEmailValues ( emailType ) ,
133+ sender_name : null ,
134+ sender_email : null ,
135+ sender_reply_to : null ,
136+ created_at : '' ,
137+ updated_at : null
138+ } ) ;
139+
140+ // Create a new automated email row with the given status
141+ const createAutomatedEmail = async ( emailType : 'free' | 'paid' , status : 'active' | 'inactive' ) => {
142+ const defaults = getDefaultEmailValues ( emailType ) ;
143+ return addAutomatedEmail ( { ...defaults , status} ) ;
144+ } ;
145+
82146 const handleToggle = async ( emailType : 'free' | 'paid' ) => {
83147 const slug = `member-welcome-email-${ emailType } ` ;
84148 const existing = automatedEmails . find ( email => email . slug === slug ) ;
149+ const label = emailType === 'free' ? 'Free members' : 'Paid members' ;
85150
86- const defaultSubject = emailType === 'free'
87- ? `Welcome to ${ siteTitle || 'our site' } `
88- : 'Welcome to your paid subscription' ;
151+ if ( isBusy ) {
152+ return ;
153+ }
89154
90155 try {
91156 if ( ! existing ) {
92- // First toggle ON - create with defaults
93- const defaultContent = emailType === 'free'
94- ? DEFAULT_FREE_LEXICAL_CONTENT
95- : DEFAULT_PAID_LEXICAL_CONTENT ;
96- await addAutomatedEmail ( {
97- name : emailType === 'free' ? 'Welcome Email (Free)' : 'Welcome Email (Paid)' ,
98- slug : slug ,
99- subject : defaultSubject ,
100- status : 'active' ,
101- lexical : defaultContent
102- } ) ;
157+ await createAutomatedEmail ( emailType , 'active' ) ;
158+ showToast ( { type : 'success' , title : `${ label } welcome email enabled` } ) ;
103159 } else if ( existing . status === 'active' ) {
104- // Toggle OFF
105160 await editAutomatedEmail ( { ...existing , status : 'inactive' } ) ;
161+ showToast ( { type : 'success' , title : `${ label } welcome email disabled` } ) ;
106162 } else {
107- // Toggle ON (re-enable)
108163 await editAutomatedEmail ( { ...existing , status : 'active' } ) ;
164+ showToast ( { type : 'success' , title : `${ label } welcome email enabled` } ) ;
109165 }
110166 } catch ( e ) {
111167 handleError ( e ) ;
112168 }
113169 } ;
114170
171+ // Handle Edit button click - creates inactive row if needed, then opens modal
172+ const handleEditClick = async ( emailType : 'free' | 'paid' ) => {
173+ const slug = `member-welcome-email-${ emailType } ` ;
174+ const existing = automatedEmails . find ( email => email . slug === slug ) ;
175+
176+ if ( isBusy ) {
177+ return ;
178+ }
179+
180+ if ( ! existing ) {
181+ try {
182+ const result = await createAutomatedEmail ( emailType , 'inactive' ) ;
183+ const newEmail = result ?. automated_emails ?. [ 0 ] ;
184+ if ( newEmail ) {
185+ NiceModal . show ( WelcomeEmailModal , { emailType, automatedEmail : newEmail } ) ;
186+ }
187+ } catch ( e ) {
188+ handleError ( e ) ;
189+ }
190+ } else {
191+ NiceModal . show ( WelcomeEmailModal , { emailType, automatedEmail : existing } ) ;
192+ }
193+ } ;
194+
195+ // Get email to display (existing or default for preview)
196+ const freeEmailForDisplay = freeWelcomeEmail || getDefaultEmail ( 'free' ) ;
197+ const paidEmailForDisplay = paidWelcomeEmail || getDefaultEmail ( 'paid' ) ;
198+
115199 return (
116200 < TopLevelGroup
117201 description = "Create and manage automated emails for your members"
@@ -122,46 +206,33 @@ const MemberEmails: React.FC<{ keywords: string[] }> = ({keywords}) => {
122206 >
123207 < SettingGroupContent className = "!gap-y-0" columns = { 1 } >
124208 < Separator />
125- < Toggle
126- key = { ` free- ${ isLoading ? 'loading' : freeWelcomeEmail ?. status ?? 'none' } ` }
127- checked = { Boolean ( freeWelcomeEmailEnabled ) }
128- containerClasses = 'items-center'
129- direction = 'rtl'
130- disabled = { isLoading }
131- gap = 'gap-0 '
132- hint = 'Sent to new free members right after they join your site.'
133- label = 'Free members'
134- labelClasses = 'py-4 w-full'
135- onChange = { ( ) => handleToggle ( 'free' ) }
209+ < EmailSettingRow
210+ description = 'Email new free members receive when they join your site.'
211+ title = 'Free members welcome email'
212+ />
213+ < EmailPreview
214+ automatedEmail = { freeEmailForDisplay }
215+ emailType = 'free '
216+ enabled = { freeWelcomeEmailEnabled }
217+ isInitialLoading = { isLoading }
218+ onEdit = { ( ) => handleEditClick ( 'free' ) }
219+ onToggle = { ( ) => handleToggle ( 'free' ) }
136220 />
137- { freeWelcomeEmail && freeWelcomeEmailEnabled &&
138- < EmailPreview
139- automatedEmail = { freeWelcomeEmail }
140- emailType = 'free'
141- />
142- }
143221 { checkStripeEnabled ( settings , config ) && (
144- < >
145- < Separator />
146- < Toggle
147- key = { `paid-${ isLoading ? 'loading' : paidWelcomeEmail ?. status ?? 'none' } ` }
148- checked = { Boolean ( paidWelcomeEmailEnabled ) }
149- containerClasses = 'items-center'
150- direction = 'rtl'
151- disabled = { isLoading }
152- gap = 'gap-0'
153- hint = 'Sent to new paid members right after they start their subscription.'
154- label = 'Paid members'
155- labelClasses = 'py-4 w-full'
156- onChange = { ( ) => handleToggle ( 'paid' ) }
222+ < div className = 'mt-4' >
223+ < EmailSettingRow
224+ description = 'Sent to new paid members as soon as they start their subscription.'
225+ title = 'Paid members welcome email'
157226 />
158- { paidWelcomeEmail && paidWelcomeEmailEnabled &&
159- < EmailPreview
160- automatedEmail = { paidWelcomeEmail }
161- emailType = 'paid'
162- />
163- }
164- </ >
227+ < EmailPreview
228+ automatedEmail = { paidEmailForDisplay }
229+ emailType = 'paid'
230+ enabled = { paidWelcomeEmailEnabled }
231+ isInitialLoading = { isLoading }
232+ onEdit = { ( ) => handleEditClick ( 'paid' ) }
233+ onToggle = { ( ) => handleToggle ( 'paid' ) }
234+ />
235+ </ div >
165236 ) }
166237 </ SettingGroupContent >
167238 </ TopLevelGroup >
0 commit comments