mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Updated newsletter settings UI for managed email (#19082)
refs GRO-59 refs GRO-56 refs GRO-52 - When email is managed without a custom domain, do not allow the Sender Email address to be changed, but allow Reply-to address to be changed to any address the publisher can verify - When email is managed with a custom domain, allow both Sender and Reply-to addresses to be changed without verification, but not their domain names --------- Co-authored-by: Djordje Vlaisavljevic <dzvlais@gmail.com>
This commit is contained in:
parent
f981993ba4
commit
ff70ffec67
4 changed files with 385 additions and 94 deletions
|
@ -48,7 +48,11 @@ export type Config = {
|
||||||
pintura?: {
|
pintura?: {
|
||||||
js?: string
|
js?: string
|
||||||
css?: string
|
css?: string
|
||||||
}
|
},
|
||||||
|
managedEmail?: {
|
||||||
|
enabled?: boolean
|
||||||
|
sendingDomain?: string
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config is relatively fluid, so we only type used properties above and still support arbitrary property access when needed
|
// Config is relatively fluid, so we only type used properties above and still support arbitrary property access when needed
|
||||||
|
@ -67,3 +71,19 @@ export const useBrowseConfig = createQuery<ConfigResponseType>({
|
||||||
dataType,
|
dataType,
|
||||||
path: '/config/'
|
path: '/config/'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
export const isManagedEmail = (config: Config) => {
|
||||||
|
return !!config?.hostSettings?.managedEmail?.enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasSendingDomain = (config: Config) => {
|
||||||
|
const isDomain = /[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
||||||
|
const sendingDomain = config?.hostSettings?.managedEmail?.sendingDomain;
|
||||||
|
return typeof sendingDomain === 'string' && isDomain.test(sendingDomain);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendingDomain = (config: Config) => {
|
||||||
|
return config?.hostSettings?.managedEmail?.sendingDomain;
|
||||||
|
};
|
||||||
|
|
|
@ -4,17 +4,33 @@ import React, {useEffect, useState} from 'react';
|
||||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import {Button, ButtonGroup, ColorPickerField, ConfirmationModal, Form, Heading, Hint, HtmlField, Icon, ImageUpload, LimitModal, PreviewModalContent, Select, SelectOption, Separator, Tab, TabView, TextArea, TextField, Toggle, ToggleGroup, showToast} from '@tryghost/admin-x-design-system';
|
import {Button, ButtonGroup, ColorPickerField, ConfirmationModal, Form, Heading, Hint, HtmlField, Icon, ImageUpload, LimitModal, PreviewModalContent, Select, SelectOption, Separator, SettingGroupContent, Tab, TabView, TextArea, TextField, Toggle, ToggleGroup, showToast} from '@tryghost/admin-x-design-system';
|
||||||
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
|
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||||
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
||||||
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
|
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
|
||||||
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
|
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
import {SiteData} from '@tryghost/admin-x-framework/api/site';
|
||||||
import {fullEmailAddress} from '@tryghost/admin-x-framework/api/site';
|
import {fullEmailAddress} from '@tryghost/admin-x-framework/api/site';
|
||||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||||
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||||
|
import {hasSendingDomain, isManagedEmail, sendingDomain} from '@tryghost/admin-x-framework/api/config';
|
||||||
import {textColorForBackgroundColor} from '@tryghost/color-utils';
|
import {textColorForBackgroundColor} from '@tryghost/color-utils';
|
||||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||||
|
|
||||||
|
const renderReplyToEmail = (newsletter: Newsletter, siteData: SiteData, membersSupportAddress?: string) => {
|
||||||
|
if (!newsletter.sender_reply_to) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newsletter.sender_reply_to === 'newsletter') {
|
||||||
|
return fullEmailAddress(newsletter.sender_email || 'noreply', siteData);
|
||||||
|
} else if (newsletter.sender_reply_to === 'support') {
|
||||||
|
return fullEmailAddress(membersSupportAddress || 'noreply', siteData);
|
||||||
|
} else {
|
||||||
|
return newsletter.sender_reply_to;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Sidebar: React.FC<{
|
const Sidebar: React.FC<{
|
||||||
newsletter: Newsletter;
|
newsletter: Newsletter;
|
||||||
onlyOne: boolean;
|
onlyOne: boolean;
|
||||||
|
@ -25,7 +41,7 @@ const Sidebar: React.FC<{
|
||||||
}> = ({newsletter, onlyOne, updateNewsletter, validate, errors, clearError}) => {
|
}> = ({newsletter, onlyOne, updateNewsletter, validate, errors, clearError}) => {
|
||||||
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||||
const limiter = useLimiter();
|
const limiter = useLimiter();
|
||||||
const {settings, siteData} = useGlobalData();
|
const {settings, siteData, config} = useGlobalData();
|
||||||
const [membersSupportAddress, icon] = getSettingValues<string>(settings, ['members_support_address', 'icon']);
|
const [membersSupportAddress, icon] = getSettingValues<string>(settings, ['members_support_address', 'icon']);
|
||||||
const {mutateAsync: uploadImage} = useUploadImage();
|
const {mutateAsync: uploadImage} = useUploadImage();
|
||||||
const [selectedTab, setSelectedTab] = useState('generalSettings');
|
const [selectedTab, setSelectedTab] = useState('generalSettings');
|
||||||
|
@ -34,9 +50,12 @@ const Sidebar: React.FC<{
|
||||||
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
|
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
|
||||||
const handleError = useHandleError();
|
const handleError = useHandleError();
|
||||||
|
|
||||||
|
const newsletterAddress = fullEmailAddress(newsletter.sender_email || 'noreply', siteData);
|
||||||
|
const supportAddress = fullEmailAddress(membersSupportAddress || 'noreply', siteData);
|
||||||
|
|
||||||
const replyToEmails = [
|
const replyToEmails = [
|
||||||
{label: `Newsletter address (${fullEmailAddress(newsletter.sender_email || 'noreply', siteData)})`, value: 'newsletter'},
|
{label: `Newsletter address (${newsletterAddress})`, value: 'newsletter'},
|
||||||
{label: `Support address (${fullEmailAddress(membersSupportAddress || 'noreply', siteData)})`, value: 'support'}
|
{label: `Support address (${supportAddress})`, value: 'support'}
|
||||||
];
|
];
|
||||||
|
|
||||||
const fontOptions: SelectOption[] = [
|
const fontOptions: SelectOption[] = [
|
||||||
|
@ -109,6 +128,103 @@ const Sidebar: React.FC<{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderSenderEmailField = () => {
|
||||||
|
if (isManagedEmail(config)) {
|
||||||
|
if (hasSendingDomain(config)) {
|
||||||
|
const sendingEmailUsername = newsletter.sender_email?.split('@')[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
error={Boolean(errors.sender_email)}
|
||||||
|
hint={errors.sender_email}
|
||||||
|
rightPlaceholder={`@${sendingDomain(config)}`}
|
||||||
|
title="Sender email address"
|
||||||
|
value={sendingEmailUsername || ''}
|
||||||
|
onBlur={validate}
|
||||||
|
onChange={(e) => {
|
||||||
|
const username = e.target.value?.split('@')[0];
|
||||||
|
const newEmail = username ? `${username}@${sendingDomain(config)}` : '';
|
||||||
|
updateNewsletter({sender_email: newEmail});
|
||||||
|
}}
|
||||||
|
onKeyDown={() => clearError('sender_email')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<SettingGroupContent
|
||||||
|
values={[
|
||||||
|
{
|
||||||
|
heading: 'Sender email address',
|
||||||
|
key: 'sender-email-addresss',
|
||||||
|
value: `${newsletter.sender_email}`,
|
||||||
|
hint: <span className="text-xs text-grey-700">To customise, set up a <a className="text-green" href="#">custom sending domain</a></span>
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
error={Boolean(errors.sender_email)}
|
||||||
|
hint={errors.sender_email}
|
||||||
|
placeholder={newsletterAddress}
|
||||||
|
title="Sender email address"
|
||||||
|
value={newsletter.sender_email || ''}
|
||||||
|
onBlur={validate}
|
||||||
|
onChange={e => updateNewsletter({sender_email: e.target.value})}
|
||||||
|
onKeyDown={() => clearError('sender_email')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderReplyToEmailField = () => {
|
||||||
|
if (isManagedEmail(config)) {
|
||||||
|
if (hasSendingDomain(config)) {
|
||||||
|
const replyToEmailUsername = ['newsletter', 'support'].includes(newsletter.sender_reply_to) ? '' : newsletter.sender_reply_to?.split('@')[0];
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
error={Boolean(errors.sender_reply_to)}
|
||||||
|
hint={errors.sender_reply_to}
|
||||||
|
rightPlaceholder={`@${sendingDomain(config)}`}
|
||||||
|
title="Reply-to address"
|
||||||
|
value={replyToEmailUsername || ''}
|
||||||
|
onBlur={validate}
|
||||||
|
onChange={(e) => {
|
||||||
|
const username = e.target.value?.split('@')[0];
|
||||||
|
const newEmail = username ? `${username}@${sendingDomain(config)}` : '';
|
||||||
|
updateNewsletter({sender_reply_to: newEmail});
|
||||||
|
}}
|
||||||
|
onKeyDown={() => clearError('sender_reply_to')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
error={Boolean(errors.sender_reply_to)}
|
||||||
|
hint={errors.sender_reply_to}
|
||||||
|
placeholder={newsletterAddress}
|
||||||
|
title="Reply-to email"
|
||||||
|
value={renderReplyToEmail(newsletter, siteData, membersSupportAddress)}
|
||||||
|
onBlur={validate}
|
||||||
|
onChange={e => updateNewsletter({sender_reply_to: e.target.value})}
|
||||||
|
onKeyDown={() => clearError('sender_reply_to')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={replyToEmails}
|
||||||
|
selectedOption={replyToEmails.find(option => option.value === newsletter.sender_reply_to)}
|
||||||
|
title="Reply-to email"
|
||||||
|
onSelect={option => updateNewsletter({sender_reply_to: option?.value})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
{
|
{
|
||||||
id: 'generalSettings',
|
id: 'generalSettings',
|
||||||
|
@ -130,22 +246,8 @@ const Sidebar: React.FC<{
|
||||||
</Form>
|
</Form>
|
||||||
<Form className='mt-6' gap='sm' margins='lg' title='Email addresses'>
|
<Form className='mt-6' gap='sm' margins='lg' title='Email addresses'>
|
||||||
<TextField placeholder={siteTitle} title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
|
<TextField placeholder={siteTitle} title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
|
||||||
<TextField
|
{renderSenderEmailField()}
|
||||||
error={Boolean(errors.sender_email)}
|
{renderReplyToEmailField()}
|
||||||
hint={errors.sender_email}
|
|
||||||
placeholder={fullEmailAddress(newsletter.sender_email || 'noreply', siteData)}
|
|
||||||
title="Sender email address"
|
|
||||||
value={newsletter.sender_email || ''}
|
|
||||||
onBlur={validate}
|
|
||||||
onChange={e => updateNewsletter({sender_email: e.target.value})}
|
|
||||||
onKeyDown={() => clearError('sender_email')}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
options={replyToEmails}
|
|
||||||
selectedOption={replyToEmails.find(option => option.value === newsletter.sender_reply_to)}
|
|
||||||
title="Reply-to email"
|
|
||||||
onSelect={option => updateNewsletter({sender_reply_to: option?.value})}
|
|
||||||
/>
|
|
||||||
</Form>
|
</Form>
|
||||||
<Form className='mt-6' gap='sm' margins='lg' title='Member settings'>
|
<Form className='mt-6' gap='sm' margins='lg' title='Member settings'>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
@ -412,10 +514,11 @@ const Sidebar: React.FC<{
|
||||||
|
|
||||||
const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: boolean;}> = ({newsletter, onlyOne}) => {
|
const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: boolean;}> = ({newsletter, onlyOne}) => {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const {siteData} = useGlobalData();
|
const {siteData, settings, config} = useGlobalData();
|
||||||
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||||
const {updateRoute} = useRouting();
|
const {updateRoute} = useRouting();
|
||||||
const handleError = useHandleError();
|
const handleError = useHandleError();
|
||||||
|
const [membersSupportAddress] = getSettingValues<string>(settings, ['members_support_address', 'icon']);
|
||||||
|
|
||||||
const {formState, saveState, updateForm, setFormState, handleSave, validate, errors, clearError, okProps} = useForm({
|
const {formState, saveState, updateForm, setFormState, handleSave, validate, errors, clearError, okProps} = useForm({
|
||||||
initialState: newsletter,
|
initialState: newsletter,
|
||||||
|
@ -423,6 +526,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
|
||||||
onSave: async () => {
|
onSave: async () => {
|
||||||
const {newsletters, meta} = await editNewsletter(formState);
|
const {newsletters, meta} = await editNewsletter(formState);
|
||||||
if (meta?.sent_email_verification) {
|
if (meta?.sent_email_verification) {
|
||||||
|
if (meta?.sent_email_verification[0] === 'sender_email') {
|
||||||
NiceModal.show(ConfirmationModal, {
|
NiceModal.show(ConfirmationModal, {
|
||||||
title: 'Confirm newsletter email address',
|
title: 'Confirm newsletter email address',
|
||||||
prompt: <>
|
prompt: <>
|
||||||
|
@ -438,6 +542,24 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
|
||||||
updateRoute('newsletters');
|
updateRoute('newsletters');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (meta?.sent_email_verification[0] === 'sender_reply_to') {
|
||||||
|
const previousReplyTo = renderReplyToEmail(newsletters[0], siteData, membersSupportAddress);
|
||||||
|
|
||||||
|
NiceModal.show(ConfirmationModal, {
|
||||||
|
title: 'Confirm reply-to address',
|
||||||
|
prompt: <>
|
||||||
|
We‘ve sent a confirmation email to <strong>{formState.sender_reply_to}</strong>.
|
||||||
|
Until the address has been verified, newsletters will use the previous reply-to address
|
||||||
|
{previousReplyTo ? ` (${previousReplyTo})` : ''}.
|
||||||
|
</>,
|
||||||
|
cancelLabel: '',
|
||||||
|
onOk: (confirmModal) => {
|
||||||
|
confirmModal?.remove();
|
||||||
|
modal.remove();
|
||||||
|
updateRoute('newsletters');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaveError: handleError,
|
onSaveError: handleError,
|
||||||
|
@ -452,6 +574,10 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
|
||||||
newErrors.sender_email = 'Invalid email.';
|
newErrors.sender_email = 'Invalid email.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isManagedEmail(config) && formState.sender_reply_to && (!validator.isEmail(formState.sender_reply_to))) {
|
||||||
|
newErrors.sender_reply_to = 'Invalid email.';
|
||||||
|
}
|
||||||
|
|
||||||
return newErrors;
|
return newErrors;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||||
import {Newsletter} from '@tryghost/admin-x-framework/api/newsletters';
|
import {Newsletter} from '@tryghost/admin-x-framework/api/newsletters';
|
||||||
import {fullEmailAddress} from '@tryghost/admin-x-framework/api/site';
|
import {fullEmailAddress} from '@tryghost/admin-x-framework/api/site';
|
||||||
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||||
|
import {hasSendingDomain, isManagedEmail, sendingDomain} from '@tryghost/admin-x-framework/api/config';
|
||||||
import {textColorForBackgroundColor} from '@tryghost/color-utils';
|
import {textColorForBackgroundColor} from '@tryghost/color-utils';
|
||||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||||
|
|
||||||
|
@ -91,6 +92,16 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
|
||||||
secondaryTextColor
|
secondaryTextColor
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
|
const renderSenderEmail = () => {
|
||||||
|
if (isManagedEmail(config)) {
|
||||||
|
if (hasSendingDomain(config)) {
|
||||||
|
return newsletter.sender_email || 'noreply@' + sendingDomain(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullEmailAddress(newsletter.sender_email || 'noreply', siteData);
|
||||||
|
};
|
||||||
|
|
||||||
return <NewsletterPreviewContent
|
return <NewsletterPreviewContent
|
||||||
authorPlaceholder={currentUser.name || currentUser.email}
|
authorPlaceholder={currentUser.name || currentUser.email}
|
||||||
backgroundColor={colors.backgroundColor || '#ffffff'}
|
backgroundColor={colors.backgroundColor || '#ffffff'}
|
||||||
|
@ -100,7 +111,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
|
||||||
headerImage={newsletter.header_image}
|
headerImage={newsletter.header_image}
|
||||||
headerSubtitle={headerSubtitle}
|
headerSubtitle={headerSubtitle}
|
||||||
headerTitle={headerTitle}
|
headerTitle={headerTitle}
|
||||||
senderEmail={fullEmailAddress(newsletter.sender_email || 'noreply', siteData)}
|
senderEmail={renderSenderEmail()}
|
||||||
senderName={newsletter.sender_name || title}
|
senderName={newsletter.sender_name || title}
|
||||||
showBadge={newsletter.show_badge}
|
showBadge={newsletter.show_badge}
|
||||||
showCommentCta={showCommentCta}
|
showCommentCta={showCommentCta}
|
||||||
|
|
|
@ -91,6 +91,8 @@ test.describe('Newsletter settings', async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Email addresses', async () => {
|
||||||
|
test.describe('For self-hosters', async () => {
|
||||||
test('Displays a prompt when email verification is required', async ({page}) => {
|
test('Displays a prompt when email verification is required', async ({page}) => {
|
||||||
await mockApi({page, requests: {
|
await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
|
@ -166,6 +168,138 @@ test.describe('Newsletter settings', async () => {
|
||||||
await expect(page.getByTestId('confirmation-modal')).toHaveText(/Confirm newsletter email address/);
|
await expect(page.getByTestId('confirmation-modal')).toHaveText(/Confirm newsletter email address/);
|
||||||
await expect(page.getByTestId('confirmation-modal')).toHaveText(/previous email address \(current@test.com\)/);
|
await expect(page.getByTestId('confirmation-modal')).toHaveText(/previous email address \(current@test.com\)/);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('For Ghost (Pro) users without custom domain', () => {
|
||||||
|
test('Does not allow the Sender email address to be edited', async ({page}) => {
|
||||||
|
await mockApi({page, requests: {
|
||||||
|
...globalDataRequests,
|
||||||
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||||
|
browseConfig: {
|
||||||
|
...globalDataRequests.browseConfig,
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
...responseFixtures.config.config,
|
||||||
|
hostSettings: {
|
||||||
|
managedEmail: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
const section = page.getByTestId('newsletters');
|
||||||
|
await section.getByText('Awesome newsletter').click();
|
||||||
|
const modal = page.getByTestId('newsletter-modal');
|
||||||
|
const senderEmailField = modal.getByLabel('Sender email');
|
||||||
|
|
||||||
|
// Test that there is no input field near "Sender email"
|
||||||
|
const parentElementLocator = senderEmailField.locator('xpath=..');
|
||||||
|
const inputElementsNearby = await parentElementLocator.locator('input').count();
|
||||||
|
|
||||||
|
expect(inputElementsNearby).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Allow full customisation of the reply-to address', async ({page}) => {
|
||||||
|
await mockApi({page, requests: {
|
||||||
|
...globalDataRequests,
|
||||||
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||||
|
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||||
|
newsletters: [responseFixtures.newsletters.newsletters[0]],
|
||||||
|
meta: {
|
||||||
|
sent_email_verification: ['sender_reply_to']
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
browseConfig: {
|
||||||
|
...globalDataRequests.browseConfig,
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
...responseFixtures.config.config,
|
||||||
|
hostSettings: {
|
||||||
|
managedEmail: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
const section = page.getByTestId('newsletters');
|
||||||
|
await section.getByText('Awesome newsletter').click();
|
||||||
|
const modal = page.getByTestId('newsletter-modal');
|
||||||
|
const replyToEmail = modal.getByLabel('Reply-to email');
|
||||||
|
|
||||||
|
await replyToEmail.fill('not-an-email');
|
||||||
|
await modal.getByRole('button', {name: 'Save'}).click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('toast-error')).toHaveText(/Can't save newsletter/);
|
||||||
|
await expect(modal).toHaveText(/Invalid email/);
|
||||||
|
|
||||||
|
await replyToEmail.fill('test@test.com');
|
||||||
|
await modal.getByRole('button', {name: 'Save'}).click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('confirmation-modal')).toHaveCount(1);
|
||||||
|
await expect(page.getByTestId('confirmation-modal')).toHaveText(/Confirm reply-to address/);
|
||||||
|
await expect(page.getByTestId('confirmation-modal')).toHaveText(/previous reply-to address \(noreply@test.com\)/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('For Ghost (Pro) users with custom domain', () => {
|
||||||
|
test('Allow sender and reply-to addresses to be changed without verification, but not their domain name', async ({page}) => {
|
||||||
|
await mockApi({page, requests: {
|
||||||
|
...globalDataRequests,
|
||||||
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=50', response: responseFixtures.newsletters},
|
||||||
|
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||||
|
newsletters: [responseFixtures.newsletters.newsletters[0]],
|
||||||
|
meta: {
|
||||||
|
sent_email_verification: []
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
browseConfig: {
|
||||||
|
...globalDataRequests.browseConfig,
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
...responseFixtures.config.config,
|
||||||
|
hostSettings: {
|
||||||
|
managedEmail: {
|
||||||
|
enabled: true,
|
||||||
|
sendingDomain: 'customdomain.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
const section = page.getByTestId('newsletters');
|
||||||
|
await section.getByText('Awesome newsletter').click();
|
||||||
|
const modal = page.getByTestId('newsletter-modal');
|
||||||
|
const senderEmail = modal.getByLabel('Sender email');
|
||||||
|
const replyToEmail = modal.getByLabel('Reply-to address');
|
||||||
|
|
||||||
|
// The sending domain is rendered as placeholder text
|
||||||
|
expect(modal).toHaveText(/@customdomain\.com/);
|
||||||
|
|
||||||
|
// The sender email field should keep the username part of the email address
|
||||||
|
await senderEmail.fill('harry@potter.com');
|
||||||
|
expect(await senderEmail.inputValue()).toBe('harry');
|
||||||
|
|
||||||
|
// The sender email field should keep the username part of the email address
|
||||||
|
await replyToEmail.fill('hermione@granger.com');
|
||||||
|
expect(await replyToEmail.inputValue()).toBe('hermione');
|
||||||
|
|
||||||
|
// The new username is saved without a confirmation popup
|
||||||
|
await modal.getByRole('button', {name: 'Save'}).click();
|
||||||
|
await expect(page.getByTestId('confirmation-modal')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Supports archiving newsletters', async ({page}) => {
|
test('Supports archiving newsletters', async ({page}) => {
|
||||||
const activate = await mockApi({page, requests: {
|
const activate = await mockApi({page, requests: {
|
||||||
|
|
Loading…
Add table
Reference in a new issue