0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Added basic wiring to save tiers from the tier modal (#17311)

refs https://github.com/TryGhost/Product/issues/3580
This commit is contained in:
Jono M 2023-07-12 13:16:07 +12:00 committed by GitHub
parent 5f30e935b3
commit 8fd9d92944
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 93 additions and 27 deletions

View file

@ -1,6 +1,6 @@
import React, {createContext, useContext, useMemo} from 'react';
import setupGhostApi from '../../utils/api';
import useDataService, {DataService, bulkEdit} from '../../utils/dataService';
import useDataService, {DataService, bulkEdit, placeholderDataService} from '../../utils/dataService';
import useSearchService, {SearchService} from '../../utils/search';
import {OfficialTheme} from '../../models/themes';
import {Tier} from '../../types/api';
@ -27,7 +27,7 @@ const ServicesContext = createContext<ServicesContextProps>({
fileService: null,
officialThemes: [],
search: {filter: '', setFilter: () => {}, checkVisible: () => true},
tiers: {data: [], update: async () => {}}
tiers: placeholderDataService
});
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, officialThemes}) => {
@ -39,7 +39,12 @@ const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersi
}
}), [apiService]);
const search = useSearchService();
const tiers = useDataService({key: 'tiers', browse: apiService.tiers.browse, edit: bulkEdit('tiers', apiService.tiers.edit)});
const tiers = useDataService({
key: 'tiers',
browse: apiService.tiers.browse,
edit: bulkEdit('tiers', apiService.tiers.edit),
add: apiService.tiers.add
});
return (
<ServicesContext.Provider value={{

View file

@ -13,7 +13,7 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
updateRoute('tiers/add');
};
const [selectedTab, setSelectedTab] = useState('active-tiers');
const {data: tiers, update: updateTiers} = useTiers();
const {data: tiers, update: updateTier} = useTiers();
const activeTiers = getPaidActiveTiers(tiers);
const archivedTiers = getArchivedTiers(tiers);
@ -27,12 +27,12 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
{
id: 'active-tiers',
title: 'Active',
contents: (<TiersList tab='active-tiers' tiers={activeTiers} updateTiers={updateTiers} />)
contents: (<TiersList tab='active-tiers' tiers={activeTiers} updateTier={updateTier} />)
},
{
id: 'archived-tiers',
title: 'Archived',
contents: (<TiersList tab='archive-tiers' tiers={archivedTiers} updateTiers={updateTiers} />)
contents: (<TiersList tab='archive-tiers' tiers={archivedTiers} updateTier={updateTier} />)
}
];

View file

@ -77,7 +77,7 @@ const PortalModal: React.FC = () => {
},
onSave: async () => {
await updateTiers(formState.tiers.filter(tier => tier.dirty));
await updateTiers(...formState.tiers.filter(tier => tier.dirty));
const {meta, settings: currentSettings} = await saveSettings(formState.settings.filter(setting => setting.dirty));
if (meta?.sent_email_verification) {

View file

@ -1,19 +1,47 @@
import Form from '../../../../admin-x-ds/global/form/Form';
import Heading from '../../../../admin-x-ds/global/Heading';
import Modal from '../../../../admin-x-ds/global/modal/Modal';
import NiceModal from '@ebay/nice-modal-react';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import TextField from '../../../../admin-x-ds/global/form/TextField';
import TierDetailPreview from './TierDetailPreview';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import useForm from '../../../../hooks/useForm';
import useRouting from '../../../../hooks/useRouting';
import {Tier} from '../../../../types/api';
import {useTiers} from '../../../providers/ServiceProvider';
interface TierDetailModalProps {
tier?: Tier
}
const TierDetailModal: React.FC<TierDetailModalProps> = () => {
const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
const modal = useModal();
const {updateRoute} = useRouting();
const {update: updateTier, create: createTier} = useTiers();
const {formState, updateForm, handleSave} = useForm({
initialState: {
...(tier || {}),
monthly_price: tier?.monthly_price?.toString() || '',
yearly_price: tier?.monthly_price?.toString() || '',
trial_days: tier?.trial_days?.toString() || ''
},
onSave: async () => {
const values = {
...formState,
monthly_price: parseFloat(formState.monthly_price),
yearly_price: parseFloat(formState.yearly_price),
trial_days: parseFloat(formState.trial_days)
};
if (tier?.id) {
await updateTier({...tier, ...values});
} else {
await createTier(values);
}
}
});
return <Modal
afterClose={() => {
updateRoute('tiers');
@ -21,28 +49,39 @@ const TierDetailModal: React.FC<TierDetailModalProps> = () => {
okLabel='Save & close'
size='lg'
title='Tier'
stickyFooter>
stickyFooter
onOk={async () => {
await handleSave();
modal.remove();
}}
>
<div className='mt-8 flex items-start gap-10'>
<div className='flex grow flex-col gap-10'>
<Form title='Basic'>
<TextField
placeholder='Bronze'
title='Name'
value={formState.name || ''}
onChange={e => updateForm(state => ({...state, name: e.target.value}))}
/>
<TextField
placeholder='Full access to premium content'
title='Description'
value={formState.description || ''}
onChange={e => updateForm(state => ({...state, description: e.target.value}))}
/>
<div className='flex gap-10'>
<div className='flex basis-1/2 flex-col gap-2'>
<TextField
placeholder='1'
title='Prices'
value='5'
value={formState.monthly_price}
onChange={e => updateForm(state => ({...state, monthly_price: e.target.value.replace(/[^\d.]/, '')}))}
/>
<TextField
placeholder='10'
value='50'
value={formState.yearly_price}
onChange={e => updateForm(state => ({...state, yearly_price: e.target.value.replace(/[^\d.]/, '')}))}
/>
</div>
<div className='basis-1/2'>
@ -55,7 +94,9 @@ const TierDetailModal: React.FC<TierDetailModalProps> = () => {
Members will be subscribed at full price once the trial ends. <a href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
</>}
placeholder='0'
value={formState.trial_days}
disabled
onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/^[\d.]/, '')}))}
/>
</div>
</div>
@ -72,4 +113,4 @@ const TierDetailModal: React.FC<TierDetailModalProps> = () => {
</Modal>;
};
export default NiceModal.create(TierDetailModal);
export default NiceModal.create(TierDetailModal);

View file

@ -9,25 +9,25 @@ import {Tier} from '../../../../types/api';
interface TiersListProps {
tab?: string;
tiers: Tier[];
updateTiers: (data: Tier[]) => Promise<void>;
updateTier: (data: Tier) => Promise<void>;
}
interface TierActionsProps {
tier: Tier;
updateTiers: (data: Tier[]) => Promise<void>;
updateTier: (data: Tier) => Promise<void>;
}
const TierActions: React.FC<TierActionsProps> = ({tier, updateTiers}) => {
const TierActions: React.FC<TierActionsProps> = ({tier, updateTier}) => {
if (tier.active) {
return (
<Button color='green' label='Archive' link onClick={() => {
updateTiers([{...tier, active: false}]);
updateTier({...tier, active: false});
}} />
);
} else {
return (
<Button color='green' label='Activate' link onClick={() => {
updateTiers([{...tier, active: true}]);
updateTier({...tier, active: true});
}}/>
);
}
@ -35,19 +35,19 @@ const TierActions: React.FC<TierActionsProps> = ({tier, updateTiers}) => {
const TiersList: React.FC<TiersListProps> = ({
tiers,
updateTiers
updateTier
}) => {
return (
<List>
{tiers.map((tier) => {
return (
<ListItem
action={<TierActions tier={tier} updateTiers={updateTiers} />}
action={<TierActions tier={tier} updateTier={updateTier} />}
detail={tier?.description || ''}
title={tier?.name}
hideActions
onClick={() => {
NiceModal.show(TierDetailModal);
NiceModal.show(TierDetailModal, {tier});
}}
/>
);
@ -56,4 +56,4 @@ const TiersList: React.FC<TiersListProps> = ({
);
};
export default TiersList;
export default TiersList;

View file

@ -171,6 +171,7 @@ export interface API {
tiers: {
browse: () => Promise<TiersResponseType>
edit: (newTier: Tier) => Promise<TiersResponseType>
add: (newTier: Partial<Tier>) => Promise<TiersResponseType>
};
labels: {
browse: () => Promise<LabelsResponseType>
@ -410,6 +411,14 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
});
const data: TiersResponseType = await response.json();
return data;
},
add: async (tier) => {
const response = await fetcher(`/tiers/`, {
method: 'POST',
body: JSON.stringify({tiers: [tier]})
});
const data: TiersResponseType = await response.json();
return data;
}
},
labels: {

View file

@ -2,18 +2,22 @@ import {useEffect, useState} from 'react';
export interface DataService<Data> {
data: Data[];
update: (data: Data[]) => Promise<void>;
update: (...data: Data[]) => Promise<void>;
create: (data: Partial<Data>) => Promise<void>;
}
// eslint-disable-next-line no-unused-vars
type BulkEditFunction<Data, DataKey extends string> = (newData: Data[]) => Promise<{ [k in DataKey]: Data[] }>
// eslint-disable-next-line no-unused-vars
type AddFunction<Data, DataKey extends string> = (newData: Partial<Data>) => Promise<{ [k in DataKey]: Data[] }>
const useDataService = <Data extends { id: string }, DataKey extends string>({key, browse, edit}: {
const useDataService = <Data extends { id: string }, DataKey extends string>({key, browse, edit, add}: {
key: DataKey
// eslint-disable-next-line no-unused-vars
browse: () => Promise<{ [k in DataKey]: Data[] }>
// eslint-disable-next-line no-unused-vars
edit: BulkEditFunction<Data, DataKey>
add: AddFunction<Data, DataKey>
}): DataService<Data> => {
const [data, setData] = useState<Data[]>([]);
@ -23,7 +27,7 @@ const useDataService = <Data extends { id: string }, DataKey extends string>({ke
});
}, [browse, key]);
const update = async (newData: Data[]) => {
const update = async (...newData: Data[]) => {
const response = await edit(newData);
setData(data.map((item) => {
const replacement = response[key].find(newItem => newItem.id === item.id);
@ -31,7 +35,12 @@ const useDataService = <Data extends { id: string }, DataKey extends string>({ke
}));
};
return {data, update};
const create = async (newData: Partial<Data>) => {
const response = await add(newData);
setData([...data, response[key][0]]);
};
return {data, update, create};
};
export default useDataService;
@ -51,3 +60,5 @@ export const bulkEdit = <Data extends { id: string }, DataKey extends string>(
} as { [k in DataKey]: Data[] };
};
};
export const placeholderDataService = {data: [], update: async () => {}, create: async () => {}};