From 58d9b8e382fe2028ae7ac7173ca489a6e78d8fdc Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 13 Dec 2023 16:25:29 +0100 Subject: [PATCH] Update migration in settings (#19278) refs. https://github.com/TryGhost/Migrate-App/commit/7b40393d7706170ff3925e00a47d38802c0c1dd3 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 --- .../src/assets/icons/download.svg | 1 + .../src/assets/icons/export.svg | 1 + .../src/assets/icons/import.svg | 1 + apps/admin-x-framework/src/test/acceptance.ts | 5 + apps/admin-x-framework/src/test/render.tsx | 7 +- .../src/assets/icons/mailchimp.svg | 9 ++ .../src/assets/icons/medium.svg | 10 ++ .../src/assets/icons/substack.svg | 5 + .../src/components/Sidebar.tsx | 1 + .../settings/advanced/AdvancedSettings.tsx | 10 +- .../settings/advanced/DangerZone.tsx | 52 +++++++++ .../src/components/settings/advanced/Labs.tsx | 12 +-- .../settings/advanced/MigrationTools.tsx | 39 +++++++ .../settings/advanced/labs/BetaFeatures.tsx | 6 -- .../migrationtools/MigrationToolsExport.tsx | 14 +++ .../migrationtools/MigrationToolsImport.tsx | 76 +++++++++++++ .../migrationtools/UniversalImportModal.tsx | 55 ++++++++++ apps/admin-x-settings/src/main.tsx | 101 ++++++++---------- apps/admin-x-settings/src/styles/demo.css | 18 ---- .../acceptance/advanced/dangerzone.test.ts | 24 +++++ .../test/acceptance/advanced/labs.test.ts | 20 ---- .../advanced/migrationTools.test.ts | 80 ++++++++++++++ .../test/utils/files/upload.zip | Bin 0 -> 4195 bytes ghost/admin/app/controllers/migrate.js | 2 +- ghost/admin/app/router.js | 4 +- ghost/admin/app/services/migrate.js | 5 + 26 files changed, 440 insertions(+), 118 deletions(-) create mode 100644 apps/admin-x-design-system/src/assets/icons/download.svg create mode 100644 apps/admin-x-design-system/src/assets/icons/export.svg create mode 100644 apps/admin-x-design-system/src/assets/icons/import.svg create mode 100644 apps/admin-x-settings/src/assets/icons/mailchimp.svg create mode 100644 apps/admin-x-settings/src/assets/icons/medium.svg create mode 100644 apps/admin-x-settings/src/assets/icons/substack.svg create mode 100644 apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx create mode 100644 apps/admin-x-settings/src/components/settings/advanced/MigrationTools.tsx create mode 100644 apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsExport.tsx create mode 100644 apps/admin-x-settings/src/components/settings/advanced/migrationtools/MigrationToolsImport.tsx create mode 100644 apps/admin-x-settings/src/components/settings/advanced/migrationtools/UniversalImportModal.tsx delete mode 100644 apps/admin-x-settings/src/styles/demo.css create mode 100644 apps/admin-x-settings/test/acceptance/advanced/dangerzone.test.ts create mode 100644 apps/admin-x-settings/test/acceptance/advanced/migrationTools.test.ts create mode 100644 apps/admin-x-settings/test/utils/files/upload.zip 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 0000000000000000000000000000000000000000..8fde66b696644c0672528c5159028dedf0894b0f GIT binary patch literal 4195 zcmd5O^S1h zQ=n97X{?}#O`QmmYCx!hBc`34p5dFC+i^vvN-OM!&`+&WRXZaTIvDixBeF$Z@aS+B|;N6M#NYuO6Z~S1&%UTMlOj4 z%0XO<68SkcGO)_5H14_%>GXHIgSF};bLA&&%lYd*`8!A|1pR(GX_`B3WIn6?W=CWt zp7`*l;vXiq=BY{V?!}E|E!^v_criRZl}OBb`cpNU@1?b1Y{@>KEn&{~NLI*(0Gh;K zdHd4eb%^1)F|84HV(~3HBgYq?6Mv7(n-Qd!q8uNzk*P*y1akvDS$>LG)$+d=8Ld&88_R9AFpZ zadn1+0sbY}18IqB;!3i?a9USL&1Xth%$c~j^ZWVzOs6z7{nD&n`L^9*j!@KeCO6)uf3DkWWBtt6hd(i7SwF)d(?-ecl{oSHze|pwo`g{ zpZ&K9d?>`gw{H-`2K;(dtok*fMdX|*ScY)uicEVKDw zLu8ZesY4UVo8qE!WrC5N(TM?jq_y1X*WnXk!+uBUOK#cSuW`KUVYn@?PtJ4FylzJM&M3|FKF=F(uTJNuh~`a} zuK2QSgd&6BWO%!sWD%_5qH|R4E|8$-PG?vzKr+WZ8S^GP)qVKw(feuU!GsQxXlvfu z!(ufeBFXcPoC!DG=vh+C4`};yp_>mtw;%hIKh<RegQc zHn@u$dRf$yuBMF?BZZyhw{LYV5$KE=N_LqM7uSsKx?K3gs`1rv3+x^p{H(P4M7-PW zhPtG2wC?DY3q7~12aI6Iu)2&YRth{D;p>mXpt!mx=B+t)kv1gC{X^ByP;M~9jzN#e zpUU!uzKA$+KgXI_dB4}-+DwdFpWS4eaut+fnpo_J(e-do*E>q<*3X6S2|iz8@}41u zGowCkx%2CusmywniH~0P=dwQdP8_wL{~+L7-u6+dWz2$fFU{%GoBY0;ma>QbvQLdX zB+`@GZs=WmDX1mNZDHz?wu6ahn1e^=pS21?N&T|VbZAeZS}3YU^_W^T95rO4(#iV@ zEhKc#T*)Si)Kz=R<9G-!(-l@GPNI85MX{X`E&i$xNtDCanay!Aya^B$R3eX*)8~SK zBljx&>TfZunCc@E?=urx7@}&npHMHhUhNl?E-nq9Pm-bcq$x(Wz?EnLW65cJx;iZ< zA86=j(mu55#&gb)e;#%e5LOrQD4e`P8PtiLM)Jw;*J*^y962L=xG;QBeM*H=*6f)D zo6A(@G)_5_S2m~}U|Xt;xFOt|VjAQymQ2f;ntDWiir+3Zr##m^u(xqi+GOLo)Zsvh zK{zb*ZV5D$>(+>+TWaZ-sZYepah1uz`le* zSd&yhg$Aw!Djh%bYLx!`nr6sb{!&J#Pgz?>Vb|GV(do*Ul~2KTUYrzSf#3wHE>K(7 zsJcKs-nwf4ShE1HtE2xfJBR^*g?G7wr~nP{Tjv0lR%`pXW+fA*Qo-TJZEZ$aR)Xwo z@kfOWo$Q)fmiWgS*;(dCgKWL+xiT8L2P$NCXo$*3U$?Q3w0pq0*qvFef?7I&UU1*F z*%eup-S3S1zv;^yP+{2G2M~}$P#9S7{$C$Qz9?Nfkg(72;H0)B3^GXLx{oIoxMlU= zL4bXL1rGvCLL)12e6xAwq`Jnj;79H311LBIFA0rIg|ZpH{*jn{BXFK&AN<%^0${+F zVXU0+K!a8pkbYFpK8=CXa>RrH literal 0 HcmV?d00001 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; }