From 3811999be71e52722e2b1ec306ef89d6812e7b46 Mon Sep 17 00:00:00 2001 From: Jono M Date: Mon, 3 Jul 2023 17:59:31 +1200 Subject: [PATCH] Improved AdminX theme installation flow (#17175) refs https://github.com/TryGhost/Team/issues/3349 --- .../components/settings/site/ThemeModal.tsx | 73 +++++++++++++++---- .../site/{ => theme}/ThemeInstalledModal.tsx | 57 +++++++++------ .../settings/site/theme/ThemePreview.tsx | 42 +++++++++-- .../test/e2e/site/theme.test.ts | 30 ++++---- 4 files changed, 144 insertions(+), 58 deletions(-) rename apps/admin-x-settings/src/components/settings/site/{ => theme}/ThemeInstalledModal.tsx (61%) diff --git a/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx b/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx index 38d2273172..999d44c04c 100644 --- a/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx +++ b/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx @@ -8,12 +8,11 @@ import OfficialThemes from './theme/OfficialThemes'; import PageHeader from '../../../admin-x-ds/global/layout/PageHeader'; import React, {useState} from 'react'; import TabView from '../../../admin-x-ds/global/TabView'; -import ThemeInstalledModal from './ThemeInstalledModal'; +import ThemeInstalledModal from './theme/ThemeInstalledModal'; import ThemePreview from './theme/ThemePreview'; import {API} from '../../../utils/api'; import {OfficialTheme} from '../../../models/themes'; import {Theme} from '../../../types/api'; -import {showToast} from '../../../admin-x-ds/global/Toast'; import {useApi} from '../../providers/ServiceProvider'; import {useThemes} from '../../../hooks/useThemes'; @@ -68,17 +67,29 @@ async function handleThemeUpload({ let title = 'Upload successful'; let prompt = <> {uploadedTheme.name} uploaded successfully. - Do you want to activate it now? ; + if (!uploadedTheme.active) { + prompt = <> + {prompt}{' '} + Do you want to activate it now? + ; + } + if (uploadedTheme.errors?.length || uploadedTheme.warnings?.length) { const hasErrors = uploadedTheme.errors?.length; title = `Upload successful with ${hasErrors ? 'errors' : 'warnings'}`; prompt = <> The theme "{uploadedTheme.name}" was installed successfully but we detected some {hasErrors ? 'errors' : 'warnings'}. - You are still able to activate and use the theme but it is recommended to fix these {hasErrors ? 'errors' : 'warnings'} before you do so. ; + + if (!uploadedTheme.active) { + prompt = <> + {prompt} + You are still able to activate and use the theme but it is recommended to fix these {hasErrors ? 'errors' : 'warnings'} before you do so. + ; + } } NiceModal.show(ThemeInstalledModal, { @@ -120,7 +131,7 @@ const ThemeToolbar: React.FC = ({ prompt: ( <> The theme {themeFileName} already exists. - Do you want to overwrite it ? + Do you want to overwrite it? ), okLabel: 'Overwrite', @@ -128,9 +139,9 @@ const ThemeToolbar: React.FC = ({ okRunningLabel: 'Overwriting...', okColor: 'red', onOk: async (confirmModal) => { - confirmModal?.remove(); + await handleThemeUpload({api, file, setThemes}); setCurrentTab('installed'); - handleThemeUpload({api, file, setThemes}); + confirmModal?.remove(); } }); } else { @@ -176,6 +187,7 @@ const ChangeThemeModal = NiceModal.create(() => { const [currentTab, setCurrentTab] = useState('official'); const [selectedTheme, setSelectedTheme] = useState(null); const [previewMode, setPreviewMode] = useState('desktop'); + const [isInstalling, setInstalling] = useState(false); const modal = useModal(); const {themes, setThemes} = useThemes(); @@ -190,16 +202,50 @@ const ChangeThemeModal = NiceModal.create(() => { if (selectedTheme) { installedTheme = themes.find(theme => theme.name.toLowerCase() === selectedTheme!.name.toLowerCase()); onInstall = async () => { + setInstalling(true); const data = await api.themes.install(selectedTheme.ref); + setInstalling(false); + const newlyInstalledTheme = data.themes[0]; setThemes([ ...themes.map(theme => ({...theme, active: false})), newlyInstalledTheme ]); - showToast({ - message: `Theme installed - ${newlyInstalledTheme.name}` + + let title = 'Success'; + let prompt = <> + {newlyInstalledTheme.name} has been successfully installed. + ; + + if (!newlyInstalledTheme.active) { + prompt = <> + {prompt}{' '} + Do you want to activate it now? + ; + } + + if (newlyInstalledTheme.errors?.length || newlyInstalledTheme.warnings?.length) { + const hasErrors = newlyInstalledTheme.errors?.length; + + title = `Installed with ${hasErrors ? 'errors' : 'warnings'}`; + prompt = <> + The theme "{newlyInstalledTheme.name}" was installed successfully but we detected some {hasErrors ? 'errors' : 'warnings'}. + ; + + if (!newlyInstalledTheme.active) { + prompt = <> + {prompt} + You are still able to activate and use the theme but it is recommended to contact the theme developer fix these {hasErrors ? 'errors' : 'warnings'} before you do so. + ; + } + } + + NiceModal.show(ThemeInstalledModal, { + title, + prompt, + installedTheme: newlyInstalledTheme, + setThemes }); - setCurrentTab('installed'); }; } @@ -217,11 +263,10 @@ const ChangeThemeModal = NiceModal.create(() => {
{selectedTheme && { setSelectedTheme(null); }} diff --git a/apps/admin-x-settings/src/components/settings/site/ThemeInstalledModal.tsx b/apps/admin-x-settings/src/components/settings/site/theme/ThemeInstalledModal.tsx similarity index 61% rename from apps/admin-x-settings/src/components/settings/site/ThemeInstalledModal.tsx rename to apps/admin-x-settings/src/components/settings/site/theme/ThemeInstalledModal.tsx index 12102703df..26219d05f7 100644 --- a/apps/admin-x-settings/src/components/settings/site/ThemeInstalledModal.tsx +++ b/apps/admin-x-settings/src/components/settings/site/theme/ThemeInstalledModal.tsx @@ -1,12 +1,13 @@ -import Button from '../../../admin-x-ds/global/Button'; -import Heading from '../../../admin-x-ds/global/Heading'; -import List from '../../../admin-x-ds/global/List'; -import ListItem from '../../../admin-x-ds/global/ListItem'; +import Button from '../../../../admin-x-ds/global/Button'; +import Heading from '../../../../admin-x-ds/global/Heading'; +import List from '../../../../admin-x-ds/global/List'; +import ListItem from '../../../../admin-x-ds/global/ListItem'; import NiceModal from '@ebay/nice-modal-react'; import React, {ReactNode, useState} from 'react'; -import {ConfirmationModalContent} from '../../../admin-x-ds/global/modal/ConfirmationModal'; -import {InstalledTheme, Theme, ThemeProblem} from '../../../types/api'; -import {useApi} from '../../providers/ServiceProvider'; +import {ConfirmationModalContent} from '../../../../admin-x-ds/global/modal/ConfirmationModal'; +import {InstalledTheme, Theme, ThemeProblem} from '../../../../types/api'; +import {showToast} from '../../../../admin-x-ds/global/Toast'; +import {useApi} from '../../../providers/ServiceProvider'; const ThemeProblemView = ({problem}:{problem: ThemeProblem}) => { const [isExpanded, setExpanded] = useState(false); @@ -59,33 +60,47 @@ const ThemeInstalledModal: React.FC<{
; } + let okLabel = `Activate${installedTheme.errors?.length ? ' with errors' : ''}`; + + if (installedTheme.active) { + okLabel = 'OK'; + } + return {prompt} + {errorPrompt} {warningPrompt} } title={title} onOk={async (activateModal) => { - const resData = await api.themes.activate(installedTheme.name); - const updatedTheme = resData.themes[0]; + if (!installedTheme.active) { + const resData = await api.themes.activate(installedTheme.name); + const updatedTheme = resData.themes[0]; - setThemes((_themes) => { - const updatedThemes: Theme[] = _themes.map((t) => { - if (t.name === updatedTheme.name) { - return updatedTheme; - } - return { - ...t, - active: false - }; + setThemes((_themes) => { + const updatedThemes: Theme[] = _themes.map((t) => { + if (t.name === updatedTheme.name) { + return updatedTheme; + } + return { + ...t, + active: false + }; + }); + return updatedThemes; }); - return updatedThemes; - }); + + showToast({ + type: 'success', + message: `${updatedTheme.name} is now your active theme.` + }); + } activateModal?.remove(); }} />; diff --git a/apps/admin-x-settings/src/components/settings/site/theme/ThemePreview.tsx b/apps/admin-x-settings/src/components/settings/site/theme/ThemePreview.tsx index 579aac84bd..7d42d0d5ce 100644 --- a/apps/admin-x-settings/src/components/settings/site/theme/ThemePreview.tsx +++ b/apps/admin-x-settings/src/components/settings/site/theme/ThemePreview.tsx @@ -1,21 +1,26 @@ import Breadcrumbs from '../../../../admin-x-ds/global/Breadcrumbs'; import Button from '../../../../admin-x-ds/global/Button'; import ButtonGroup from '../../../../admin-x-ds/global/ButtonGroup'; +import ConfirmationModal from '../../../../admin-x-ds/global/modal/ConfirmationModal'; import MobileChrome from '../../../../admin-x-ds/global/chrome/MobileChrome'; +import NiceModal from '@ebay/nice-modal-react'; import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader'; import React, {useState} from 'react'; import {OfficialTheme} from '../../../../models/themes'; +import {Theme} from '../../../../types/api'; const ThemePreview: React.FC<{ selectedTheme?: OfficialTheme; onBack: () => void; - themeInstalled?: boolean; + isInstalling?: boolean; + installedTheme?: Theme; installButtonLabel?: string; - onInstall?: () => void; + onInstall?: () => void | Promise; }> = ({ selectedTheme, onBack, - themeInstalled, + isInstalling, + installedTheme, installButtonLabel, onInstall }) => { @@ -25,6 +30,29 @@ const ThemePreview: React.FC<{ return null; } + const handleInstall = () => { + if (installedTheme) { + NiceModal.show(ConfirmationModal, { + title: 'Overwrite theme', + prompt: ( + <> + This will overwrite your existing version of {selectedTheme.name}{installedTheme?.active ? ', which is your active theme' : ''}. All custom changes will be lost. + + ), + okLabel: 'Overwrite', + okRunningLabel: 'Installing...', + cancelLabel: 'Cancel', + okColor: 'red', + onOk: async (confirmModal) => { + await onInstall?.(); + confirmModal?.remove(); + } + }); + } else { + onInstall?.(); + } + }; + const left =
; @@ -87,4 +115,4 @@ const ThemePreview: React.FC<{ ); }; -export default ThemePreview; \ No newline at end of file +export default ThemePreview; diff --git a/apps/admin-x-settings/test/e2e/site/theme.test.ts b/apps/admin-x-settings/test/e2e/site/theme.test.ts index e6a83119b4..aebe601b81 100644 --- a/apps/admin-x-settings/test/e2e/site/theme.test.ts +++ b/apps/admin-x-settings/test/e2e/site/theme.test.ts @@ -12,6 +12,14 @@ test.describe('Theme settings', async () => { active: false, templates: [] }] + }, + activate: { + themes: [{ + name: 'headline', + package: {}, + active: true, + templates: [] + }] } } }}); @@ -28,33 +36,23 @@ test.describe('Theme settings', async () => { await modal.getByRole('button', {name: /Casper/}).click(); - await expect(modal.getByRole('button', {name: 'Installed'})).toBeVisible(); - await expect(modal.getByRole('button', {name: 'Installed'})).toBeDisabled(); + await expect(modal.getByRole('button', {name: 'Update Casper'})).toBeVisible(); await expect(page.locator('iframe[title="Theme preview"]')).toHaveAttribute('src', 'https://demo.ghost.io/'); await modal.getByRole('button', {name: 'Official themes'}).click(); - // The "edition" theme is activated in fixtures - - await modal.getByRole('button', {name: /Edition/}).click(); - - await expect(modal.getByRole('button', {name: 'Activated'})).toBeVisible(); - await expect(modal.getByRole('button', {name: 'Activated'})).toBeDisabled(); - - await expect(page.locator('iframe[title="Theme preview"]')).toHaveAttribute('src', 'https://edition.ghost.io/'); - - await modal.getByRole('button', {name: 'Official themes'}).click(); - // Try installing another theme await modal.getByRole('button', {name: /Headline/}).click(); await modal.getByRole('button', {name: 'Install Headline'}).click(); - await expect(modal.getByRole('button', {name: 'Installed'})).toBeVisible(); - await expect(modal.getByRole('button', {name: 'Installed'})).toBeDisabled(); - await expect(page.getByTestId('toast')).toHaveText(/Theme installed - headline/); + await expect(page.getByTestId('confirmation-modal')).toHaveText(/successfully installed/); + + await page.getByRole('button', {name: 'Activate'}).click(); + + await expect(page.getByTestId('toast')).toHaveText(/headline is now your active theme/); expect(lastApiRequests.themes.install.url).toMatch(/\?source=github&ref=TryGhost%2FHeadline/); });