Update migration in settings (#19278)

refs.
7b40393d77

We're improving the usability and possibilities for publishers to
migrate from other platforms such as Substack, Medium or Mailchimp. This
PR applies changes to Ghost Settings to support the new flows, more
specifically:

- moves import and export functions out of Labs to its own setting,
directly available from search and the menu
- adds direct access to various platform migrations
- moves "Delete all content" to a dedicated setting group at the bottom
of all setting

---------

Co-authored-by: Jono Mingard <reason.koan@gmail.com>
This commit is contained in:
Peter Zimon 2023-12-13 16:25:29 +01:00 committed by GitHub
parent f23767fe12
commit 58d9b8e382
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 440 additions and 118 deletions

View File

@ -0,0 +1 @@
<svg viewBox="-0.75 -0.75 24 24" xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="m11.2509375 3.515625 0 11.25" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m7.0321875 10.546875 4.21875 4.21875 4.21875 -4.21875" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M21.797812500000003 14.765625v1.40625a2.8125 2.8125 0 0 1 -2.8125 2.8125h-15.46875a2.8125 2.8125 0 0 1 -2.8125 -2.8125v-1.40625" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-0.75 -0.75 24 24" height="24" width="24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M18.09646875 20.3938125c0.674625 0 1.219125 -0.54459375 1.219125 -1.21921875V5.666521875c0 -0.325096875 -0.13003125 -0.6420750000000001 -0.36571875 -0.8696531249999999l-2.43825 -2.34075c-0.227625 -0.227578125 -0.5364375 -0.349490625 -0.85340625 -0.349490625H4.4042625c-0.674596875 0 -1.21914375 0.544546875 -1.21914375 1.21914375V19.17459375c0 0.674625 0.544546875 1.21921875 1.21914375 1.21921875H18.09646875Z" stroke-width="1.5"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m8.476865625 12.861375 2.774446875 2.77453125 2.77453125 -2.77453125" stroke-width="1.5"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m11.2490625 15.63534375 0 -8.770715625" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 943 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-0.75 -0.75 24 24" height="24" width="24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M18.09553125 20.3938125c0.674625 0 1.21921875 -0.54459375 1.21921875 -1.21921875V5.666521875c0 -0.325096875 -0.13012500000000002 -0.6420750000000001 -0.3658125 -0.8696531249999999l-2.43825 -2.34075c-0.227625 -0.227578125 -0.5364375 -0.349490625 -0.85340625 -0.349490625H4.40334375c-0.6745875 0 -1.21914375 0.544546875 -1.21914375 1.21914375V19.17459375c0 0.674625 0.5445562500000001 1.21921875 1.21914375 1.21921875h13.692187500000001Z" stroke-width="1.5"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m8.47595625 9.638625 2.7744187499999997 -2.774559375L14.025 9.638625" stroke-width="1.5"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m11.248125 6.864684375 0 8.770659375000001" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 972 B

View File

@ -30,6 +30,7 @@ import {SettingsResponseType} from '../api/settings';
import {ThemesResponseType} from '../api/themes';
import {TiersResponseType} from '../api/tiers';
import {UsersResponseType} from '../api/users';
import {ExternalLink} from '../routing';
interface MockRequestConfig {
method: string;
@ -257,3 +258,7 @@ export async function testUrlValidation(input: Locator, textToEnter: string, exp
await expect(input.locator('xpath=../..')).toContainText(expectedError);
}
};
export async function expectExternalNavigate(page: Page, link: Partial<ExternalLink>) {
await page.waitForURL(`/external/${encodeURIComponent(JSON.stringify({isExternal: true, ...link}))}`);
};

View File

@ -3,7 +3,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import {TopLevelFrameworkProps} from '../providers/FrameworkProvider';
export default function renderStandaloneApp<Props extends Record<string, never>>(
export default function renderStandaloneApp<Props extends object>(
App: React.ComponentType<Props & {
framework: TopLevelFrameworkProps;
designSystem: DesignSystemAppProps;
@ -38,7 +38,10 @@ export default function renderStandaloneApp<Props extends Record<string, never>>
<App
designSystem={{darkMode: false, fetchKoenigLexical: async () => {}}}
framework={{
externalNavigate: () => {},
externalNavigate: (link) => {
// Use the expectExternalNavigate helper to test this dummy external linking
window.location.href = `/external/${encodeURIComponent(JSON.stringify(link))}`;
},
ghostVersion: '5.x',
sentryDSN: null,
unsplashConfig: {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,10 @@
<svg width="260" height="150" viewBox="0 0 260 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_480_8)">
<path d="M146.656 75.0013C146.656 116.422 113.825 150 73.3291 150C32.8329 150 0 116.414 0 75.0013C0 33.5881 32.8304 0 73.3291 0C113.828 0 146.656 33.5805 146.656 75.0013ZM227.097 75.0013C227.097 113.99 210.682 145.609 190.433 145.609C170.184 145.609 153.768 113.99 153.768 75.0013C153.768 36.0125 170.181 4.39338 190.43 4.39338C210.68 4.39338 227.095 36.0024 227.095 75.0013M260 75.0013C260 109.926 254.228 138.255 247.105 138.255C239.982 138.255 234.213 109.933 234.213 75.0013C234.213 40.0693 239.985 11.7477 247.105 11.7477C254.225 11.7477 260 40.0668 260 75.0013Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_480_8">
<rect width="260" height="150" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -0,0 +1,5 @@
<svg width="168" height="200" viewBox="0 0 168 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M168 44.4445H0V66.6667H168V44.4445Z" fill="#FF6719"/>
<path d="M0 88.8889V200L83.9927 150.378L168 200V88.8889H0Z" fill="#FF6719"/>
<path d="M168 0H0V22.2222H168V0Z" fill="#FF6719"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View File

@ -159,6 +159,7 @@ const Sidebar: React.FC = () => {
<SettingNavSection isVisible={checkVisible(Object.values(advancedSearchKeywords).flat())} title="Advanced">
<NavItem icon='modules-3' keywords={advancedSearchKeywords.integrations} navid='integrations' title="Integrations" onClick={handleSectionClick} />
<NavItem icon='download' keywords={advancedSearchKeywords.migrationtools} navid='migration' title="Import/Export" onClick={handleSectionClick} />
<NavItem icon='brackets' keywords={advancedSearchKeywords.codeInjection} navid='code-injection' title="Code injection" onClick={handleSectionClick} />
<NavItem icon='labs-flask' keywords={advancedSearchKeywords.labs} navid='labs' title="Labs" onClick={handleSectionClick} />
<NavItem icon='time-back' keywords={advancedSearchKeywords.history} navid='history' title="History" onClick={handleSectionClick} />

View File

@ -1,24 +1,30 @@
import CodeInjection from './CodeInjection';
import DangerZone from './DangerZone';
import History from './History';
import Integrations from './Integrations';
import Labs from './Labs';
import MigrationTools from './MigrationTools';
import React from 'react';
import SearchableSection from '../../SearchableSection';
export const searchKeywords = {
integrations: ['advanced', 'integrations', 'zapier', 'slack', 'amp', 'unsplash', 'first promoter', 'firstpromoter', 'pintura', 'disqus', 'analytics', 'ulysses', 'typeform', 'buffer', 'plausible', 'github'],
migrationtools: ['import', 'export', 'migrate', 'substack', 'substack', 'migration', 'medium'],
codeInjection: ['advanced', 'code injection', 'head', 'footer'],
labs: ['advanced', 'labs', 'alpha', 'beta', 'flag', 'import', 'export', 'migrate', 'routes', 'redirect', 'translation', 'delete', 'content', 'editor', 'substack', 'migration', 'portal'],
history: ['advanced', 'history', 'log', 'events', 'user events', 'staff']
labs: ['advanced', 'labs', 'alpha', 'beta', 'flag', 'routes', 'redirect', 'translation', 'editor', 'portal'],
history: ['advanced', 'history', 'log', 'events', 'user events', 'staff'],
dangerzone: ['danger', 'danger zone', 'delete', 'content', 'delete all content', 'delete site']
};
const AdvancedSettings: React.FC = () => {
return (
<SearchableSection keywords={Object.values(searchKeywords).flat()} title='Advanced'>
<Integrations keywords={searchKeywords.integrations} />
<MigrationTools keywords={searchKeywords.migrationtools} />
<CodeInjection keywords={searchKeywords.codeInjection} />
<Labs keywords={searchKeywords.labs} />
<History keywords={searchKeywords.history} />
<DangerZone keywords={searchKeywords.dangerzone} />
</SearchableSection>
);
};

View File

@ -0,0 +1,52 @@
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import {Button, ConfirmationModal, SettingGroupHeader, showToast, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {useDeleteAllContent} from '@tryghost/admin-x-framework/api/db';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useQueryClient} from '@tryghost/admin-x-framework';
const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {mutateAsync: deleteAllContent} = useDeleteAllContent();
const client = useQueryClient();
const handleError = useHandleError();
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 (modal) => {
try {
await deleteAllContent(null);
showToast({
type: 'success',
message: 'All content deleted from database.'
});
modal?.remove();
await client.refetchQueries();
} catch (e) {
handleError(e);
}
}
});
};
return (
<TopLevelGroup
customHeader={
<SettingGroupHeader description='Permanently delete all posts and tags from the database, a hard reset' title='Danger zone' />
}
keywords={keywords}
navid='dangerzone'
testId='dangerzone'
>
<div>
<Button color='red' label='Delete all content' onClick={handleDeleteAllContent} />
</div>
</TopLevelGroup>
);
};
export default withErrorBoundary(DangerZone, 'Danger zone');

View File

@ -1,25 +1,19 @@
import AlphaFeatures from './labs/AlphaFeatures';
import BetaFeatures from './labs/BetaFeatures';
import LabsBubbles from '../../../assets/images/labs-bg.svg';
import MigrationOptions from './labs/MigrationOptions';
import React, {useState} from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import {Button, SettingGroupHeader, Tab, TabView, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {useGlobalData} from '../../providers/GlobalDataProvider';
type LabsTab = 'labs-migration-options' | 'labs-alpha-features' | 'labs-beta-features';
type LabsTab = 'labs-alpha-features' | 'labs-beta-features';
const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
const [selectedTab, setSelectedTab] = useState<LabsTab>('labs-migration-options');
const [selectedTab, setSelectedTab] = useState<LabsTab>('labs-beta-features');
const [isOpen, setIsOpen] = useState(false);
const {config} = useGlobalData();
const tabs = [
{
id: 'labs-migration-options',
title: 'Migration options',
contents: <MigrationOptions />
},
{
id: 'labs-beta-features',
title: 'Beta features',
@ -54,7 +48,7 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
testId='labs'
>
{isOpen ?
<TabView<'labs-migration-options' | 'labs-alpha-features' | 'labs-beta-features'> selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
<TabView<'labs-alpha-features' | 'labs-beta-features'> selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
:
<div className='absolute inset-0 z-0 overflow-hidden opacity-70'>
<img className='absolute -right-6 -top-6 dark:opacity-10' src={LabsBubbles} />

View File

@ -0,0 +1,39 @@
import MigrationToolsExport from './migrationtools/MigrationToolsExport';
import MigrationToolsImport from './migrationtools/MigrationToolsImport';
import React, {useState} from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import {SettingGroupHeader, Tab, TabView, withErrorBoundary} from '@tryghost/admin-x-design-system';
type MigrationTab = 'import' | 'export';
const MigrationTools: React.FC<{ keywords: string[] }> = ({keywords}) => {
const [selectedTab, setSelectedTab] = useState<MigrationTab>('import');
const tabs = [
{
id: 'import',
title: 'Import',
contents: <MigrationToolsImport />
},
{
id: 'export',
title: 'Export',
contents: <MigrationToolsExport />
}
].filter(Boolean) as Tab<MigrationTab>[];
return (
<TopLevelGroup
customHeader={
<SettingGroupHeader description='Import content, members and subscriptions from other platforms or export your Ghost data.' title='Migration tools' />
}
keywords={keywords}
navid='migration'
testId='migrationtools'
>
<TabView<'import' | 'export'> selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
</TopLevelGroup>
);
};
export default withErrorBoundary(MigrationTools, 'Migration tools');

View File

@ -5,10 +5,8 @@ import {Button, FileUpload, List, showToast} from '@tryghost/admin-x-design-syst
import {downloadRedirects, useUploadRedirects} from '@tryghost/admin-x-framework/api/redirects';
import {downloadRoutes, useUploadRoutes} from '@tryghost/admin-x-framework/api/routes';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const BetaFeatures: React.FC = () => {
const {updateRoute} = useRouting();
const {mutateAsync: uploadRedirects} = useUploadRedirects();
const {mutateAsync: uploadRoutes} = useUploadRoutes();
const handleError = useHandleError();
@ -17,10 +15,6 @@ const BetaFeatures: React.FC = () => {
return (
<List titleSeparator={false}>
<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>). Dont 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></>}

View File

@ -0,0 +1,14 @@
import React from 'react';
import {Button} from '@tryghost/admin-x-design-system';
import {downloadAllContent} from '@tryghost/admin-x-framework/api/db';
const MigrationToolsExport: React.FC = () => {
return (
<div className='flex flex-col items-center gap-3 pb-5 pt-10'>
<div>Download all of your <strong>posts and settings</strong> in a single, glorious JSON file.</div>
<Button className='!h-9 !font-semibold' color='grey' icon='export' iconColorClass='!h-5 !w-auto' label='Export content' onClick={() => downloadAllContent()} />
</div>
);
};
export default MigrationToolsExport;

View File

@ -0,0 +1,76 @@
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import UniversalImportModal from './UniversalImportModal';
import clsx from 'clsx';
import {Icon} from '@tryghost/admin-x-design-system';
import {ReactComponent as MailchimpIcon} from '../../../../assets/icons/mailchimp.svg';
import {ReactComponent as MediumIcon} from '../../../../assets/icons/medium.svg';
import {ReactComponent as SubstackIcon} from '../../../../assets/icons/substack.svg';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const ImportButton: React.FC<{
icon?: React.ReactNode,
title?: string,
onClick?: () => void
}> = ({
icon,
title,
onClick
}) => {
const classNames = clsx(
'flex h-9 cursor-pointer items-center justify-center gap-2 rounded-md bg-grey-100 px-2 text-sm font-semibold transition-all hover:bg-grey-200 dark:bg-grey-900'
);
if (onClick) {
return (
<button className={classNames} type='button' onClick={onClick}>
{icon}
{title}
</button>
);
} else {
return <></>;
}
};
const MigrationToolsImport: React.FC = () => {
const {updateRoute} = useRouting();
const handleImportContent = () => {
NiceModal.show(UniversalImportModal);
};
return (
<div className='grid grid-cols-1 gap-4 pt-4 md:grid-cols-2 lg:grid-cols-3'>
<ImportButton
icon={
<SubstackIcon className='h-[18px] w-auto' />
}
title='Substack'
onClick={() => updateRoute({isExternal: true, route: '/migrate/substack'})}
/>
<ImportButton
icon={
<MediumIcon className='h-[18px] w-auto dark:invert' />
}
title='Medium'
onClick={() => updateRoute({isExternal: true, route: '/migrate/medium'})}
/>
<ImportButton
icon={
<MailchimpIcon className='h-5 w-auto' />
}
title='Mailchimp'
onClick={() => updateRoute({isExternal: true, route: '/migrate/mailchimp'})}
/>
<ImportButton
icon={
<Icon className='h-4 w-auto' name='import' />
}
title='Universal import'
onClick={handleImportContent}
/>
</div>
);
};
export default MigrationToolsImport;

View File

@ -0,0 +1,55 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import {ConfirmationModal, FileUpload, Modal} from '@tryghost/admin-x-design-system';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useImportContent} from '@tryghost/admin-x-framework/api/db';
const UniversalImportModal: React.FC = () => {
const modal = useModal();
const {mutateAsync: importContent} = useImportContent();
const [uploading, setUploading] = useState(false);
const handleError = useHandleError();
return (
<Modal
backDropClick={false}
okLabel=''
size='sm'
testId='universal-import-modal'
title='Universal import'
>
<div className='py-4 leading-9'>
<FileUpload
id="import-file"
onUpload={async (file) => {
setUploading(true);
try {
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
});
} catch (e) {
handleError(e);
} finally {
setUploading(false);
}
}}
>
<div className="-mb-4 cursor-pointer bg-grey-75 p-10 text-center dark:bg-grey-950">
{uploading ? 'Uploading...' : <>
Select any JSON or zip file that contains <br />posts and settings
</>}
</div>
</FileUpload>
</div>
</Modal>
);
};
export default NiceModal.create(UniversalImportModal);

View File

@ -1,63 +1,46 @@
import './styles/demo.css';
import './styles/index.css';
import App from './App.tsx';
import React from 'react';
import ReactDOM from 'react-dom/client';
import {DefaultHeaderTypes} from './unsplash/UnsplashTypes.ts';
import renderStandaloneApp from '@tryghost/admin-x-framework/test/render';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App
designSystem={{darkMode: false, fetchKoenigLexical: async () => {}}}
framework={{
externalNavigate: () => {},
ghostVersion: '5.x',
sentryDSN: null,
unsplashConfig: {} as DefaultHeaderTypes,
onDelete: () => {},
onInvalidate: () => {},
onUpdate: () => {}
}}
officialThemes={[{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png',
variants: [
{
category: 'Magazine',
previewUrl: 'https://source-magazine.ghost.io/',
image: 'assets/img/themes/Source-Magazine.png'
},
{
category: 'Newsletter',
previewUrl: 'https://source-newsletter.ghost.io/',
image: 'assets/img/themes/Source-Newsletter.png'
}
]
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Casper.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Edition',
renderStandaloneApp(App, {
officialThemes: [{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png',
variants: [
{
category: 'Magazine',
previewUrl: 'https://source-magazine.ghost.io/',
image: 'assets/img/themes/Source-Magazine.png'
},
{
category: 'Newsletter',
url: 'https://github.com/TryGhost/Edition',
previewUrl: 'https://edition.ghost.io/',
ref: 'TryGhost/Edition',
image: 'assets/img/themes/Edition.png'
}]}
zapierTemplates={[]}
/>
</React.StrictMode>
);
previewUrl: 'https://source-newsletter.ghost.io/',
image: 'assets/img/themes/Source-Newsletter.png'
}
]
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Casper.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Edition',
category: 'Newsletter',
url: 'https://github.com/TryGhost/Edition',
previewUrl: 'https://edition.ghost.io/',
ref: 'TryGhost/Edition',
image: 'assets/img/themes/Edition.png'
}],
zapierTemplates: []
});

View File

@ -1,18 +0,0 @@
:root {
font-size: 62.5%;
line-height: 1.5;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
letter-spacing: unset;
}

View File

@ -0,0 +1,24 @@
import {expect, test} from '@playwright/test';
import {globalDataRequests} from '../../utils/acceptance';
import {mockApi} from '@tryghost/admin-x-framework/test/acceptance';
test.describe('DangerZone', async () => {
test('Delete all content', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
deleteAllContent: {method: 'DELETE', path: '/db/', response: {}}
}});
await page.goto('/');
const dangeZoneSection = page.getByTestId('dangerzone');
await dangeZoneSection.getByRole('button', {name: 'Delete all content'}).click();
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Delete'}).click();
await expect(page.getByTestId('toast-success')).toContainText('All content deleted');
expect(lastApiRequests.deleteAllContent).toBeTruthy();
});
});

View File

@ -3,26 +3,6 @@ import {globalDataRequests} from '../../utils/acceptance';
import {mockApi} from '@tryghost/admin-x-framework/test/acceptance';
test.describe('Labs', async () => {
test('Delete all content', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
deleteAllContent: {method: 'DELETE', path: '/db/', response: {}}
}});
await page.goto('/');
const labsSection = page.getByTestId('labs');
await labsSection.getByRole('button', {name: 'Open'}).click();
await labsSection.getByRole('button', {name: 'Delete'}).click();
await page.getByTestId('confirmation-modal').getByRole('button', {name: 'Delete'}).click();
await expect(page.getByTestId('toast-success')).toContainText('All content deleted');
expect(lastApiRequests.deleteAllContent).toBeTruthy();
});
test('Uploading/downloading redirects', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,

View File

@ -0,0 +1,80 @@
import {expect, test} from '@playwright/test';
import {expectExternalNavigate, mockApi} from '@tryghost/admin-x-framework/test/acceptance';
import {globalDataRequests} from '../../utils/acceptance';
test.describe('Migration tools', async () => {
test('Built-in migrators', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests
}});
await page.goto('/');
const migrationSection = page.getByTestId('migrationtools');
await migrationSection.getByRole('button', {name: 'Substack'}).click();
await expectExternalNavigate(page, {route: '/migrate/substack'});
await page.goto('/');
await migrationSection.getByRole('button', {name: 'Medium'}).click();
await expectExternalNavigate(page, {route: '/migrate/medium'});
await page.goto('/');
await migrationSection.getByRole('button', {name: 'Mailchimp'}).click();
await expectExternalNavigate(page, {route: '/migrate/mailchimp'});
});
test('Universal import', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
importContent: {path: '/db/', method: 'POST', response: {}}
}});
await page.goto('/');
const migrationSection = page.getByTestId('migrationtools');
await migrationSection.getByRole('button', {name: 'Universal import'}).click();
const universalImportModal = page.getByTestId('universal-import-modal');
const fileChooserPromise = page.waitForEvent('filechooser');
universalImportModal.getByText(/JSON or zip file/).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(`${__dirname}/../../utils/files/upload.zip`);
const confirmationModal = page.getByTestId('confirmation-modal');
await expect(confirmationModal).toContainText('Import in progress');
await confirmationModal.getByRole('button', {name: 'Got it'}).click();
await expect(universalImportModal).not.toBeVisible();
await expect(confirmationModal).not.toBeVisible();
expect(lastApiRequests.importContent).toBeTruthy();
});
test('Content export', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
downloadAllContent: {path: '/db/', method: 'GET', response: {}}
}});
await page.goto('/');
const migrationSection = page.getByTestId('migrationtools');
await migrationSection.getByRole('tab', {name: 'Export'}).click();
await migrationSection.getByRole('button', {name: 'Export content'}).click();
await expect(page.locator('iframe#iframeDownload')).toHaveAttribute('src', /\/db\/$/);
expect(lastApiRequests.downloadAllContent).toBeTruthy();
});
});

Binary file not shown.

View File

@ -12,6 +12,6 @@ export default class MigrateController extends Controller {
@action
closeMigrate() {
this.router.transitionTo('/settings/labs');
this.router.transitionTo('/settings/migration');
}
}

View File

@ -71,7 +71,9 @@ Router.map(function () {
});
});
this.route('migrate');
this.route('migrate', function () {
this.route('migrate', {path: '/*platform'});
});
this.route('members', function () {
this.route('import');

View File

@ -15,6 +15,7 @@ export default class MigrateService extends Service {
@tracked siteData = null;
@tracked previousRoute = null;
@tracked isIframeTransition = false;
@tracked platform = null;
get apiUrl() {
const origin = window.location.origin;
@ -71,6 +72,10 @@ export default class MigrateService extends Service {
getIframeURL() {
let url = this.migrateUrl;
const params = this.router.currentRoute.params;
if (params.platform) {
url = url + '?platform=' + params.platform;
}
return url;
}