diff --git a/apps/admin-x-design-system/src/assets/icons/download.svg b/apps/admin-x-design-system/src/assets/icons/download.svg new file mode 100644 index 0000000000..d975351932 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin-x-design-system/src/assets/icons/export.svg b/apps/admin-x-design-system/src/assets/icons/export.svg new file mode 100644 index 0000000000..12ef11c485 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/export.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin-x-design-system/src/assets/icons/import.svg b/apps/admin-x-design-system/src/assets/icons/import.svg new file mode 100644 index 0000000000..fb62e9fd76 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/import.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin-x-framework/src/test/acceptance.ts b/apps/admin-x-framework/src/test/acceptance.ts index bf15ba2901..ea7f3cc2d4 100644 --- a/apps/admin-x-framework/src/test/acceptance.ts +++ b/apps/admin-x-framework/src/test/acceptance.ts @@ -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) { + await page.waitForURL(`/external/${encodeURIComponent(JSON.stringify({isExternal: true, ...link}))}`); +}; diff --git a/apps/admin-x-framework/src/test/render.tsx b/apps/admin-x-framework/src/test/render.tsx index e949ef6195..4e12470f82 100644 --- a/apps/admin-x-framework/src/test/render.tsx +++ b/apps/admin-x-framework/src/test/render.tsx @@ -3,7 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import {TopLevelFrameworkProps} from '../providers/FrameworkProvider'; -export default function renderStandaloneApp>( +export default function renderStandaloneApp( App: React.ComponentType> {}}} 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: { diff --git a/apps/admin-x-settings/src/assets/icons/mailchimp.svg b/apps/admin-x-settings/src/assets/icons/mailchimp.svg new file mode 100644 index 0000000000..686868d51f --- /dev/null +++ b/apps/admin-x-settings/src/assets/icons/mailchimp.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/admin-x-settings/src/assets/icons/medium.svg b/apps/admin-x-settings/src/assets/icons/medium.svg new file mode 100644 index 0000000000..27c179ca52 --- /dev/null +++ b/apps/admin-x-settings/src/assets/icons/medium.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/admin-x-settings/src/assets/icons/substack.svg b/apps/admin-x-settings/src/assets/icons/substack.svg new file mode 100644 index 0000000000..73a92e1202 --- /dev/null +++ b/apps/admin-x-settings/src/assets/icons/substack.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/admin-x-settings/src/components/Sidebar.tsx b/apps/admin-x-settings/src/components/Sidebar.tsx index 7f4d204f2d..cec7372770 100644 --- a/apps/admin-x-settings/src/components/Sidebar.tsx +++ b/apps/admin-x-settings/src/components/Sidebar.tsx @@ -159,6 +159,7 @@ const Sidebar: React.FC = () => { + diff --git a/apps/admin-x-settings/src/components/settings/advanced/AdvancedSettings.tsx b/apps/admin-x-settings/src/components/settings/advanced/AdvancedSettings.tsx index ba9daa8cb6..8cf975334e 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/AdvancedSettings.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/AdvancedSettings.tsx @@ -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 ( + + ); }; diff --git a/apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx b/apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx new file mode 100644 index 0000000000..24b2cdeeab --- /dev/null +++ b/apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx @@ -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 ( + + } + keywords={keywords} + navid='dangerzone' + testId='dangerzone' + > +
+
+
+ ); +}; + +export default withErrorBoundary(DangerZone, 'Danger zone'); 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 83653b29fe..a55a7b802a 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/Labs.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/Labs.tsx @@ -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('labs-migration-options'); + const [selectedTab, setSelectedTab] = useState('labs-beta-features'); const [isOpen, setIsOpen] = useState(false); const {config} = useGlobalData(); const tabs = [ - { - id: 'labs-migration-options', - title: 'Migration options', - contents: - }, { id: 'labs-beta-features', title: 'Beta features', @@ -54,7 +48,7 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => { testId='labs' > {isOpen ? - selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} /> + selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} /> :
diff --git a/apps/admin-x-settings/src/components/settings/advanced/MigrationTools.tsx b/apps/admin-x-settings/src/components/settings/advanced/MigrationTools.tsx new file mode 100644 index 0000000000..77079050bd --- /dev/null +++ b/apps/admin-x-settings/src/components/settings/advanced/MigrationTools.tsx @@ -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('import'); + + const tabs = [ + { + id: 'import', + title: 'Import', + contents: + }, + { + id: 'export', + title: 'Export', + contents: + } + ].filter(Boolean) as Tab[]; + + return ( + + } + keywords={keywords} + navid='migration' + testId='migrationtools' + > + selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} /> + + ); +}; + +export default withErrorBoundary(MigrationTools, 'Migration tools'); 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 index 17008d14c9..51ef6e5126 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx @@ -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 ( - 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} diff --git a/apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsExport.tsx b/apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsExport.tsx new file mode 100644 index 0000000000..99112c7ce8 --- /dev/null +++ b/apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsExport.tsx @@ -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 ( +
+
Download all of your posts and settings in a single, glorious JSON file.
+
+ ); +}; + +export default MigrationToolsExport; diff --git a/apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsImport.tsx b/apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsImport.tsx new file mode 100644 index 0000000000..d1ff9bbf0f --- /dev/null +++ b/apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsImport.tsx @@ -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 ( + + ); + } else { + return <>; + } +}; + +const MigrationToolsImport: React.FC = () => { + const {updateRoute} = useRouting(); + + const handleImportContent = () => { + NiceModal.show(UniversalImportModal); + }; + + return ( +
+ + } + title='Substack' + onClick={() => updateRoute({isExternal: true, route: '/migrate/substack'})} + /> + + } + title='Medium' + onClick={() => updateRoute({isExternal: true, route: '/migrate/medium'})} + /> + + } + title='Mailchimp' + onClick={() => updateRoute({isExternal: true, route: '/migrate/mailchimp'})} + /> + + } + title='Universal import' + onClick={handleImportContent} + /> +
+ ); +}; + +export default MigrationToolsImport; diff --git a/apps/admin-x-settings/src/components/settings/advanced/migrationtools/UniversalImportModal.tsx b/apps/admin-x-settings/src/components/settings/advanced/migrationtools/UniversalImportModal.tsx new file mode 100644 index 0000000000..737b637888 --- /dev/null +++ b/apps/admin-x-settings/src/components/settings/advanced/migrationtools/UniversalImportModal.tsx @@ -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 ( + +
+ { + 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); + } + }} + > +
+ {uploading ? 'Uploading...' : <> + Select any JSON or zip file that contains
posts and settings + } +
+
+
+
+ ); +}; + +export default NiceModal.create(UniversalImportModal); diff --git a/apps/admin-x-settings/src/main.tsx b/apps/admin-x-settings/src/main.tsx index 44aef09f02..a99e69c955 100644 --- a/apps/admin-x-settings/src/main.tsx +++ b/apps/admin-x-settings/src/main.tsx @@ -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( - - {}}} - 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={[]} - /> - -); + 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: [] +}); diff --git a/apps/admin-x-settings/src/styles/demo.css b/apps/admin-x-settings/src/styles/demo.css deleted file mode 100644 index 8fd3a7ab6d..0000000000 --- a/apps/admin-x-settings/src/styles/demo.css +++ /dev/null @@ -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; -} diff --git a/apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts b/apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts new file mode 100644 index 0000000000..6543926a91 --- /dev/null +++ b/apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts @@ -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(); + }); +}); diff --git a/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts b/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts index daf98413ad..2b6f99d793 100644 --- a/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts +++ b/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts @@ -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, diff --git a/apps/admin-x-settings/test/acceptance/advanced/migrationTools.test.ts b/apps/admin-x-settings/test/acceptance/advanced/migrationTools.test.ts new file mode 100644 index 0000000000..2601214fdd --- /dev/null +++ b/apps/admin-x-settings/test/acceptance/advanced/migrationTools.test.ts @@ -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(); + }); +}); diff --git a/apps/admin-x-settings/test/utils/files/upload.zip b/apps/admin-x-settings/test/utils/files/upload.zip new file mode 100644 index 0000000000..8fde66b696 Binary files /dev/null and b/apps/admin-x-settings/test/utils/files/upload.zip differ diff --git a/ghost/admin/app/controllers/migrate.js b/ghost/admin/app/controllers/migrate.js index f08060bfde..4aa0628652 100644 --- a/ghost/admin/app/controllers/migrate.js +++ b/ghost/admin/app/controllers/migrate.js @@ -12,6 +12,6 @@ export default class MigrateController extends Controller { @action closeMigrate() { - this.router.transitionTo('/settings/labs'); + this.router.transitionTo('/settings/migration'); } } diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index aff3bcd32a..e63f0eb076 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -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'); diff --git a/ghost/admin/app/services/migrate.js b/ghost/admin/app/services/migrate.js index b4a692b260..9fac9c0308 100644 --- a/ghost/admin/app/services/migrate.js +++ b/ghost/admin/app/services/migrate.js @@ -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; }