0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00
ghost/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModal.tsx
Jono M a79de45392
Fixed tiers and scrolling bugs with AdminX (#18138)
refs https://github.com/TryGhost/Product/issues/3832

- Updated tier benefits to be added when pressing enter
- Updated tier sorting logic to make sure free comes first
- Updated scrolling behaviour and made the sidebar scroll automatically
- Added placeholder backdrop when loading a modal to prevent flashing

And other minor fixes
- Removed placeholder from newsletter footer field
- Updated theme installation to work with the default theme
2023-09-14 14:09:51 +00:00

457 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import ButtonGroup from '../../../../admin-x-ds/global/ButtonGroup';
import ColorPickerField from '../../../../admin-x-ds/global/form/ColorPickerField';
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
import Form from '../../../../admin-x-ds/global/form/Form';
import Heading from '../../../../admin-x-ds/global/Heading';
import Hint from '../../../../admin-x-ds/global/Hint';
import HtmlField from '../../../../admin-x-ds/global/form/HtmlField';
import Icon from '../../../../admin-x-ds/global/Icon';
import ImageUpload from '../../../../admin-x-ds/global/form/ImageUpload';
import NewsletterPreview from './NewsletterPreview';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import Select, {SelectOption} from '../../../../admin-x-ds/global/form/Select';
import StickyFooter from '../../../../admin-x-ds/global/StickyFooter';
import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import validator from 'validator';
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '../../../../api/newsletters';
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
import {RoutingModalProps} from '../../../providers/RoutingProvider';
import {fullEmailAddress} from '../../../../api/site';
import {getImageUrl, useUploadImage} from '../../../../api/images';
import {getSettingValues} from '../../../../api/settings';
import {showToast} from '../../../../admin-x-ds/global/Toast';
import {textColorForBackgroundColor} from '@tryghost/color-utils';
import {toast} from 'react-hot-toast';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
const Sidebar: React.FC<{
newsletter: Newsletter;
updateNewsletter: (fields: Partial<Newsletter>) => void;
validate: () => void;
errors: ErrorMessages;
clearError: (field: string) => void;
}> = ({newsletter, updateNewsletter, validate, errors, clearError}) => {
const {settings, siteData, config} = useGlobalData();
const [membersSupportAddress, icon] = getSettingValues<string>(settings, ['members_support_address', 'icon']);
const {mutateAsync: uploadImage} = useUploadImage();
const [selectedTab, setSelectedTab] = useState('generalSettings');
const hasEmailCustomization = useFeatureFlag('emailCustomization');
const {localSettings} = useSettingGroup();
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
const replyToEmails = [
{label: `Newsletter address (${fullEmailAddress(newsletter.sender_email || 'noreply', siteData)})`, value: 'newsletter'},
{label: `Support address (${fullEmailAddress(membersSupportAddress || 'noreply', siteData)})`, value: 'support'}
];
const fontOptions: SelectOption[] = [
{value: 'serif', label: 'Elegant serif', className: 'font-serif'},
{value: 'sans_serif', label: 'Clean sans-serif'}
];
const backgroundColorIsDark = () => {
if (newsletter.background_color === 'dark') {
return true;
}
if (newsletter.background_color === 'light') {
return false;
}
return textColorForBackgroundColor(newsletter.background_color).hex().toLowerCase() === '#ffffff';
};
const tabs: Tab[] = [
{
id: 'generalSettings',
title: 'General',
contents:
<>
<Form className='mt-6' gap='sm' margins='lg' title='Name and description'>
<TextField
error={Boolean(errors.name)}
hint={errors.name}
placeholder="Weekly Roundup"
title="Name"
value={newsletter.name || ''}
onBlur={validate}
onChange={e => updateNewsletter({name: e.target.value})}
onKeyDown={() => clearError('name')}
/>
<TextArea rows={2} title="Description" value={newsletter.description || ''} onChange={e => updateNewsletter({description: e.target.value})} />
</Form>
<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
error={Boolean(errors.sender_email)}
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={newsletter.sender_reply_to} title="Reply-to email" onSelect={value => updateNewsletter({sender_reply_to: value})}/>
</Form>
<Form className='mt-6' gap='sm' margins='lg' title='Member settings'>
<Toggle
checked={newsletter.subscribe_on_signup}
direction='rtl'
label='Subscribe new members on signup'
labelStyle='value'
onChange={e => updateNewsletter({subscribe_on_signup: e.target.checked})}
/>
</Form>
</>
},
{
id: 'design',
title: 'Design',
contents:
<>
<Form className='mt-6' gap='sm' margins='lg' title='Header'>
<div>
<div>
<Heading className="mb-2" level={6}>Header image</Heading>
</div>
<div className='flex-column flex gap-1'>
<ImageUpload
deleteButtonClassName='!top-1 !right-1'
height={newsletter.header_image ? '66px' : '64px'}
id='logo'
imageURL={newsletter.header_image || undefined}
onDelete={() => {
updateNewsletter({header_image: null});
}}
onUpload={async (file) => {
const imageUrl = getImageUrl(await uploadImage({file}));
updateNewsletter({header_image: imageUrl});
}}
>
Upload header image
</ImageUpload>
<Hint>Optional, recommended size 1200x600</Hint>
</div>
</div>
<ToggleGroup>
{icon && <Toggle
checked={newsletter.show_header_icon}
direction="rtl"
label='Publication icon'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_icon: e.target.checked})}
/>}
<Toggle
checked={newsletter.show_header_title}
direction="rtl"
label='Publication title'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_title: e.target.checked})}
/>
<Toggle
checked={newsletter.show_header_name}
direction="rtl"
label='Newsletter name'
labelStyle='heading'
onChange={e => updateNewsletter({show_header_name: e.target.checked})}
/>
</ToggleGroup>
</Form>
<Form className='mt-6' gap='sm' margins='lg' title='Body'>
{hasEmailCustomization && <>
<ColorPickerField
direction='rtl'
swatches={[
{
hex: '#f0f0f0',
title: 'Light grey'
},
{
hex: '#ffffff',
value: 'light',
title: 'White'
}
]}
title='Background color'
value={newsletter.background_color || 'light'}
onChange={color => updateNewsletter({background_color: color!})}
/>
<ColorPickerField
clearButtonValue={null}
direction='rtl'
swatches={[
{
hex: siteData.accent_color,
value: 'accent',
title: 'Accent'
},
{
hex: backgroundColorIsDark() ? '#ffffff' : '#000000',
value: 'auto',
title: 'Auto'
},
{
value: null,
title: 'Transparent',
hex: '#00000000'
}
]}
title='Border color'
value={newsletter.border_color}
onChange={color => updateNewsletter({border_color: color})}
/>
</>}
<Toggle
checked={newsletter.show_post_title_section}
direction="rtl"
label='Post title'
labelStyle='heading'
onChange={e => updateNewsletter({show_post_title_section: e.target.checked})}
/>
<div className={newsletter.show_post_title_section ? 'mt-[-16px] flex items-end' : 'hidden'}>
<div className="w-full pr-4">
<Select
disabled={!newsletter.show_post_title_section}
options={fontOptions}
selectedOption={newsletter.title_font_category}
onSelect={value => updateNewsletter({title_font_category: value})}
/>
</div>
<ButtonGroup buttons={[
{
icon: 'align-left',
label: 'Align left',
hideLabel: true,
link: false,
size: 'sm',
color: newsletter.title_alignment === 'left' ? 'clear' : 'clear',
iconColorClass: newsletter.title_alignment === 'left' ? 'text-grey-900' : 'text-grey-500',
onClick: () => updateNewsletter({title_alignment: 'left'}),
disabled: !newsletter.show_post_title_section
},
{
icon: 'align-center',
label: 'Align center',
hideLabel: true,
link: false,
size: 'sm',
color: newsletter.title_alignment === 'center' ? 'clear' : 'clear',
iconColorClass: newsletter.title_alignment === 'center' ? 'text-grey-900' : 'text-grey-500',
onClick: () => updateNewsletter({title_alignment: 'center'}),
disabled: !newsletter.show_post_title_section
}
]}
className="mb-1 !gap-0"
/>
</div>
{hasEmailCustomization && <ColorPickerField
direction='rtl'
swatches={[
{
value: 'accent',
title: 'Accent',
hex: siteData.accent_color
},
{
value: null,
title: 'Auto',
hex: backgroundColorIsDark() ? '#ffffff' : '#000000'
}
]}
title='Heading color'
value={newsletter.title_color}
onChange={color => updateNewsletter({title_color: color})}
/>}
<Select
options={fontOptions}
selectedOption={newsletter.body_font_category}
title='Body style'
onSelect={value => updateNewsletter({body_font_category: value})}
/>
<Toggle
checked={newsletter.show_feature_image}
direction="rtl"
label='Feature image'
labelStyle='heading'
onChange={e => updateNewsletter({show_feature_image: e.target.checked})}
/>
</Form>
<Form className='mt-6' gap='sm' margins='lg' title='Footer'>
<ToggleGroup gap='lg'>
<Toggle
checked={newsletter.feedback_enabled}
direction="rtl"
label='Ask your readers for feedback'
labelStyle='heading'
onChange={e => updateNewsletter({feedback_enabled: e.target.checked})}
/>
<Toggle
checked={newsletter.show_comment_cta}
direction="rtl"
label='Add a link to your comments'
labelStyle='heading'
onChange={e => updateNewsletter({show_comment_cta: e.target.checked})}
/>
<Toggle
checked={newsletter.show_latest_posts}
direction="rtl"
label='Share your latest posts'
labelStyle='heading'
onChange={e => updateNewsletter({show_latest_posts: e.target.checked})}
/>
<Toggle
checked={newsletter.show_subscription_details}
direction="rtl"
label='Show subscription details'
labelStyle='heading'
onChange={e => updateNewsletter({show_subscription_details: e.target.checked})}
/>
</ToggleGroup>
<HtmlField
config={config}
hint='Any extra information or legal text'
nodes='MINIMAL_NODES'
placeholder=' '
title='Email footer'
value={newsletter.footer_content || ''}
onChange={html => updateNewsletter({footer_content: html})}
/>
</Form>
</>
}
];
const handleTabChange = (id: string) => {
setSelectedTab(id);
};
return (
<div className='flex h-full flex-col justify-between'>
<div className='px-7 pb-7 pt-5'>
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={handleTabChange} />
</div>
<StickyFooter height={96}>
<div className='flex w-full items-start px-7'>
<span>
<Icon className='mr-2 mt-[-1px]' colorClass='text-red' name='heart'/>
</span>
<Form marginBottom={false}>
<Toggle
checked={newsletter.show_badge}
direction='rtl'
label={
<div className='flex flex-col gap-0.5'>
<span className='text-sm md:text-base'>Promote independent publishing</span>
<span className='text-[11px] leading-tight text-grey-700 md:text-xs md:leading-tight'>Show youre a part of the indie publishing movement with a small badge in the footer</span>
</div>
}
labelStyle='value'
onChange={e => updateNewsletter({show_badge: e.target.checked})}
/>
</Form>
</div>
</StickyFooter>
</div>
);
};
const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter}> = ({newsletter}) => {
const modal = useModal();
const {siteData} = useGlobalData();
const {mutateAsync: editNewsletter} = useEditNewsletter();
const {updateRoute} = useRouting();
const {formState, updateForm, handleSave, validate, errors, clearError} = useForm({
initialState: newsletter,
onSave: async () => {
const {newsletters, meta} = await editNewsletter(formState);
if (meta?.sent_email_verification) {
NiceModal.show(ConfirmationModal, {
title: 'Confirm newsletter email address',
prompt: <>
We&lsquo;ve sent a confirmation email to <strong>{formState.sender_email}</strong>.
Until the address has been verified newsletters will be sent from the
{newsletters[0].sender_email ? ' previous' : ' default'} email address
({fullEmailAddress(newsletters[0].sender_email || 'noreply', siteData)}).
</>,
cancelLabel: '',
onOk: (confirmModal) => {
confirmModal?.remove();
modal.remove();
}
});
} else {
modal.remove();
}
},
onValidate: () => {
const newErrors: Record<string, string> = {};
if (!formState.name) {
newErrors.name = 'Please enter a name';
}
if (formState.sender_email && !validator.isEmail(formState.sender_email)) {
newErrors.sender_email = 'Invalid email.';
}
return newErrors;
}
});
const updateNewsletter = (fields: Partial<Newsletter>) => {
updateForm(state => ({...state, ...fields}));
};
const preview = <NewsletterPreview newsletter={formState} />;
const sidebar = <Sidebar clearError={clearError} errors={errors} newsletter={formState} updateNewsletter={updateNewsletter} validate={validate} />;
return <PreviewModalContent
afterClose={() => updateRoute('newsletters')}
deviceSelector={false}
okLabel='Save & close'
preview={preview}
previewBgColor={'grey'}
previewToolbar={false}
sidebar={sidebar}
sidebarPadding={false}
testId='newsletter-modal'
title='Newsletter'
onOk={async () => {
toast.remove();
if (await handleSave()) {
modal.remove();
updateRoute('newsletters');
} else {
showToast({
type: 'pageError',
message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.'
});
}
}}
/>;
};
const NewsletterDetailModal: React.FC<RoutingModalProps> = ({params}) => {
const {data: {newsletters} = {}} = useBrowseNewsletters();
const newsletter = newsletters?.find(({id}) => id === params?.id);
if (newsletter) {
return <NewsletterDetailModalContent newsletter={newsletter} />;
} else {
return null;
}
};
export default NiceModal.create(NewsletterDetailModal);