diff --git a/apps/admin-x-settings/src/api/config.ts b/apps/admin-x-settings/src/api/config.ts index bd733aa9ee..722bdd52c2 100644 --- a/apps/admin-x-settings/src/api/config.ts +++ b/apps/admin-x-settings/src/api/config.ts @@ -48,6 +48,8 @@ export interface ConfigResponseType { const dataType = 'ConfigResponseType'; +export const configDataType = dataType; + export const useBrowseConfig = createQuery({ dataType, path: '/config/' diff --git a/apps/admin-x-settings/src/api/db.ts b/apps/admin-x-settings/src/api/db.ts new file mode 100644 index 0000000000..c9231eb4a9 --- /dev/null +++ b/apps/admin-x-settings/src/api/db.ts @@ -0,0 +1,19 @@ +import {createMutation} from '../utils/apiRequests'; +import {downloadFromEndpoint} from '../utils/helpers'; + +export const useImportContent = createMutation({ + method: 'POST', + path: () => '/db/', + body: (file) => { + const formData = new FormData(); + formData.append('importfile', file); + return formData; + } +}); + +export const useDeleteAllContent = createMutation({ + method: 'DELETE', + path: () => '/db/' +}); + +export const downloadAllContent = () => downloadFromEndpoint('/db/'); diff --git a/apps/admin-x-settings/src/api/redirects.ts b/apps/admin-x-settings/src/api/redirects.ts new file mode 100644 index 0000000000..cf1cea2f61 --- /dev/null +++ b/apps/admin-x-settings/src/api/redirects.ts @@ -0,0 +1,14 @@ +import {createMutation} from '../utils/apiRequests'; +import {downloadFromEndpoint} from '../utils/helpers'; + +export const useUploadRedirects = createMutation({ + method: 'POST', + path: () => '/redirects/upload/', + body: (file) => { + const formData = new FormData(); + formData.append('redirects', file); + return formData; + } +}); + +export const downloadRedirects = () => downloadFromEndpoint('/redirects/download/'); diff --git a/apps/admin-x-settings/src/api/routes.ts b/apps/admin-x-settings/src/api/routes.ts new file mode 100644 index 0000000000..c453d1bbea --- /dev/null +++ b/apps/admin-x-settings/src/api/routes.ts @@ -0,0 +1,14 @@ +import {createMutation} from '../utils/apiRequests'; +import {downloadFromEndpoint} from '../utils/helpers'; + +export const useUploadRoutes = createMutation({ + 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/'); diff --git a/apps/admin-x-settings/src/api/settings.ts b/apps/admin-x-settings/src/api/settings.ts index bcb736b824..f27930e50b 100644 --- a/apps/admin-x-settings/src/api/settings.ts +++ b/apps/admin-x-settings/src/api/settings.ts @@ -63,12 +63,12 @@ export function getSettingValues(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(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) { diff --git a/apps/admin-x-settings/src/components/Sidebar.tsx b/apps/admin-x-settings/src/components/Sidebar.tsx index b262a906f6..bd7f545201 100644 --- a/apps/admin-x-settings/src/components/Sidebar.tsx +++ b/apps/admin-x-settings/src/components/Sidebar.tsx @@ -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 (
@@ -48,7 +51,7 @@ const Sidebar: React.FC = () => { - + {hasTipsAndDonations && } diff --git a/apps/admin-x-settings/src/components/settings/advanced/Labs.tsx b/apps/admin-x-settings/src/components/settings/advanced/Labs.tsx index e655265459..b4b90fe855 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/Labs.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/Labs.tsx @@ -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 ( - - ); -}; - -const MigrationOptions: React.FC = () => { - return ( - - } - detail='Import posts from a JSON or zip file' - title='Import content' - /> - } - detail='Download all of your posts and settings in a single, glorious JSON file' - title='Export your content' - /> - } - detail='Permanently delete all posts and tags from the database, a hard reset' - title='Delete all content' - /> - - ); -}; - -/* - -*/ - -const AlphaFeatures: React.FC = () => { - return ( - - } - detail={<>Try out Ghost{`'`}s brand new editor, and get early access to the latest features and improvements} - title='Ghost editor (beta)' - /> - } - detail={<>A step-by-step tool to easily import all your content, members and paid subscriptions} - title='Substack migrator' - /> - } - detail={<>Translate your membership flows into your publication language (supported languages). Don’t see yours? Get involved} - title='Portal translation' - /> - -
- } - detail={<>Configure redirects for old or moved content,
more info in the docs} - title='Redirects' - /> - -
- } - detail='Configure dynamic routing by modifying the routes.yaml file' - title='Routes' - /> - - ); -}; - -const BetaFeautres: React.FC = () => { - return ( - - } - detail='This is dynamic' - title='Example beta feature' - /> - - ); -}; +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('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: }, { + id: 'labs-beta-features', + title: 'Beta features', + contents: + }, + config.enableDeveloperExperiments && ({ id: 'labs-alpha-features', title: 'Alpha features', contents: - }, - { - id: 'labs-beta-features', - title: 'Beta features', - contents: - } - ] as const; + }) + ].filter(Boolean) as Tab[]; return ( /ghost/#/websockets.', + 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 ( + + {features.map(feature => ( + } + detail={feature.description} + title={feature.title} /> + ))} + + ); +}; + +export default AlphaFeatures; diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx new file mode 100644 index 0000000000..6fd2e4c374 --- /dev/null +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx @@ -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 ( + + } + detail={<>Try out Ghost{`'`}s brand new editor, and get early access to the latest features and improvements} + title='Ghost editor (beta)' /> + updateRoute({isExternal: true, route: 'migrate'})} />} + detail={<>A step-by-step tool to easily import all your content, members and paid subscriptions} + title='Substack migrator' /> + } + detail={<>Translate your membership flows into your publication language (supported languages). Don’t see yours? Get involved} + title='Portal translation' /> + + { + setRedirectsUploading(true); + await uploadRedirects(file); + showToast({ + type: 'success', + message: 'Redirects uploaded successfully' + }); + setRedirectsUploading(false); + }} + > +