mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Updated portal popup to support selecting and updating tiers (#17192)
refs https://github.com/TryGhost/Team/issues/3545
This commit is contained in:
parent
7f9f467fc6
commit
1cc55eda2e
13 changed files with 273 additions and 33 deletions
|
@ -19,8 +19,7 @@ type Story = StoryObj<typeof Checkbox>;
|
|||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Checkbox 1',
|
||||
id: 'my-radio-button'
|
||||
label: 'Checkbox 1'
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import Heading from '../Heading';
|
||||
import Hint from '../Hint';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useId, useState} from 'react';
|
||||
import Separator from '../Separator';
|
||||
|
||||
interface CheckboxProps {
|
||||
id: string;
|
||||
title?: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
error?:boolean;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
hint?: React.ReactNode;
|
||||
checked?: boolean;
|
||||
separator?: boolean;
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<CheckboxProps> = ({id, title, label, value, onChange, error, hint, checked, separator}) => {
|
||||
const Checkbox: React.FC<CheckboxProps> = ({title, label, value, onChange, disabled, error, hint, checked, separator}) => {
|
||||
const id = useId();
|
||||
const [isChecked, setIsChecked] = useState(checked);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -36,6 +37,7 @@ const Checkbox: React.FC<CheckboxProps> = ({id, title, label, value, onChange, e
|
|||
<input
|
||||
checked={isChecked}
|
||||
className="relative float-left mt-[3px] h-4 w-4 appearance-none border-2 border-solid border-grey-300 outline-none checked:border-green checked:bg-green checked:after:absolute checked:after:-mt-px checked:after:ml-[3px] checked:after:block checked:after:h-[11px] checked:after:w-[6px] checked:after:rotate-45 checked:after:border-[2px] checked:after:border-l-0 checked:after:border-t-0 checked:after:border-solid checked:after:border-white checked:after:bg-transparent checked:after:content-[''] hover:cursor-pointer focus:shadow-none focus:transition-[border-color_0.2s] dark:border-grey-600 dark:checked:border-green dark:checked:bg-green"
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
type='checkbox'
|
||||
value={value}
|
||||
|
@ -52,4 +54,4 @@ const Checkbox: React.FC<CheckboxProps> = ({id, title, label, value, onChange, e
|
|||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
export default Checkbox;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React, {createContext, useContext, useMemo} from 'react';
|
||||
import setupGhostApi from '../../utils/api';
|
||||
import useDataService, {DataService, bulkEdit} from '../../utils/dataService';
|
||||
import useSearchService, {SearchService} from '../../utils/search';
|
||||
import {OfficialTheme} from '../../models/themes';
|
||||
import {Tier} from '../../types/api';
|
||||
|
||||
export interface FileService {
|
||||
uploadImage: (file: File) => Promise<string>;
|
||||
|
@ -11,6 +13,7 @@ interface ServicesContextProps {
|
|||
fileService: FileService|null;
|
||||
officialThemes: OfficialTheme[];
|
||||
search: SearchService
|
||||
tiers: DataService<Tier>
|
||||
}
|
||||
|
||||
interface ServicesProviderProps {
|
||||
|
@ -23,7 +26,8 @@ const ServicesContext = createContext<ServicesContextProps>({
|
|||
api: setupGhostApi({ghostVersion: ''}),
|
||||
fileService: null,
|
||||
officialThemes: [],
|
||||
search: {filter: '', setFilter: () => {}, checkVisible: () => true}
|
||||
search: {filter: '', setFilter: () => {}, checkVisible: () => true},
|
||||
tiers: {data: [], update: async () => {}}
|
||||
});
|
||||
|
||||
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, officialThemes}) => {
|
||||
|
@ -35,13 +39,15 @@ 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)});
|
||||
|
||||
return (
|
||||
<ServicesContext.Provider value={{
|
||||
api: apiService,
|
||||
fileService,
|
||||
officialThemes,
|
||||
search
|
||||
search,
|
||||
tiers
|
||||
}}>
|
||||
{children}
|
||||
</ServicesContext.Provider>
|
||||
|
@ -57,3 +63,5 @@ export const useApi = () => useServices().api;
|
|||
export const useOfficialThemes = () => useServices().officialThemes;
|
||||
|
||||
export const useSearch = () => useServices().search;
|
||||
|
||||
export const useTiers = () => useServices().tiers;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
|
||||
import {Config, Setting, SiteData} from '../../types/api';
|
||||
import {ServicesContext} from './ServiceProvider';
|
||||
import {Setting, SiteData} from '../../types/api';
|
||||
|
||||
// Define the Settings Context
|
||||
interface SettingsContextProps {
|
||||
settings: Setting[] | null;
|
||||
saveSettings: (updatedSettings: Setting[]) => Promise<Setting[]>;
|
||||
siteData: SiteData | null;
|
||||
config: Config | null;
|
||||
}
|
||||
|
||||
interface SettingsProviderProps {
|
||||
|
@ -16,6 +17,7 @@ interface SettingsProviderProps {
|
|||
const SettingsContext = createContext<SettingsContextProps>({
|
||||
settings: null,
|
||||
siteData: null,
|
||||
config: null,
|
||||
saveSettings: async () => []
|
||||
});
|
||||
|
||||
|
@ -79,18 +81,23 @@ function deserializeSettings(settings: Setting[]): Setting[] {
|
|||
// Create a Settings Provider component
|
||||
const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
||||
const {api} = useContext(ServicesContext);
|
||||
const [settings, setSettings] = useState <Setting[] | null> (null);
|
||||
const [siteData, setSiteData] = useState <SiteData | null> (null);
|
||||
const [settings, setSettings] = useState<Setting[] | null> (null);
|
||||
const [siteData, setSiteData] = useState<SiteData | null> (null);
|
||||
const [config, setConfig] = useState<Config | null> (null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSettings = async (): Promise<void> => {
|
||||
try {
|
||||
// Make an API call to fetch the settings
|
||||
const data = await api.settings.browse();
|
||||
const siteDataRes = await api.site.browse();
|
||||
const [settingsData, siteDataResponse, configData] = await Promise.all([
|
||||
api.settings.browse(),
|
||||
api.site.browse(),
|
||||
api.config.browse()
|
||||
]);
|
||||
|
||||
setSettings(serialiseSettingsData(data.settings));
|
||||
setSiteData(siteDataRes.site);
|
||||
setSettings(serialiseSettingsData(settingsData.settings));
|
||||
setSiteData(siteDataResponse.site);
|
||||
setConfig(configData.config);
|
||||
} catch (error) {
|
||||
// Log error in settings API
|
||||
}
|
||||
|
@ -120,7 +127,7 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
|||
// Provide the settings and the saveSettings function to the children components
|
||||
return (
|
||||
<SettingsContext.Provider value={{
|
||||
settings, saveSettings, siteData
|
||||
settings, saveSettings, siteData, config
|
||||
}}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
|
|
|
@ -2,24 +2,28 @@ import AccountPage from './portal/AccountPage';
|
|||
import LookAndFeel from './portal/LookAndFeel';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import PortalPreview from './portal/PortalPreview';
|
||||
import React, {useState} from 'react';
|
||||
import React, {useContext, useState} from 'react';
|
||||
import SignupOptions from './portal/SignupOptions';
|
||||
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import useForm, {Dirtyable} from '../../../hooks/useForm';
|
||||
import {PreviewModalContent} from '../../../admin-x-ds/global/modal/PreviewModal';
|
||||
import {Setting, SettingValue} from '../../../types/api';
|
||||
import {Setting, SettingValue, Tier} from '../../../types/api';
|
||||
import {SettingsContext} from '../../providers/SettingsProvider';
|
||||
import {useTiers} from '../../providers/ServiceProvider';
|
||||
|
||||
const Sidebar: React.FC<{
|
||||
localSettings: Setting[]
|
||||
updateSetting: (key: string, setting: SettingValue) => void
|
||||
}> = ({localSettings, updateSetting}) => {
|
||||
localTiers: Tier[]
|
||||
updateTier: (tier: Tier) => void
|
||||
}> = ({localSettings, updateSetting, localTiers, updateTier}) => {
|
||||
const [selectedTab, setSelectedTab] = useState('signupOptions');
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
id: 'signupOptions',
|
||||
title: 'Signup options',
|
||||
contents: <SignupOptions localSettings={localSettings} updateSetting={updateSetting} />
|
||||
contents: <SignupOptions localSettings={localSettings} localTiers={localTiers} updateSetting={updateSetting} updateTier={updateTier} />
|
||||
},
|
||||
{
|
||||
id: 'lookAndFeel',
|
||||
|
@ -48,13 +52,44 @@ const PortalModal: React.FC = () => {
|
|||
const modal = useModal();
|
||||
|
||||
const [selectedPreviewTab, setSelectedPreviewTab] = useState('signup');
|
||||
const {localSettings, updateSetting, handleSave, saveState} = useSettingGroup();
|
||||
const {settings, saveSettings} = useContext(SettingsContext);
|
||||
const {data: tiers, update: updateTiers} = useTiers();
|
||||
|
||||
const {formState, saveState, handleSave, updateForm} = useForm({
|
||||
initialState: {
|
||||
settings: settings as Dirtyable<Setting>[],
|
||||
tiers: tiers as Dirtyable<Tier>[]
|
||||
},
|
||||
|
||||
onSave: async () => {
|
||||
await updateTiers(formState.tiers.filter(tier => tier.dirty));
|
||||
await saveSettings(formState.settings.filter(setting => setting.dirty));
|
||||
}
|
||||
});
|
||||
|
||||
const updateSetting = (key: string, value: SettingValue) => {
|
||||
updateForm(state => ({
|
||||
...state,
|
||||
settings: state.settings.map(setting => (
|
||||
setting.key === key ? {...setting, value, dirty: true} : setting
|
||||
))
|
||||
}));
|
||||
};
|
||||
|
||||
const updateTier = (newTier: Tier) => {
|
||||
updateForm(state => ({
|
||||
...state,
|
||||
tiers: state.tiers.map(tier => (
|
||||
tier.id === newTier.id ? {...newTier, dirty: true} : tier
|
||||
))
|
||||
}));
|
||||
};
|
||||
|
||||
const onSelectURL = (id: string) => {
|
||||
setSelectedPreviewTab(id);
|
||||
};
|
||||
|
||||
const sidebar = <Sidebar localSettings={localSettings} updateSetting={updateSetting} />;
|
||||
const sidebar = <Sidebar localSettings={formState.settings} localTiers={formState.tiers} updateSetting={updateSetting} updateTier={updateTier} />;
|
||||
const preview = <PortalPreview selectedTab={selectedPreviewTab} />;
|
||||
|
||||
let previewTabs: Tab[] = [
|
||||
|
|
|
@ -1,17 +1,39 @@
|
|||
import React from 'react';
|
||||
import Checkbox from '../../../../admin-x-ds/global/form/Checkbox';
|
||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import React, {useContext} from 'react';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import {Setting, SettingValue} from '../../../../types/api';
|
||||
import {getSettingValues} from '../../../../utils/helpers';
|
||||
import {Setting, SettingValue, Tier} from '../../../../types/api';
|
||||
import {SettingsContext} from '../../../providers/SettingsProvider';
|
||||
import {checkStripeEnabled, getSettingValues} from '../../../../utils/helpers';
|
||||
|
||||
const SignupOptions: React.FC<{
|
||||
localSettings: Setting[]
|
||||
updateSetting: (key: string, setting: SettingValue) => void
|
||||
}> = ({localSettings, updateSetting}) => {
|
||||
const [membersSignupAccess, portalName, portalSignupCheckboxRequired] = getSettingValues(localSettings, ['members_signup_access', 'portal_name', 'portal_signup_checkbox_required']);
|
||||
localTiers: Tier[]
|
||||
updateTier: (tier: Tier) => void
|
||||
}> = ({localSettings, updateSetting, localTiers, updateTier}) => {
|
||||
const {config} = useContext(SettingsContext);
|
||||
|
||||
const [membersSignupAccess, portalName, portalSignupCheckboxRequired, portalPlansJson] = getSettingValues(localSettings, ['members_signup_access', 'portal_name', 'portal_signup_checkbox_required', 'portal_plans']);
|
||||
const portalPlans = JSON.parse(portalPlansJson?.toString() || '[]') as string[];
|
||||
|
||||
const togglePlan = (plan: string) => {
|
||||
const index = portalPlans.indexOf(plan);
|
||||
|
||||
if (index === -1) {
|
||||
portalPlans.push(plan);
|
||||
} else {
|
||||
portalPlans.splice(index, 1);
|
||||
}
|
||||
|
||||
updateSetting('portal_plans', JSON.stringify(portalPlans));
|
||||
};
|
||||
|
||||
// This is a bit unclear in current admin, maybe we should add a message if the settings are disabled?
|
||||
const isDisabled = membersSignupAccess !== 'all';
|
||||
|
||||
const isStripeEnabled = checkStripeEnabled(localSettings, config!);
|
||||
|
||||
return <>
|
||||
<Toggle
|
||||
checked={Boolean(portalName)}
|
||||
|
@ -19,7 +41,27 @@ const SignupOptions: React.FC<{
|
|||
label='Display name in signup form'
|
||||
onChange={e => updateSetting('portal_name', e.target.checked)}
|
||||
/>
|
||||
<div>TODO: Tiers available at signup</div>
|
||||
|
||||
<Heading level={6} grey>Tiers available at signup</Heading>
|
||||
<Checkbox checked={portalPlans.includes('free')} disabled={isDisabled} label='Free' value='free' onChange={() => togglePlan('free')} />
|
||||
|
||||
{isStripeEnabled && localTiers.map(tier => (
|
||||
<Checkbox
|
||||
checked={tier.visibility === 'public'}
|
||||
label={tier.name}
|
||||
value={tier.id}
|
||||
onChange={checked => updateTier({...tier, visibility: checked ? 'public' : 'none'})}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isStripeEnabled && localTiers.some(tier => tier.visibility === 'public') && (
|
||||
<>
|
||||
<Heading level={6} grey>Prices available at signup</Heading>
|
||||
<Checkbox checked={portalPlans.includes('monthly')} disabled={isDisabled} label='Monthly' value='monthly' onChange={() => togglePlan('monthly')} />
|
||||
<Checkbox checked={portalPlans.includes('yearly')} disabled={isDisabled} label='Yearly' value='yearly' onChange={() => togglePlan('yearly')} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>TODO: Display notice at signup (Koenig)</div>
|
||||
<Toggle
|
||||
checked={Boolean(portalSignupCheckboxRequired)}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import {useCallback, useEffect, useState} from 'react';
|
||||
|
||||
export type Dirtyable<Data> = Data & {
|
||||
dirty?: boolean;
|
||||
}
|
||||
|
||||
export type SaveState = 'unsaved' | 'saving' | 'saved' | 'error' | '';
|
||||
|
||||
export interface FormHook<State> {
|
||||
|
|
|
@ -5,6 +5,10 @@ export type Setting = {
|
|||
value: SettingValue;
|
||||
}
|
||||
|
||||
export type Config = {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {CustomThemeSetting, InstalledTheme, Label, Offer, Post, Setting, SiteData, Theme, Tier, User, UserRole} from '../types/api';
|
||||
import {Config, CustomThemeSetting, InstalledTheme, Label, Offer, Post, Setting, SiteData, Theme, Tier, User, UserRole} from '../types/api';
|
||||
import {getGhostPaths} from './helpers';
|
||||
|
||||
interface Meta {
|
||||
|
@ -17,6 +17,10 @@ export interface SettingsResponseType {
|
|||
settings: Setting[];
|
||||
}
|
||||
|
||||
export interface ConfigResponseType {
|
||||
config: Config;
|
||||
}
|
||||
|
||||
export interface UsersResponseType {
|
||||
meta?: Meta;
|
||||
users: User[];
|
||||
|
@ -124,6 +128,9 @@ export interface API {
|
|||
browse: () => Promise<SettingsResponseType>;
|
||||
edit: (newSettings: Setting[]) => Promise<SettingsResponseType>;
|
||||
};
|
||||
config: {
|
||||
browse: () => Promise<ConfigResponseType>;
|
||||
};
|
||||
users: {
|
||||
browse: () => Promise<UsersResponseType>;
|
||||
currentUser: () => Promise<User>;
|
||||
|
@ -161,6 +168,7 @@ export interface API {
|
|||
};
|
||||
tiers: {
|
||||
browse: () => Promise<TiersResponseType>
|
||||
edit: (newTier: Tier) => Promise<TiersResponseType>
|
||||
};
|
||||
labels: {
|
||||
browse: () => Promise<LabelsResponseType>
|
||||
|
@ -230,6 +238,13 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
|||
return data;
|
||||
}
|
||||
},
|
||||
config: {
|
||||
browse: async () => {
|
||||
const response = await fetcher(`/config/`, {});
|
||||
const data: ConfigResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
},
|
||||
users: {
|
||||
browse: async () => {
|
||||
const response = await fetcher(`/users/?limit=all&include=roles`, {});
|
||||
|
@ -386,6 +401,14 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
|||
const response = await fetcher(`/tiers/?filter=${filter}&limit=all`);
|
||||
const data: TiersResponseType = await response.json();
|
||||
return data;
|
||||
},
|
||||
edit: async (tier) => {
|
||||
const response = await fetcher(`/tiers/${tier.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({tiers: [tier]})
|
||||
});
|
||||
const data: TiersResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
},
|
||||
labels: {
|
||||
|
|
53
apps/admin-x-settings/src/utils/dataService.ts
Normal file
53
apps/admin-x-settings/src/utils/dataService.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
export interface DataService<Data> {
|
||||
data: Data[];
|
||||
update: (data: Data[]) => Promise<void>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type BulkEditFunction<Data, DataKey extends string> = (newData: Data[]) => Promise<{ [k in DataKey]: Data[] }>
|
||||
|
||||
const useDataService = <Data extends { id: string }, DataKey extends string>({key, browse, edit}: {
|
||||
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>
|
||||
}): DataService<Data> => {
|
||||
const [data, setData] = useState<Data[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
browse().then((response) => {
|
||||
setData(response[key]);
|
||||
});
|
||||
}, [browse, key]);
|
||||
|
||||
const update = async (newData: Data[]) => {
|
||||
const response = await edit(newData);
|
||||
setData(data.map((item) => {
|
||||
const replacement = response[key].find(newItem => newItem.id === item.id);
|
||||
return replacement || item;
|
||||
}));
|
||||
};
|
||||
|
||||
return {data, update};
|
||||
};
|
||||
|
||||
export default useDataService;
|
||||
|
||||
// Utility for APIs which edit one object at a time
|
||||
export const bulkEdit = <Data extends { id: string }, DataKey extends string>(
|
||||
key: DataKey,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
updateOne: (data: Data) => Promise<{ [k in DataKey]: Data[] }>
|
||||
): BulkEditFunction<Data, DataKey> => {
|
||||
return async (newData: Data[]) => {
|
||||
const response = await Promise.all(newData.map(updateOne));
|
||||
|
||||
return {
|
||||
[key]: response.reduce((all, current) => all.concat(current[key]), [] as Data[])
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} as { [k in DataKey]: Data[] };
|
||||
};
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import {Setting, SettingValue, SiteData, User} from '../types/api';
|
||||
import {Config, Setting, SettingValue, SiteData, User} from '../types/api';
|
||||
|
||||
export interface IGhostPaths {
|
||||
adminRoot: string;
|
||||
|
@ -107,3 +107,16 @@ export function getEmailDomain(siteData: SiteData): string {
|
|||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
export function checkStripeEnabled(settings: Setting[], config: Config) {
|
||||
const hasSetting = (key: string) => settings.some(setting => setting.key === key && setting.value);
|
||||
|
||||
const hasDirectKeys = hasSetting('stripe_secret_key') && hasSetting('stripe_publishable_key');
|
||||
const hasConnectKeys = hasSetting('stripe_connect_secret_key') && hasSetting('stripe_connect_publishable_key');
|
||||
|
||||
if (config.stripeDirect) {
|
||||
return hasDirectKeys;
|
||||
}
|
||||
|
||||
return hasConnectKeys || hasDirectKeys;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import {CustomThemeSettingsResponseType, ImagesResponseType, InvitesResponseType, LabelsResponseType, OffersResponseType, PostsResponseType, RolesResponseType, SettingsResponseType, SiteResponseType, ThemesResponseType, TiersResponseType, UsersResponseType} from '../../src/utils/api';
|
||||
import {ConfigResponseType, CustomThemeSettingsResponseType, ImagesResponseType, InvitesResponseType, LabelsResponseType, OffersResponseType, PostsResponseType, RolesResponseType, SettingsResponseType, SiteResponseType, ThemesResponseType, TiersResponseType, UsersResponseType} from '../../src/utils/api';
|
||||
import {Page, Request} from '@playwright/test';
|
||||
import {readFileSync} from 'fs';
|
||||
|
||||
export const responseFixtures = {
|
||||
settings: JSON.parse(readFileSync(`${__dirname}/responses/settings.json`).toString()) as SettingsResponseType,
|
||||
config: JSON.parse(readFileSync(`${__dirname}/responses/config.json`).toString()) as ConfigResponseType,
|
||||
users: JSON.parse(readFileSync(`${__dirname}/responses/users.json`).toString()) as UsersResponseType,
|
||||
me: JSON.parse(readFileSync(`${__dirname}/responses/me.json`).toString()) as UsersResponseType,
|
||||
roles: JSON.parse(readFileSync(`${__dirname}/responses/roles.json`).toString()) as RolesResponseType,
|
||||
|
@ -21,6 +22,9 @@ interface Responses {
|
|||
browse?: SettingsResponseType
|
||||
edit?: SettingsResponseType
|
||||
}
|
||||
config?: {
|
||||
browse?: ConfigResponseType
|
||||
}
|
||||
users?: {
|
||||
browse?: UsersResponseType
|
||||
currentUser?: UsersResponseType
|
||||
|
@ -83,6 +87,9 @@ type LastRequests = {
|
|||
browse: RequestRecord
|
||||
edit: RequestRecord
|
||||
}
|
||||
config: {
|
||||
browse: RequestRecord
|
||||
}
|
||||
users: {
|
||||
browse: RequestRecord
|
||||
currentUser: RequestRecord
|
||||
|
@ -137,6 +144,7 @@ type LastRequests = {
|
|||
export async function mockApi({page,responses}: {page: Page, responses?: Responses}) {
|
||||
const lastApiRequests: LastRequests = {
|
||||
settings: {browse: {}, edit: {}},
|
||||
config: {browse: {}},
|
||||
users: {browse: {}, currentUser: {}, edit: {}, delete: {}, updatePassword: {}, makeOwner: {}},
|
||||
roles: {browse: {}},
|
||||
invites: {browse: {}, add: {}, delete: {}},
|
||||
|
@ -166,6 +174,17 @@ export async function mockApi({page,responses}: {page: Page, responses?: Respons
|
|||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/config\//,
|
||||
respondTo: {
|
||||
GET: {
|
||||
body: responses?.config?.browse ?? responseFixtures.config,
|
||||
updateLastRequest: lastApiRequests.config.browse
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await mockApiResponse({
|
||||
page,
|
||||
path: /\/ghost\/api\/admin\/users\/\?/,
|
||||
|
|
31
apps/admin-x-settings/test/utils/responses/config.json
Normal file
31
apps/admin-x-settings/test/utils/responses/config.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"config": {
|
||||
"version": "5.53.3",
|
||||
"environment": "development",
|
||||
"database": "sqlite3",
|
||||
"mail": "SMTP",
|
||||
"useGravatar": true,
|
||||
"labs": {},
|
||||
"clientExtensions": {},
|
||||
"enableDeveloperExperiments": true,
|
||||
"stripeDirect": false,
|
||||
"mailgunIsConfigured": false,
|
||||
"emailAnalytics": true,
|
||||
"tenor": {
|
||||
"googleApiKey": null,
|
||||
"contentFilter": "off"
|
||||
},
|
||||
"editor": {
|
||||
"url": "http://editor.test/koenig-lexical.umd.js",
|
||||
"version": ""
|
||||
},
|
||||
"adminX": {
|
||||
"url": "http://admin-x.test/admin-x-settings.umd.js",
|
||||
"version": "0.0"
|
||||
},
|
||||
"signupForm": {
|
||||
"url": "https://signup-form.test/signup-form.min.js",
|
||||
"version": "0.1"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue