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:
parent
5f30e935b3
commit
8fd9d92944
7 changed files with 93 additions and 27 deletions
|
@ -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={{
|
||||
|
|
|
@ -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} />)
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 () => {}};
|
||||
|
|
Loading…
Add table
Reference in a new issue