Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {checkStripeEnabled, getSettingValues} from '@tryghost/admin-x-framework/
import {useAddAutomatedEmail, useBrowseAutomatedEmails, useEditAutomatedEmail} from '@tryghost/admin-x-framework/api/automated-emails';
import {useGlobalData} from '../../providers/global-data-provider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useWelcomeEmailSenderDetails} from '../../../hooks/use-welcome-email-sender-details';
import type {AutomatedEmail} from '@tryghost/admin-x-framework/api/automated-emails';

// Default welcome email content in Lexical JSON format
Expand All @@ -32,10 +33,9 @@ const EmailPreview: React.FC<{
onToggle
}) => {
const {settings} = useGlobalData();
const [accentColor, icon, siteTitle] = getSettingValues<string>(settings, ['accent_color', 'icon', 'title']);
const [accentColor, icon] = getSettingValues<string>(settings, ['accent_color', 'icon']);
const color = accentColor || '#F6414E';

const senderName = automatedEmail.sender_name || siteTitle || 'Your Site';
const {resolvedSenderName} = useWelcomeEmailSenderDetails(automatedEmail);

return (
<div
Expand All @@ -60,7 +60,7 @@ const EmailPreview: React.FC<{
</div>
}
<div className='text-left'>
<div className='font-semibold'>{senderName}</div>
<div className='font-semibold'>{resolvedSenderName}</div>
<div className='text-sm'>{automatedEmail.subject}</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useCallback, useEffect, useRef, useState} from 'react';
import MemberEmailEditor from './member-email-editor';
import {Button, Hint, Modal, TextField} from '@tryghost/admin-x-design-system';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useWelcomeEmailSenderDetails} from '../../../../hooks/use-welcome-email-sender-details';

import TestEmailDropdown from './test-email-dropdown';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
Expand Down Expand Up @@ -51,7 +52,8 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
const hasEditorBeenFocused = useRef(false);
const handleError = useHandleError();
const {settings} = useGlobalData();
const [siteTitle, defaultEmailAddress] = getSettingValues<string>(settings, ['title', 'default_email_address']);
const [siteTitle] = getSettingValues<string>(settings, ['title']);
const {resolvedSenderName, resolvedSenderEmail, resolvedReplyToEmail, hasDistinctReplyTo} = useWelcomeEmailSenderDetails(automatedEmail);

const {formState, saveState, updateForm, setFormState, handleSave, okProps, errors, validate} = useForm({
initialState: {
Expand Down Expand Up @@ -135,9 +137,6 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
}
}, [setFormState, updateForm]);

const senderEmail = automatedEmail?.sender_email || defaultEmailAddress;
const replyToEmail = automatedEmail?.sender_reply_to || defaultEmailAddress;

return (
<Modal
afterClose={() => {
Expand Down Expand Up @@ -177,15 +176,15 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
<div className='flex items-center'>
<div className='w-20 font-semibold'>From:</div>
<div className='flex grow items-center gap-1'>
<span>{automatedEmail?.sender_name || siteTitle}</span>
<span className='text-grey-700 dark:text-grey-400'>{`<${senderEmail}>`}</span>
<span>{resolvedSenderName}</span>
<span className='text-grey-700 dark:text-grey-400'>{`<${resolvedSenderEmail}>`}</span>
</div>
</div>
{replyToEmail !== senderEmail && (
{hasDistinctReplyTo && (
<div className='flex items-center py-0.5'>
<div className='w-20 font-semibold'>Reply-to:</div>
<div className='grow text-grey-700 dark:text-grey-400'>
{replyToEmail}
{resolvedReplyToEmail}
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {renderReplyToEmail, renderSenderEmail} from '../utils/newsletter-emails';
import {useBrowseNewsletters} from '@tryghost/admin-x-framework/api/newsletters';
import {useGlobalData} from '../components/providers/global-data-provider';
import {useMemo} from 'react';
import type {AutomatedEmail} from '@tryghost/admin-x-framework/api/automated-emails';

type AutomatedEmailSenderFields = Pick<AutomatedEmail, 'sender_name' | 'sender_email' | 'sender_reply_to'> | null | undefined;

const trimValue = (value: string | null | undefined) => value?.trim() || '';

export const useWelcomeEmailSenderDetails = (automatedEmail: AutomatedEmailSenderFields) => {
const {settings, config} = useGlobalData();
const [siteTitle, defaultEmailAddress, supportEmailAddress] = getSettingValues<string>(settings, ['title', 'default_email_address', 'support_email_address']);
const {data: newslettersData} = useBrowseNewsletters({
searchParams: {
filter: 'status:active',
limit: '1'
}
});
const defaultNewsletter = newslettersData?.newsletters?.[0];

return useMemo(() => {
const automatedSenderName = trimValue(automatedEmail?.sender_name);
const automatedSenderEmail = trimValue(automatedEmail?.sender_email);
const automatedSenderReplyTo = trimValue(automatedEmail?.sender_reply_to);

const defaultNewsletterSenderName = trimValue(defaultNewsletter?.sender_name);
const defaultNewsletterSenderEmail = defaultNewsletter ? trimValue(renderSenderEmail(defaultNewsletter, config, defaultEmailAddress)) : '';
const defaultNewsletterReplyTo = defaultNewsletter ? trimValue(renderReplyToEmail(defaultNewsletter, config, supportEmailAddress, defaultEmailAddress)) : '';

const resolvedSenderName = automatedSenderName || defaultNewsletterSenderName || trimValue(siteTitle) || 'Your Site';
const resolvedSenderEmail = automatedSenderEmail || defaultNewsletterSenderEmail || trimValue(defaultEmailAddress) || '';
const resolvedReplyToEmail = automatedSenderReplyTo || defaultNewsletterReplyTo || '';
const hasDistinctReplyTo = resolvedReplyToEmail !== '' && resolvedReplyToEmail !== resolvedSenderEmail;

return {
resolvedSenderName,
resolvedSenderEmail,
resolvedReplyToEmail,
defaultNewsletterSenderName,
hasDistinctReplyTo
};
}, [
automatedEmail?.sender_email,
automatedEmail?.sender_name,
automatedEmail?.sender_reply_to,
config,
defaultEmailAddress,
defaultNewsletter,
siteTitle,
supportEmailAddress
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const automatedEmailsFixture = {
}]
};

const newslettersRequest = {
browseNewslettersLimit: {method: 'GET', path: '/newsletters/?filter=status%3Aactive&limit=1', response: responseFixtures.newsletters}
};

test.describe('Member emails settings', async () => {
test.describe('Welcome email modal', async () => {
test('Escape key closes test email dropdown without closing modal', async ({page}) => {
Expand All @@ -33,6 +37,7 @@ test.describe('Member emails settings', async () => {

await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: automatedEmailsFixture}
}});
Expand Down Expand Up @@ -82,6 +87,7 @@ test.describe('Member emails settings', async () => {

await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: automatedEmailsFixture}
}});
Expand Down Expand Up @@ -121,6 +127,7 @@ test.describe('Member emails settings', async () => {

await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: automatedEmailsFixture}
}});
Expand Down Expand Up @@ -178,6 +185,7 @@ test.describe('Member emails settings', async () => {

await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: automatedEmailsFixture}
}});
Expand Down Expand Up @@ -227,6 +235,150 @@ test.describe('Member emails settings', async () => {
document.querySelector('[data-kg-link-input]')?.remove();
});
});

test('uses automated email sender fields when populated, even if newsletter differs', async ({page}) => {
const configResponse = {
config: {
...responseFixtures.config.config,
labs: {
welcomeEmails: true
}
}
};

const populatedAutomatedEmailsFixture = {
automated_emails: [{
...automatedEmailsFixture.automated_emails[0],
sender_name: 'Automated Sender',
sender_email: 'automated@example.com',
sender_reply_to: 'reply-automated@example.com'
}]
};

const defaultNewsletterResponse = {
newsletters: [{
...responseFixtures.newsletters.newsletters[0],
sender_name: 'Newsletter Sender',
sender_email: 'newsletter@example.com',
sender_reply_to: 'support'
}],
meta: responseFixtures.newsletters.meta
};

await mockApi({page, requests: {
...globalDataRequests,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: populatedAutomatedEmailsFixture},
browseNewslettersLimit: {method: 'GET', path: '/newsletters/?filter=status%3Aactive&limit=1', response: defaultNewsletterResponse}
}});

await page.goto('/#/memberemails');
await page.waitForLoadState('networkidle');

const section = page.getByTestId('memberemails');
await expect(section).toBeVisible({timeout: 10000});
await section.getByTestId('free-welcome-email-preview').click();

const modal = page.getByTestId('welcome-email-modal');
await expect(modal).toBeVisible();
await expect(modal).toContainText('Automated Sender');
await expect(modal).toContainText('automated@example.com');
await expect(modal).toContainText('reply-automated@example.com');
await expect(modal).not.toContainText('newsletter@example.com');
});

test('falls back to default newsletter sender values when automated fields are empty', async ({page}) => {
const configResponse = {
config: {
...responseFixtures.config.config,
labs: {
welcomeEmails: true
}
}
};

const emptyAutomatedSenderFixture = {
automated_emails: [{
...automatedEmailsFixture.automated_emails[0],
sender_name: ' ',
sender_email: ' ',
sender_reply_to: ' '
}]
};

const defaultNewsletterResponse = {
newsletters: [{
...responseFixtures.newsletters.newsletters[0],
sender_name: 'Newsletter Sender',
sender_email: 'newsletter@example.com',
sender_reply_to: 'support'
}],
meta: responseFixtures.newsletters.meta
};

await mockApi({page, requests: {
...globalDataRequests,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: emptyAutomatedSenderFixture},
browseNewslettersLimit: {method: 'GET', path: '/newsletters/?filter=status%3Aactive&limit=1', response: defaultNewsletterResponse}
}});

await page.goto('/#/memberemails');
await page.waitForLoadState('networkidle');

const section = page.getByTestId('memberemails');
await expect(section).toBeVisible({timeout: 10000});
await section.getByTestId('free-welcome-email-preview').click();

const modal = page.getByTestId('welcome-email-modal');
await expect(modal).toBeVisible();
await expect(modal).toContainText('Newsletter Sender');
await expect(modal).toContainText('newsletter@example.com');
await expect(modal).toContainText('support@example.com');
await expect(modal).not.toContainText('default@example.com');
});

test('preview card uses newsletter sender name when automated sender name is empty', async ({page}) => {
const configResponse = {
config: {
...responseFixtures.config.config,
labs: {
welcomeEmails: true
}
}
};

const emptyAutomatedSenderFixture = {
automated_emails: [{
...automatedEmailsFixture.automated_emails[0],
sender_name: ' '
}]
};

const defaultNewsletterResponse = {
newsletters: [{
...responseFixtures.newsletters.newsletters[0],
sender_name: 'Newsletter Sender'
}],
meta: responseFixtures.newsletters.meta
};

await mockApi({page, requests: {
...globalDataRequests,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: emptyAutomatedSenderFixture},
browseNewslettersLimit: {method: 'GET', path: '/newsletters/?filter=status%3Aactive&limit=1', response: defaultNewsletterResponse}
}});

await page.goto('/#/memberemails');
await page.waitForLoadState('networkidle');

const section = page.getByTestId('memberemails');
await expect(section).toBeVisible({timeout: 10000});

const cardSenderName = section.locator('[data-testid="free-welcome-email-preview"] .font-semibold').first();
await expect(cardSenderName).toHaveText('Newsletter Sender');
});
});

// NY-842: Tests for editing/viewing welcome emails before activation
Expand All @@ -249,6 +401,7 @@ test.describe('Member emails settings', async () => {

await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: emptyAutomatedEmailsFixture}
}});
Expand Down Expand Up @@ -305,6 +458,7 @@ test.describe('Member emails settings', async () => {

const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: emptyAutomatedEmailsFixture},
addAutomatedEmail: {method: 'POST', path: '/automated_emails/', response: createdAutomatedEmailResponse}
Expand Down Expand Up @@ -361,6 +515,7 @@ test.describe('Member emails settings', async () => {

const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: existingAutomatedEmailsFixture},
addAutomatedEmail: {method: 'POST', path: '/automated_emails/', response: existingAutomatedEmailsFixture}
Expand Down Expand Up @@ -416,6 +571,7 @@ test.describe('Member emails settings', async () => {

const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: emptyAutomatedEmailsFixture},
addAutomatedEmail: {method: 'POST', path: '/automated_emails/', response: createdActiveResponse}
Expand Down Expand Up @@ -475,6 +631,7 @@ test.describe('Member emails settings', async () => {

const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: inactiveAutomatedEmailsFixture},
editAutomatedEmail: {method: 'PUT', path: '/automated_emails/free-welcome-email-id/', response: updatedActiveResponse}
Expand Down Expand Up @@ -534,6 +691,7 @@ test.describe('Member emails settings', async () => {

const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
...newslettersRequest,
browseConfig: {method: 'GET', path: '/config/', response: configResponse},
browseAutomatedEmails: {method: 'GET', path: '/automated_emails/', response: activeAutomatedEmailsFixture},
editAutomatedEmail: {method: 'PUT', path: '/automated_emails/free-welcome-email-id/', response: updatedInactiveResponse}
Expand Down
2 changes: 1 addition & 1 deletion apps/portal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/portal",
"version": "2.64.2",
"version": "2.64.3",
"license": "MIT",
"repository": "https://github.com/TryGhost/Ghost",
"author": "Ghost Foundation",
Expand Down
2 changes: 1 addition & 1 deletion apps/portal/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ export default class App extends React.Component {
/**Handle state update for preview url and Portal Link changes */
updateStateForPreviewLinks() {
const {site: previewSite, ...restPreviewData} = this.fetchPreviewData();
const {site: linkSite, ...restLinkData} = this.fetchLinkData();
const {site: linkSite, ...restLinkData} = this.fetchLinkData(this.state.site, this.state.member);

const updatedState = {
site: {
Expand Down
Loading
Loading