mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 05:37:34 +03:00
Wired up AdminX Labs settings (#17844)
refs https://github.com/TryGhost/Product/issues/3745 --- This pull request adds and refactors several components and hooks for the labs and advanced settings in the admin app. It introduces feature flags for alpha and beta features, and allows the user to upload and download redirects, routes and content files.
This commit is contained in:
parent
7c934ebf46
commit
05e6588832
@ -48,6 +48,8 @@ export interface ConfigResponseType {
|
||||
|
||||
const dataType = 'ConfigResponseType';
|
||||
|
||||
export const configDataType = dataType;
|
||||
|
||||
export const useBrowseConfig = createQuery<ConfigResponseType>({
|
||||
dataType,
|
||||
path: '/config/'
|
||||
|
19
apps/admin-x-settings/src/api/db.ts
Normal file
19
apps/admin-x-settings/src/api/db.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {downloadFromEndpoint} from '../utils/helpers';
|
||||
|
||||
export const useImportContent = createMutation<unknown, File>({
|
||||
method: 'POST',
|
||||
path: () => '/db/',
|
||||
body: (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('importfile', file);
|
||||
return formData;
|
||||
}
|
||||
});
|
||||
|
||||
export const useDeleteAllContent = createMutation<unknown, null>({
|
||||
method: 'DELETE',
|
||||
path: () => '/db/'
|
||||
});
|
||||
|
||||
export const downloadAllContent = () => downloadFromEndpoint('/db/');
|
14
apps/admin-x-settings/src/api/redirects.ts
Normal file
14
apps/admin-x-settings/src/api/redirects.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {downloadFromEndpoint} from '../utils/helpers';
|
||||
|
||||
export const useUploadRedirects = createMutation<unknown, File>({
|
||||
method: 'POST',
|
||||
path: () => '/redirects/upload/',
|
||||
body: (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('redirects', file);
|
||||
return formData;
|
||||
}
|
||||
});
|
||||
|
||||
export const downloadRedirects = () => downloadFromEndpoint('/redirects/download/');
|
14
apps/admin-x-settings/src/api/routes.ts
Normal file
14
apps/admin-x-settings/src/api/routes.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {downloadFromEndpoint} from '../utils/helpers';
|
||||
|
||||
export const useUploadRoutes = createMutation<unknown, File>({
|
||||
method: 'POST',
|
||||
path: () => '/settings/routes/yaml/',
|
||||
body: (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('routes', file);
|
||||
return formData;
|
||||
}
|
||||
});
|
||||
|
||||
export const downloadRoutes = () => downloadFromEndpoint('/settings/routes/yaml/');
|
@ -63,12 +63,12 @@ export function getSettingValues<ValueType = SettingValue>(settings: Setting[] |
|
||||
return keys.map(key => settings?.find(setting => setting.key === key)?.value) as ValueType[];
|
||||
}
|
||||
|
||||
export function getSettingValue(settings: Setting[] | null | undefined, key: string): SettingValue {
|
||||
export function getSettingValue<ValueType = SettingValue>(settings: Setting[] | null | undefined, key: string): ValueType | null {
|
||||
if (!settings) {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
const setting = settings.find(d => d.key === key);
|
||||
return setting?.value || null;
|
||||
return setting?.value as ValueType || null;
|
||||
}
|
||||
|
||||
export function checkStripeEnabled(settings: Setting[], config: Config) {
|
||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||
import SettingNavItem from '../admin-x-ds/settings/SettingNavItem';
|
||||
import SettingNavSection from '../admin-x-ds/settings/SettingNavSection';
|
||||
import TextField from '../admin-x-ds/global/form/TextField';
|
||||
import useFeatureFlag from '../hooks/useFeatureFlag';
|
||||
import useRouting from '../hooks/useRouting';
|
||||
import {getSettingValues} from '../api/settings';
|
||||
import {useGlobalData} from './providers/GlobalDataProvider';
|
||||
@ -19,6 +20,8 @@ const Sidebar: React.FC = () => {
|
||||
updateRoute(e.currentTarget.name);
|
||||
};
|
||||
|
||||
const hasTipsAndDonations = useFeatureFlag('tipsAndDonations');
|
||||
|
||||
return (
|
||||
<div className="hidden md:!visible md:!block md:h-[calc(100vh-5vmin-84px)] md:w-[240px] md:overflow-y-scroll md:pt-[32px]">
|
||||
<div className='relative mb-10'>
|
||||
@ -48,7 +51,7 @@ const Sidebar: React.FC = () => {
|
||||
<SettingNavItem navid='portal' title="Portal" onClick={handleSectionClick} />
|
||||
<SettingNavItem navid='access' title="Access" onClick={handleSectionClick} />
|
||||
<SettingNavItem navid='tiers' title="Tiers" onClick={handleSectionClick} />
|
||||
<SettingNavItem navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />
|
||||
{hasTipsAndDonations && <SettingNavItem navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />}
|
||||
<SettingNavItem navid='analytics' title="Analytics" onClick={handleSectionClick} />
|
||||
</SettingNavSection>
|
||||
|
||||
|
@ -1,116 +1,20 @@
|
||||
import AlphaFeatures from './labs/AlphaFeatures';
|
||||
import BetaFeatures from './labs/BetaFeatures';
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import LabsBubbles from '../../../assets/images/labs-bg.svg';
|
||||
import List from '../../../admin-x-ds/global/List';
|
||||
import ListItem from '../../../admin-x-ds/global/ListItem';
|
||||
import MigrationOptions from './labs/MigrationOptions';
|
||||
import React, {useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupHeader from '../../../admin-x-ds/settings/SettingGroupHeader';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||
|
||||
const LabItem: React.FC<{
|
||||
title?: React.ReactNode;
|
||||
detail?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
}> = ({
|
||||
title,
|
||||
detail,
|
||||
action
|
||||
}) => {
|
||||
return (
|
||||
<ListItem
|
||||
action={action}
|
||||
bgOnHover={false}
|
||||
detail={detail}
|
||||
paddingRight={false}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MigrationOptions: React.FC = () => {
|
||||
return (
|
||||
<List titleSeparator={false}>
|
||||
<LabItem
|
||||
action={<Button color='grey' label='Open importer' size='sm' />}
|
||||
detail='Import posts from a JSON or zip file'
|
||||
title='Import content'
|
||||
/>
|
||||
<LabItem
|
||||
action={<Button color='grey' label='Export' size='sm' />}
|
||||
detail='Download all of your posts and settings in a single, glorious JSON file'
|
||||
title='Export your content'
|
||||
/>
|
||||
<LabItem
|
||||
action={<Button color='red' label='Delete' size='sm' />}
|
||||
detail='Permanently delete all posts and tags from the database, a hard reset'
|
||||
title='Delete all content'
|
||||
/>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
<a className='text-green' href="" rel="noopener noreferrer" target="_blank"></a>
|
||||
*/
|
||||
|
||||
const AlphaFeatures: React.FC = () => {
|
||||
return (
|
||||
<List titleSeparator={false}>
|
||||
<LabItem
|
||||
action={<Toggle />}
|
||||
detail={<>Try out <a className='text-green' href="https://ghost.org/changelog/editor-beta/" rel="noopener noreferrer" target="_blank">Ghost{`'`}s brand new editor</a>, and get early access to the latest features and improvements</>}
|
||||
title='Ghost editor (beta)'
|
||||
/>
|
||||
<LabItem
|
||||
action={<Button color='grey' label='Open' size='sm' />}
|
||||
detail={<>A <a className='text-green' href="https://ghost.org/help/importing-from-substack/" rel="noopener noreferrer" target="_blank">step-by-step tool</a> to easily import all your content, members and paid subscriptions</>}
|
||||
title='Substack migrator'
|
||||
/>
|
||||
<LabItem
|
||||
action={<Toggle />}
|
||||
detail={<>Translate your membership flows into your publication language (<a className='text-green' href="https://github.com/TryGhost/Ghost/tree/main/ghost/i18n/locales" rel="noopener noreferrer" target="_blank">supported languages</a>). Don’t see yours? <a className='text-green' href="https://forum.ghost.org/t/help-translate-ghost-beta/37461" rel="noopener noreferrer" target="_blank">Get involved</a></>}
|
||||
title='Portal translation'
|
||||
/>
|
||||
<LabItem
|
||||
action={
|
||||
<div className='flex flex-col items-end gap-1'>
|
||||
<Button color='grey' label='Upload redirects file' size='sm' />
|
||||
<Button color='green' label='Download current redirects' link />
|
||||
</div>
|
||||
}
|
||||
detail={<>Configure redirects for old or moved content, <br /> more info in the <a className='text-green' href="https://ghost.org/tutorials/implementing-redirects/" rel="noopener noreferrer" target="_blank">docs</a></>}
|
||||
title='Redirects'
|
||||
/>
|
||||
<LabItem
|
||||
action={
|
||||
<div className='flex flex-col items-end gap-1'>
|
||||
<Button color='grey' label='Upload routes file' size='sm' />
|
||||
<Button color='green' label='Download current routes' link />
|
||||
</div>
|
||||
}
|
||||
detail='Configure dynamic routing by modifying the routes.yaml file'
|
||||
title='Routes'
|
||||
/>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const BetaFeautres: React.FC = () => {
|
||||
return (
|
||||
<List titleSeparator={false}>
|
||||
<LabItem
|
||||
action={<Toggle />}
|
||||
detail='This is dynamic'
|
||||
title='Example beta feature'
|
||||
/>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
type LabsTab = 'labs-migration-options' | 'labs-alpha-features' | 'labs-beta-features';
|
||||
|
||||
const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const [selectedTab, setSelectedTab] = useState<'labs-migration-options' | 'labs-alpha-features' | 'labs-beta-features'>('labs-migration-options');
|
||||
const [selectedTab, setSelectedTab] = useState<LabsTab>('labs-migration-options');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const {config} = useGlobalData();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
@ -119,16 +23,16 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
contents: <MigrationOptions />
|
||||
},
|
||||
{
|
||||
id: 'labs-beta-features',
|
||||
title: 'Beta features',
|
||||
contents: <BetaFeatures />
|
||||
},
|
||||
config.enableDeveloperExperiments && ({
|
||||
id: 'labs-alpha-features',
|
||||
title: 'Alpha features',
|
||||
contents: <AlphaFeatures />
|
||||
},
|
||||
{
|
||||
id: 'labs-beta-features',
|
||||
title: 'Beta features',
|
||||
contents: <BetaFeautres />
|
||||
}
|
||||
] as const;
|
||||
})
|
||||
].filter(Boolean) as Tab<LabsTab>[];
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
|
@ -0,0 +1,73 @@
|
||||
import FeatureToggle from './FeatureToggle';
|
||||
import LabItem from './LabItem';
|
||||
import List from '../../../../admin-x-ds/global/List';
|
||||
import React from 'react';
|
||||
|
||||
const features = [{
|
||||
title: 'URL cache',
|
||||
description: 'Enable URL Caching',
|
||||
flag: 'urlCache'
|
||||
},{
|
||||
title: 'Lexical multiplayer',
|
||||
description: 'Enables multiplayer editing in the lexical editor.',
|
||||
flag: 'lexicalMultiplayer'
|
||||
},{
|
||||
title: 'Webmentions',
|
||||
description: 'Allows viewing received mentions on the dashboard.',
|
||||
flag: 'webmentions'
|
||||
},{
|
||||
title: 'Websockets',
|
||||
description: 'Test out Websockets functionality at <code>/ghost/#/websockets</code>.',
|
||||
flag: 'websockets'
|
||||
},{
|
||||
title: 'Stripe Automatic Tax',
|
||||
description: 'Use Stripe Automatic Tax at Stripe Checkout. Needs to be enabled in Stripe',
|
||||
flag: 'stripeAutomaticTax'
|
||||
},{
|
||||
title: 'Email customization',
|
||||
description: 'Adding more control over the newsletter template',
|
||||
flag: 'emailCustomization'
|
||||
},{
|
||||
title: 'Collections',
|
||||
description: 'Enables Collections 2.0',
|
||||
flag: 'collections'
|
||||
},{
|
||||
title: 'Collections Card',
|
||||
description: 'Enables the Collections Card for pages - requires Collections and the beta Editor to be enabled',
|
||||
flag: 'collectionsCard'
|
||||
},{
|
||||
title: 'Admin X',
|
||||
description: 'Enables Admin X, the new admin UI for Ghost',
|
||||
flag: 'adminXSettings'
|
||||
},{
|
||||
title: 'Mail Events',
|
||||
description: 'Enables processing of mail events',
|
||||
flag: 'mailEvents'
|
||||
},{
|
||||
title: 'Convert to Lexical',
|
||||
description: 'Convert mobiledoc posts to lexical upon opening in the editor.',
|
||||
flag: 'convertToLexical'
|
||||
},{
|
||||
title: 'Import Member Tier',
|
||||
description: 'Enables tier to be specified when importing members',
|
||||
flag: 'importMemberTier'
|
||||
},{
|
||||
title: 'Tips & donations',
|
||||
description: 'Enables publishers to collect one-time payments',
|
||||
flag: 'tipsAndDonations'
|
||||
}];
|
||||
|
||||
const AlphaFeatures: React.FC = () => {
|
||||
return (
|
||||
<List titleSeparator={false}>
|
||||
{features.map(feature => (
|
||||
<LabItem
|
||||
action={<FeatureToggle flag={feature.flag} />}
|
||||
detail={feature.description}
|
||||
title={feature.title} />
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlphaFeatures;
|
@ -0,0 +1,77 @@
|
||||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import FeatureToggle from './FeatureToggle';
|
||||
import FileUpload from '../../../../admin-x-ds/global/form/FileUpload';
|
||||
import LabItem from './LabItem';
|
||||
import List from '../../../../admin-x-ds/global/List';
|
||||
import React, {useState} from 'react';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {downloadRedirects, useUploadRedirects} from '../../../../api/redirects';
|
||||
import {downloadRoutes, useUploadRoutes} from '../../../../api/routes';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
|
||||
const BetaFeatures: React.FC = () => {
|
||||
const {updateRoute} = useRouting();
|
||||
const {mutateAsync: uploadRedirects} = useUploadRedirects();
|
||||
const {mutateAsync: uploadRoutes} = useUploadRoutes();
|
||||
const [redirectsUploading, setRedirectsUploading] = useState(false);
|
||||
const [routesUploading, setRoutesUploading] = useState(false);
|
||||
|
||||
return (
|
||||
<List titleSeparator={false}>
|
||||
<LabItem
|
||||
action={<FeatureToggle flag='lexicalEditor' />}
|
||||
detail={<>Try out <a className='text-green' href="https://ghost.org/changelog/editor-beta/" rel="noopener noreferrer" target="_blank">Ghost{`'`}s brand new editor</a>, and get early access to the latest features and improvements</>}
|
||||
title='Ghost editor (beta)' />
|
||||
<LabItem
|
||||
action={<Button color='grey' label='Open' size='sm' onClick={() => updateRoute({isExternal: true, route: 'migrate'})} />}
|
||||
detail={<>A <a className='text-green' href="https://ghost.org/help/importing-from-substack/" rel="noopener noreferrer" target="_blank">step-by-step tool</a> to easily import all your content, members and paid subscriptions</>}
|
||||
title='Substack migrator' />
|
||||
<LabItem
|
||||
action={<FeatureToggle flag='i18n' />}
|
||||
detail={<>Translate your membership flows into your publication language (<a className='text-green' href="https://github.com/TryGhost/Ghost/tree/main/ghost/i18n/locales" rel="noopener noreferrer" target="_blank">supported languages</a>). Don’t see yours? <a className='text-green' href="https://forum.ghost.org/t/help-translate-ghost-beta/37461" rel="noopener noreferrer" target="_blank">Get involved</a></>}
|
||||
title='Portal translation' />
|
||||
<LabItem
|
||||
action={<div className='flex flex-col items-end gap-1'>
|
||||
<FileUpload
|
||||
id='upload-redirects'
|
||||
onUpload={async (file) => {
|
||||
setRedirectsUploading(true);
|
||||
await uploadRedirects(file);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Redirects uploaded successfully'
|
||||
});
|
||||
setRedirectsUploading(false);
|
||||
}}
|
||||
>
|
||||
<Button color='grey' label={redirectsUploading ? 'Uploading ...' : 'Upload redirects file'} size='sm' tag='div' />
|
||||
</FileUpload>
|
||||
<Button color='green' label='Download current redirects' link onClick={() => downloadRedirects()} />
|
||||
</div>}
|
||||
detail={<>Configure redirects for old or moved content, <br /> more info in the <a className='text-green' href="https://ghost.org/tutorials/implementing-redirects/" rel="noopener noreferrer" target="_blank">docs</a></>}
|
||||
title='Redirects' />
|
||||
<LabItem
|
||||
action={<div className='flex flex-col items-end gap-1'>
|
||||
<FileUpload
|
||||
id='upload-routes'
|
||||
onUpload={async (file) => {
|
||||
setRoutesUploading(true);
|
||||
await uploadRoutes(file);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'Routes uploaded successfully'
|
||||
});
|
||||
setRoutesUploading(false);
|
||||
}}
|
||||
>
|
||||
<Button color='grey' label={routesUploading ? 'Uploading ...' : 'Upload routes file'} size='sm' tag='div' />
|
||||
</FileUpload>
|
||||
<Button color='green' label='Download current routes' link onClick={() => downloadRoutes()} />
|
||||
</div>}
|
||||
detail='Configure dynamic routing by modifying the routes.yaml file'
|
||||
title='Routes' />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default BetaFeatures;
|
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import {ConfigResponseType, configDataType} from '../../../../api/config';
|
||||
import {getSettingValue, useEditSettings} from '../../../../api/settings';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
import {useQueryClient} from '@tanstack/react-query';
|
||||
|
||||
const FeatureToggle: React.FC<{ flag: string; }> = ({flag}) => {
|
||||
const {settings} = useGlobalData();
|
||||
const labs = JSON.parse(getSettingValue<string>(settings, 'labs') || '{}');
|
||||
const {mutateAsync: editSettings} = useEditSettings();
|
||||
const client = useQueryClient();
|
||||
|
||||
return <Toggle checked={labs[flag]} onChange={async () => {
|
||||
const newValue = !labs[flag];
|
||||
await editSettings([{
|
||||
key: 'labs',
|
||||
value: JSON.stringify({...labs, [flag]: newValue})
|
||||
}]);
|
||||
client.setQueriesData([configDataType], current => ({
|
||||
config: {
|
||||
...(current as ConfigResponseType).config,
|
||||
labs: {
|
||||
...(current as ConfigResponseType).config.labs,
|
||||
[flag]: newValue
|
||||
}
|
||||
}
|
||||
}));
|
||||
}} />;
|
||||
};
|
||||
|
||||
export default FeatureToggle;
|
@ -0,0 +1,21 @@
|
||||
import ListItem from '../../../../admin-x-ds/global/ListItem';
|
||||
import React from 'react';
|
||||
|
||||
const LabItem: React.FC<{
|
||||
title?: React.ReactNode;
|
||||
detail?: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
}> = ({
|
||||
title, detail, action
|
||||
}) => {
|
||||
return (
|
||||
<ListItem
|
||||
action={action}
|
||||
bgOnHover={false}
|
||||
detail={detail}
|
||||
paddingRight={false}
|
||||
title={title} />
|
||||
);
|
||||
};
|
||||
|
||||
export default LabItem;
|
@ -0,0 +1,87 @@
|
||||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal';
|
||||
import FileUpload from '../../../../admin-x-ds/global/form/FileUpload';
|
||||
import LabItem from './LabItem';
|
||||
import List from '../../../../admin-x-ds/global/List';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useState} from 'react';
|
||||
import {downloadAllContent, useDeleteAllContent, useImportContent} from '../../../../api/db';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
import {useQueryClient} from '@tanstack/react-query';
|
||||
|
||||
const ImportModalContent = () => {
|
||||
const modal = useModal();
|
||||
const {mutateAsync: importContent} = useImportContent();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
return <FileUpload
|
||||
id="import-file"
|
||||
onUpload={async (file) => {
|
||||
setUploading(true);
|
||||
await importContent(file);
|
||||
modal.remove();
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Import in progress',
|
||||
prompt: `Your import is being processed, and you'll receive a confirmation email as soon as it's complete. Usually this only takes a few minutes, but larger imports may take longer.`,
|
||||
cancelLabel: '',
|
||||
okLabel: 'Got it',
|
||||
onOk: confirmModal => confirmModal?.remove(),
|
||||
formSheet: false
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="cursor-pointer bg-grey-100 p-10 text-center">
|
||||
{uploading ? 'Uploading ...' : 'Select a JSON or zip file'}
|
||||
</div>
|
||||
</FileUpload>;
|
||||
};
|
||||
|
||||
const MigrationOptions: React.FC = () => {
|
||||
const {mutateAsync: deleteAllContent} = useDeleteAllContent();
|
||||
const client = useQueryClient();
|
||||
|
||||
const handleImportContent = () => {
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Import content',
|
||||
prompt: <ImportModalContent />,
|
||||
okLabel: '',
|
||||
formSheet: false
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteAllContent = () => {
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Would you really like to delete all content from your blog?',
|
||||
prompt: 'This is permanent! No backups, no restores, no magic undo button. We warned you, k?',
|
||||
okColor: 'red',
|
||||
okLabel: 'Delete',
|
||||
onOk: async () => {
|
||||
await deleteAllContent(null);
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: 'All content deleted from database.'
|
||||
});
|
||||
await client.refetchQueries();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<List titleSeparator={false}>
|
||||
<LabItem
|
||||
action={<Button color='grey' label='Open importer' size='sm' onClick={handleImportContent} />}
|
||||
detail='Import posts from a JSON or zip file'
|
||||
title='Import content' />
|
||||
<LabItem
|
||||
action={<Button color='grey' label='Export' size='sm' onClick={() => downloadAllContent()} />}
|
||||
detail='Download all of your posts and settings in a single, glorious JSON file'
|
||||
title='Export your content' />
|
||||
<LabItem
|
||||
action={<Button color='red' label='Delete' size='sm' onClick={handleDeleteAllContent} />}
|
||||
detail='Permanently delete all posts and tags from the database, a hard reset'
|
||||
title='Delete all content' />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default MigrationOptions;
|
@ -5,6 +5,7 @@ import React from 'react';
|
||||
import SettingSection from '../../../admin-x-ds/settings/SettingSection';
|
||||
import Tiers from './Tiers';
|
||||
import TipsOrDonations from './TipsOrDonations';
|
||||
import useFeatureFlag from '../../../hooks/useFeatureFlag';
|
||||
|
||||
const searchKeywords = {
|
||||
portal: ['portal', 'signup', 'sign up', 'signin', 'sign in', 'login', 'account', 'membership'],
|
||||
@ -15,12 +16,14 @@ const searchKeywords = {
|
||||
};
|
||||
|
||||
const MembershipSettings: React.FC = () => {
|
||||
const hasTipsAndDonations = useFeatureFlag('tipsAndDonations');
|
||||
|
||||
return (
|
||||
<SettingSection keywords={Object.values(searchKeywords).flat()} title='Membership'>
|
||||
<Portal keywords={searchKeywords.portal} />
|
||||
<Access keywords={searchKeywords.access} />
|
||||
<Tiers keywords={searchKeywords.tiers} />
|
||||
<TipsOrDonations keywords={searchKeywords.tips} />
|
||||
{hasTipsAndDonations && <TipsOrDonations keywords={searchKeywords.tips} />}
|
||||
<Analytics keywords={searchKeywords.analytics} />
|
||||
</SettingSection>
|
||||
);
|
||||
|
9
apps/admin-x-settings/src/hooks/useFeatureFlag.tsx
Normal file
9
apps/admin-x-settings/src/hooks/useFeatureFlag.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import {useGlobalData} from '../components/providers/GlobalDataProvider';
|
||||
|
||||
const useFeatureFlag = (flag: string) => {
|
||||
const {config} = useGlobalData();
|
||||
|
||||
return config.labs[flag] || false;
|
||||
};
|
||||
|
||||
export default useFeatureFlag;
|
@ -63,6 +63,10 @@ export function downloadFile(url: string) {
|
||||
iframe.setAttribute('src', url);
|
||||
}
|
||||
|
||||
export function downloadFromEndpoint(path: string) {
|
||||
downloadFile(`${getGhostPaths().apiRoot}${path}`);
|
||||
}
|
||||
|
||||
export function numberWithCommas(x: number) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ test.describe('Zapier integration settings', async () => {
|
||||
...globalDataRequests.browseConfig,
|
||||
response: {
|
||||
config: {
|
||||
...responseFixtures.config,
|
||||
...responseFixtures.config.config,
|
||||
hostSettings: {
|
||||
limits: {
|
||||
customIntegrations: {
|
||||
|
@ -51,7 +51,7 @@ test.describe('Stripe settings', async () => {
|
||||
...globalDataRequests,
|
||||
browseConfig: {method: 'GET', path: '/config/', response: {
|
||||
config: {
|
||||
...responseFixtures.config,
|
||||
...responseFixtures.config.config,
|
||||
stripeDirect: true
|
||||
}
|
||||
}},
|
||||
|
Loading…
Reference in New Issue
Block a user