mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-15 03:01:37 -05:00
Added remaining wiring to AdminX Newsletters (#17587)
refs https://github.com/TryGhost/Product/issues/3601 - Wired up add newsletter modal - Fixed bugs with editing newsletters - Added archive/reactivate modals
This commit is contained in:
parent
d960b1284d
commit
21f57c5ab5
9 changed files with 239 additions and 125 deletions
|
@ -1,19 +1,14 @@
|
|||
import React from 'react';
|
||||
import React, {HTMLProps} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface TableCellProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const TableCell: React.FC<TableCellProps> = ({className, children}) => {
|
||||
const TableCell: React.FC<HTMLProps<HTMLTableCellElement>> = ({className, children, ...props}) => {
|
||||
const tableCellClasses = clsx(
|
||||
'!py-3 !pl-0 !pr-6 align-top',
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<td className={tableCellClasses}>
|
||||
<td className={tableCellClasses} {...props}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
|
|
@ -24,12 +24,12 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
{
|
||||
id: 'active-newsletters',
|
||||
title: 'Active',
|
||||
contents: (<NewslettersList newsletters={newsletters?.filter(newsletter => newsletter.status === 'active') || []} tab='active-newsletters' />)
|
||||
contents: (<NewslettersList newsletters={newsletters?.filter(newsletter => newsletter.status === 'active') || []} />)
|
||||
},
|
||||
{
|
||||
id: 'archived-newsletters',
|
||||
title: 'Archived',
|
||||
contents: (<NewslettersList newsletters={newsletters?.filter(newsletter => newsletter.status !== 'active') || []} tab='archive-newsletters' />)
|
||||
contents: (<NewslettersList newsletters={newsletters?.filter(newsletter => newsletter.status !== 'active') || []} />)
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -1,17 +1,56 @@
|
|||
import Form from '../../../../admin-x-ds/global/form/Form';
|
||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {useState} from 'react';
|
||||
import NewsletterDetailModal from './NewsletterDetailModal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
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 useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
import {toast} from 'react-hot-toast';
|
||||
import {useAddNewsletter} from '../../../../utils/api/newsletters';
|
||||
import {useBrowseMembers} from '../../../../utils/api/members';
|
||||
|
||||
interface AddNewsletterModalProps {}
|
||||
|
||||
const AddNewsletterModal: React.FC<AddNewsletterModalProps> = () => {
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
const [optIn, setOptIn] = useState(true);
|
||||
|
||||
const {data: members} = useBrowseMembers({
|
||||
searchParams: {filter: 'newsletters.status:active+email_disabled:0', limit: '1', page: '1', include: 'newsletters,labels'}
|
||||
});
|
||||
|
||||
const {mutateAsync: addNewsletter} = useAddNewsletter();
|
||||
const {formState, updateForm, handleSave, errors, validate, clearError} = useForm({
|
||||
initialState: {
|
||||
name: '',
|
||||
description: '',
|
||||
optInExistingSubscribers: true
|
||||
},
|
||||
onSave: async () => {
|
||||
const response = await addNewsletter({
|
||||
name: formState.name,
|
||||
description: formState.description,
|
||||
opt_in_existing: formState.optInExistingSubscribers
|
||||
});
|
||||
|
||||
NiceModal.show(NewsletterDetailModal, {
|
||||
newsletter: response.newsletters[0]
|
||||
});
|
||||
},
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formState.name) {
|
||||
newErrors.name = 'Please enter a name';
|
||||
}
|
||||
|
||||
return newErrors;
|
||||
}
|
||||
});
|
||||
|
||||
return <Modal
|
||||
afterClose={() => {
|
||||
|
@ -22,30 +61,51 @@ const AddNewsletterModal: React.FC<AddNewsletterModalProps> = () => {
|
|||
size='sm'
|
||||
testId='add-newsletter-modal'
|
||||
title='Create newsletter'
|
||||
onOk={async () => {
|
||||
toast.remove();
|
||||
if (await handleSave()) {
|
||||
modal.remove();
|
||||
updateRoute('newsletters');
|
||||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save newsletter! One or more fields have errors, please doublecheck you filled all mandatory fields'
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
marginBottom={false}
|
||||
marginTop
|
||||
>
|
||||
<TextField
|
||||
error={Boolean(errors.name)}
|
||||
hint={errors.name}
|
||||
placeholder='Weekly roundup'
|
||||
title='Name'
|
||||
value={formState.name}
|
||||
onBlur={validate}
|
||||
onChange={e => updateForm(state => ({...state, name: e.target.value}))}
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextArea
|
||||
title='Description'
|
||||
value={formState.description}
|
||||
onChange={e => updateForm(state => ({...state, description: e.target.value}))}
|
||||
/>
|
||||
<Toggle
|
||||
checked={optIn}
|
||||
checked={formState.optInExistingSubscribers}
|
||||
direction='rtl'
|
||||
hint='This newsletter will be available to all members. Your 1 existing subscriber will also be opted-in to receive it.'
|
||||
hint={formState.optInExistingSubscribers ?
|
||||
`This newsletter will be available to all members. Your ${members?.meta?.pagination.total} existing subscriber${members?.meta?.pagination.total === 1 ? '' : 's'} will also be opted-in to receive it.` :
|
||||
'The newsletter will be available to all new members. Existing members won’t be subscribed, but may visit their account area to opt-in to future emails.'
|
||||
}
|
||||
label='Opt-in existing subscribers'
|
||||
labelStyle='heading'
|
||||
onChange={(e) => {
|
||||
setOptIn(e.target.checked);
|
||||
}}
|
||||
onChange={e => updateForm(state => ({...state, optInExistingSubscribers: e.target.checked}))}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>;
|
||||
};
|
||||
|
||||
export default NiceModal.create(AddNewsletterModal);
|
||||
export default NiceModal.create(AddNewsletterModal);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Form from '../../../../admin-x-ds/global/form/Form';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
|
||||
import ButtonGroup from '../../../../admin-x-ds/global/ButtonGroup';
|
||||
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';
|
||||
|
@ -16,10 +16,13 @@ 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 useForm from '../../../../hooks/useForm';
|
||||
import validator from 'validator';
|
||||
import {Newsletter} from '../../../../types/api';
|
||||
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
|
||||
import {fullEmailAddress, getSettingValues} from '../../../../utils/helpers';
|
||||
import {getImageUrl, useUploadImage} from '../../../../utils/api/images';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
import {toast} from 'react-hot-toast';
|
||||
import {useEditNewsletter} from '../../../../utils/api/newsletters';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
|
||||
|
@ -30,8 +33,11 @@ interface NewsletterDetailModalProps {
|
|||
const Sidebar: React.FC<{
|
||||
newsletter: Newsletter;
|
||||
updateNewsletter: (fields: Partial<Newsletter>) => void;
|
||||
}> = ({newsletter, updateNewsletter}) => {
|
||||
const {settings, siteData} = useGlobalData();
|
||||
validate: () => void;
|
||||
errors: Record<string, string>;
|
||||
clearError: (field: string) => void;
|
||||
}> = ({newsletter, updateNewsletter, validate, errors, clearError}) => {
|
||||
const {settings, siteData, config} = useGlobalData();
|
||||
const [membersSupportAddress] = getSettingValues<string>(settings, ['members_support_address']);
|
||||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const [selectedTab, setSelectedTab] = useState('generalSettings');
|
||||
|
@ -53,12 +59,30 @@ const Sidebar: React.FC<{
|
|||
contents:
|
||||
<>
|
||||
<Form className='mt-6' gap='sm' margins='lg' title='Name and description'>
|
||||
<TextField placeholder="Weekly Roundup" title="Name" value={newsletter.name || ''} onChange={e => updateNewsletter({name: e.target.value})} />
|
||||
<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="Ghost" title="Sender name" value={newsletter.sender_name || ''} onChange={e => updateNewsletter({sender_name: e.target.value})} />
|
||||
<TextField placeholder="noreply@localhost" title="Sender email address" value={newsletter.sender_email || ''} onChange={e => updateNewsletter({sender_email: e.target.value})} />
|
||||
<TextField
|
||||
error={Boolean(errors.sender_email)}
|
||||
hint={errors.sender_email}
|
||||
placeholder="noreply@localhost"
|
||||
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'>
|
||||
|
@ -209,12 +233,13 @@ const Sidebar: React.FC<{
|
|||
onChange={e => updateNewsletter({show_subscription_details: e.target.checked})}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
<TextArea
|
||||
hint="Any extra information or legal text"
|
||||
rows={2}
|
||||
title="Email footer"
|
||||
<HtmlField
|
||||
config={config}
|
||||
hint='Any extra information or legal text'
|
||||
nodes='MINIMAL_NODES'
|
||||
title='Email footer'
|
||||
value={newsletter.footer_content || ''}
|
||||
onChange={e => updateNewsletter({footer_content: e.target.value})}
|
||||
onChange={html => updateNewsletter({footer_content: html})}
|
||||
/>
|
||||
</Form>
|
||||
</>
|
||||
|
@ -255,11 +280,24 @@ const NewsletterDetailModal: React.FC<NewsletterDetailModalProps> = ({newsletter
|
|||
const modal = useModal();
|
||||
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||
|
||||
const {formState, updateForm, handleSave} = useForm({
|
||||
const {formState, updateForm, handleSave, validate, errors, clearError} = useForm({
|
||||
initialState: newsletter,
|
||||
onSave: async () => {
|
||||
await editNewsletter(formState);
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -268,7 +306,7 @@ const NewsletterDetailModal: React.FC<NewsletterDetailModalProps> = ({newsletter
|
|||
};
|
||||
|
||||
const preview = <NewsletterPreview newsletter={formState} />;
|
||||
const sidebar = <Sidebar newsletter={formState} updateNewsletter={updateNewsletter} />;
|
||||
const sidebar = <Sidebar clearError={clearError} errors={errors} newsletter={formState} updateNewsletter={updateNewsletter} validate={validate} />;
|
||||
|
||||
return <PreviewModalContent
|
||||
deviceSelector={false}
|
||||
|
@ -280,7 +318,17 @@ const NewsletterDetailModal: React.FC<NewsletterDetailModalProps> = ({newsletter
|
|||
sidebarPadding={false}
|
||||
testId='newsletter-modal'
|
||||
title='Newsletter'
|
||||
onOk={handleSave}
|
||||
onOk={async () => {
|
||||
toast.remove();
|
||||
if (await handleSave()) {
|
||||
modal.remove();
|
||||
} else {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save newsletter! One or more fields have errors, please doublecheck you filled all mandatory fields'
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
|
||||
import NewsletterDetailModal from './NewsletterDetailModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
|
||||
|
@ -7,98 +8,78 @@ import Table from '../../../../admin-x-ds/global/Table';
|
|||
import TableCell from '../../../../admin-x-ds/global/TableCell';
|
||||
import TableRow from '../../../../admin-x-ds/global/TableRow';
|
||||
import {Newsletter} from '../../../../types/api';
|
||||
import {useEditNewsletter} from '../../../../utils/api/newsletters';
|
||||
|
||||
interface NewslettersListProps {
|
||||
tab?: string;
|
||||
newsletters: Newsletter[]
|
||||
}
|
||||
|
||||
// We should create a NewsletterItem component based on TableRow and then loop through newsletters
|
||||
//
|
||||
// interface NewsletterItemProps {
|
||||
// name: string;
|
||||
// description: string;
|
||||
// subscribers: number;
|
||||
// emailsSent: number;
|
||||
// }
|
||||
const NewsletterItem: React.FC<{newsletter: Newsletter}> = ({newsletter}) => {
|
||||
const {mutateAsync: editNewsletter} = useEditNewsletter();
|
||||
|
||||
// const NewsletterItem: React.FC<NewsletterItemProps> = ({name, description, subscribers, emailsSent}) => {
|
||||
// const action = tab === 'active-newsletters' ? (
|
||||
// <Button color='green' label='Archive' link />
|
||||
// ) : (
|
||||
// <Button color='green' label='Activate' link />
|
||||
// );
|
||||
|
||||
// return (
|
||||
// <TableRow
|
||||
// action={action}
|
||||
// onClick={() => {
|
||||
// NiceModal.show(NewsletterDetailModal);
|
||||
// }}>
|
||||
// hideActions
|
||||
// separator
|
||||
// >
|
||||
// <TableCell>
|
||||
// <div className={`flex grow flex-col`}>
|
||||
// <span className='font-medium'>{name}</span>
|
||||
// <span className='whitespace-nowrap text-xs text-grey-700'>{description}</span>
|
||||
// </div>
|
||||
// </TableCell>
|
||||
// <TableCell>
|
||||
// <div className={`flex grow flex-col`}>
|
||||
// <span>{subscribers}</span>
|
||||
// <span className='whitespace-nowrap text-xs text-grey-700'>Subscribers</span>
|
||||
// </div>
|
||||
// </TableCell>
|
||||
// <TableCell>
|
||||
// <div className={`flex grow flex-col`}>
|
||||
// <span>{emailsSent}</span>
|
||||
// <span className='whitespace-nowrap text-xs text-grey-700'>Emails sent</span>
|
||||
// </div>
|
||||
// </TableCell>
|
||||
// </TableRow>
|
||||
// );
|
||||
// };
|
||||
|
||||
const NewslettersList: React.FC<NewslettersListProps> = ({
|
||||
tab,
|
||||
newsletters
|
||||
}) => {
|
||||
const action = tab === 'active-newsletters' ? (
|
||||
<Button color='green' label='Archive' link />
|
||||
const action = newsletter.status === 'active' ? (
|
||||
<Button color='green' label='Archive' link onClick={() => {
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Archive newsletter',
|
||||
prompt: <>
|
||||
<p>Your newsletter <strong>{newsletter.name}</strong> will no longer be visible to members or available as an option when publishing new posts.</p>
|
||||
<p>Existing posts previously sent as this newsletter will remain unchanged.</p>
|
||||
</>,
|
||||
okLabel: 'Archive',
|
||||
onOk: async (modal) => {
|
||||
await editNewsletter({...newsletter, status: 'archived'});
|
||||
modal?.remove();
|
||||
}
|
||||
});
|
||||
}} />
|
||||
) : (
|
||||
<Button color='green' label='Activate' link />
|
||||
<Button color='green' label='Activate' link onClick={() => {
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Reactivate newsletter',
|
||||
prompt: <>
|
||||
Reactivating <strong>{newsletter.name}</strong> will immediately make it visible to members and re-enable it as an option when publishing new posts.
|
||||
</>,
|
||||
okLabel: 'Reactivate',
|
||||
onOk: async (modal) => {
|
||||
await editNewsletter({...newsletter, status: 'active'});
|
||||
modal?.remove();
|
||||
}
|
||||
});
|
||||
}} />
|
||||
);
|
||||
|
||||
const showDetails = () => {
|
||||
NiceModal.show(NewsletterDetailModal, {newsletter});
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow action={action} hideActions>
|
||||
<TableCell onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span className='font-medium'>{newsletter.name}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>{newsletter.description || 'No description'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{newsletter.count?.active_members}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>Subscribers</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell onClick={showDetails}>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{newsletter.count?.posts}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>Posts sent</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const NewslettersList: React.FC<NewslettersListProps> = ({newsletters}) => {
|
||||
if (newsletters.length) {
|
||||
return <Table>
|
||||
{newsletters.map(newsletter => (
|
||||
<TableRow
|
||||
action={action}
|
||||
hideActions
|
||||
onClick={() => {
|
||||
NiceModal.show(NewsletterDetailModal, {newsletter});
|
||||
}}>
|
||||
<TableCell>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span className='font-medium'>{newsletter.name}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>{newsletter.description || 'No description'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{newsletter.count?.active_members}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>Subscribers</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<span>{newsletter.count?.posts}</span>
|
||||
<span className='whitespace-nowrap text-xs text-grey-700'>Posts sent</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{newsletters.map(newsletter => <NewsletterItem key={newsletter.id} newsletter={newsletter} />)}
|
||||
</Table>;
|
||||
} else {
|
||||
return <NoValueLabel icon='mail-block'>
|
||||
|
|
|
@ -25,7 +25,7 @@ const Sidebar: React.FC<{
|
|||
updateBrandSetting: (key: string, value: SettingValue) => void
|
||||
updateThemeSetting: (updated: CustomThemeSetting) => void
|
||||
onTabChange: (id: string) => void
|
||||
handleSave: () => Promise<void>
|
||||
handleSave: () => Promise<boolean>
|
||||
}> = ({
|
||||
brandSettings,
|
||||
themeSettingSections,
|
||||
|
|
|
@ -9,7 +9,7 @@ export type SaveState = 'unsaved' | 'saving' | 'saved' | 'error' | '';
|
|||
export interface FormHook<State> {
|
||||
formState: State;
|
||||
saveState: SaveState;
|
||||
handleSave: () => Promise<void>;
|
||||
handleSave: () => Promise<boolean>;
|
||||
/**
|
||||
* Update the form state and mark the form as dirty. Should be used in input events
|
||||
*/
|
||||
|
@ -19,16 +19,21 @@ export interface FormHook<State> {
|
|||
*/
|
||||
setFormState: (updater: (state: State) => State) => void;
|
||||
reset: () => void;
|
||||
|
||||
validate: () => boolean;
|
||||
clearError: (field: string) => void;
|
||||
isValid: boolean;
|
||||
errors: Record<string, string>;
|
||||
}
|
||||
|
||||
// TODO: figure out if we need to extend `any`?
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
|
||||
const useForm = <State>({initialState, onSave}: {
|
||||
const useForm = <State>({initialState, onSave, onValidate}: {
|
||||
initialState: State,
|
||||
onSave: () => void | Promise<void>
|
||||
onValidate?: () => Record<string, string>
|
||||
}): FormHook<State> => {
|
||||
const [formState, setFormState] = useState(initialState);
|
||||
const [saveState, setSaveState] = useState<SaveState>('');
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Reset saved state after 2 seconds
|
||||
useEffect(() => {
|
||||
|
@ -39,16 +44,33 @@ const useForm = <State>({initialState, onSave}: {
|
|||
}
|
||||
}, [saveState]);
|
||||
|
||||
const isValid = (errs: Record<string, string>) => Object.values(errs).filter(Boolean).length === 0;
|
||||
|
||||
const validate = () => {
|
||||
if (!onValidate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const newErrors = onValidate();
|
||||
setErrors(newErrors);
|
||||
return isValid(newErrors);
|
||||
};
|
||||
|
||||
// function to save the changed settings via API
|
||||
const handleSave = async () => {
|
||||
if (saveState !== 'unsaved') {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setSaveState('saving');
|
||||
try {
|
||||
await onSave();
|
||||
setSaveState('saved');
|
||||
return true;
|
||||
} catch (e) {
|
||||
setSaveState('unsaved');
|
||||
throw e;
|
||||
|
@ -69,7 +91,13 @@ const useForm = <State>({initialState, onSave}: {
|
|||
reset() {
|
||||
setFormState(initialState);
|
||||
setSaveState('');
|
||||
}
|
||||
},
|
||||
validate,
|
||||
isValid: isValid(errors),
|
||||
clearError: (field: string) => {
|
||||
setErrors(state => ({...state, [field]: ''}));
|
||||
},
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface SettingGroupHook {
|
|||
saveState: SaveState;
|
||||
siteData: SiteData | null;
|
||||
focusRef: React.RefObject<HTMLInputElement>;
|
||||
handleSave: () => Promise<void>;
|
||||
handleSave: () => Promise<boolean>;
|
||||
handleCancel: () => void;
|
||||
updateSetting: (key: string, value: SettingValue) => void;
|
||||
handleEditingChange: (newState: boolean) => void;
|
||||
|
|
|
@ -14,10 +14,12 @@ export const useBrowseNewsletters = createQuery<NewslettersResponseType>({
|
|||
defaultSearchParams: {include: 'count.active_members,count.posts', limit: 'all'}
|
||||
});
|
||||
|
||||
export const useAddNewsletter = createMutation<NewslettersResponseType, Partial<Newsletter>>({
|
||||
export const useAddNewsletter = createMutation<NewslettersResponseType, Partial<Newsletter> & {opt_in_existing: boolean}>({
|
||||
method: 'POST',
|
||||
path: () => '/newsletters/',
|
||||
body: newsletter => ({newsletters: [newsletter]}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
body: ({opt_in_existing: _, ...newsletter}) => ({newsletters: [newsletter]}),
|
||||
searchParams: payload => ({opt_in_existing: payload.opt_in_existing.toString(), include: 'count.active_members,count.posts', limit: 'all'}),
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: (newData, currentData) => ({
|
||||
|
|
Loading…
Add table
Reference in a new issue