Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, {useState} from 'react';
import {ButtonColor} from '../button';

export interface ConfirmationModalProps {
title?: string;
title?: React.ReactNode;
prompt?: React.ReactNode;
cancelLabel?: string;
okLabel?: string;
Expand All @@ -16,6 +16,7 @@ export interface ConfirmationModalProps {
}) => void | Promise<void>;
customFooter?: boolean | React.ReactNode;
formSheet?: boolean;
stickyFooter?: boolean;
}

export const ConfirmationModalContent: React.FC<ConfirmationModalProps> = ({
Expand All @@ -28,7 +29,8 @@ export const ConfirmationModalContent: React.FC<ConfirmationModalProps> = ({
onCancel,
onOk,
customFooter,
formSheet = true
formSheet = true,
stickyFooter = false
}) => {
const modal = useModal();
const [taskState, setTaskState] = useState<'running' | ''>('');
Expand All @@ -41,6 +43,7 @@ export const ConfirmationModalContent: React.FC<ConfirmationModalProps> = ({
formSheet={formSheet}
okColor={okColor}
okLabel={taskState === 'running' ? okRunningLabel : okLabel}
stickyFooter={stickyFooter}
testId='confirmation-modal'
title={title}
width={540}
Expand Down
2 changes: 1 addition & 1 deletion apps/admin-x-design-system/src/global/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface ModalProps {
align?: 'center' | 'left' | 'right';

testId?: string;
title?: string;
title?: React.ReactNode;
okLabel?: string;
okColor?: ButtonColor;
okLoading?: boolean;
Expand Down
20 changes: 8 additions & 12 deletions apps/admin-x-settings/src/components/settings/site/theme-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
}

if (fatalErrors && !data) {
let title = 'Invalid Theme';
let prompt = <>This theme is invalid and cannot be activated. Fix the following errors and re-upload the theme</>;
let title = 'Theme not uploaded';
let prompt = <>This theme couldn&apos;t be uploaded because Ghost found a blocking validation error. Fix the issue below and upload the theme again.</>;
NiceModal.show(InvalidThemeModal, {
title,
prompt,
Expand Down Expand Up @@ -190,17 +190,15 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
}

if (uploadedTheme?.errors?.length || uploadedTheme.warnings?.length) {
const hasErrors = uploadedTheme?.errors?.length;

title = `Upload successful with ${hasErrors ? 'errors' : 'warnings'}`;
title = 'Upload successful';
prompt = <>
The theme <strong>&quot;{uploadedTheme.name}&quot;</strong> was installed but we detected some {hasErrors ? 'errors' : 'warnings'}.
The theme <strong>&quot;{uploadedTheme.name}&quot;</strong> was installed successfully.
</>;

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.
You can activate it when you&apos;re ready.
</>;
}
}
Expand Down Expand Up @@ -484,17 +482,15 @@ const ChangeThemeModal: React.FC<ChangeThemeModalProps> = ({source, themeRef}) =
}

if (newlyInstalledTheme.errors?.length || newlyInstalledTheme.warnings?.length) {
const hasErrors = newlyInstalledTheme.errors?.length;

title = `Installed with ${hasErrors ? 'errors' : 'warnings'}`;
title = 'Installed successfully';
prompt = <>
The theme <strong>&quot;{newlyInstalledTheme.name}&quot;</strong> was installed successfully but we detected some {hasErrors ? 'errors' : 'warnings'}.
The theme <strong>&quot;{newlyInstalledTheme.name}&quot;</strong> was installed successfully.
</>;

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.
You can activate it when you&apos;re ready.
</>;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ const ThemeActions: React.FC<ThemeActionProps> = ({
} else {
handleError(e);
}
let title = 'Invalid Theme';
let prompt = <>This theme is invalid and cannot be activated. Fix the following errors and re-upload the theme</>;
let title = 'Theme not activated';
let prompt = <>This theme couldn&apos;t be activated because Ghost found a blocking validation error. Fix the issue below and try again.</>;

if (fatalErrors) {
NiceModal.show(InvalidThemeModal, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,89 +1,50 @@
import NiceModal from '@ebay/nice-modal-react';
import React, {type ReactNode, useState} from 'react';
import {Button, ConfirmationModalContent, Heading, List, ListItem} from '@tryghost/admin-x-design-system';
import {type ThemeProblem} from '@tryghost/admin-x-framework/api/themes';
import React, {type ReactNode} from 'react';
import {ConfirmationModalContent} from '@tryghost/admin-x-design-system';
import {ErrorTextCard, type FatalErrors, ThemeValidationDetailsDisclosure, ValidationProblemCard, getIssuesFromFatalErrors} from './theme-validation-details';
import {useBrowseConfig} from '@tryghost/admin-x-framework/api/config';

type FatalError = {
details: {
errors: ThemeProblem[];
}|string;
};

export type FatalErrors = FatalError[];

export const ThemeProblemView = ({problem}:{problem: ThemeProblem}) => {
const [isExpanded, setExpanded] = useState(false);

const handleClick = () => {
setExpanded(!isExpanded);
};

return <ListItem
title={
<>
<div className={`${problem.level === 'error' ? 'before:bg-red' : 'before:bg-yellow'} relative px-4 text-sm before:absolute before:top-1.5 before:left-0 before:block before:size-2 before:rounded-full before:content-['']`}>
{
problem?.fatal ?
<strong>Fatal: </strong>
:
<strong>{problem.level === 'error' ? 'Error: ' : 'Warning: '}</strong>
}
<span dangerouslySetInnerHTML={{__html: problem.rule}} />
<div className='absolute top-1 -right-4'>
<Button color="green" icon={isExpanded ? 'chevron-down' : 'chevron-right'} iconColorClass='text-grey-700' size='sm' link onClick={() => handleClick()} />
</div>
</div>
{
isExpanded ?
<div className='mt-2 px-4 text-[13px]'>
<div dangerouslySetInnerHTML={{__html: problem.details}} className='mb-4' />
<Heading level={6}>Affected files:</Heading>
<ul className='mt-1'>
{problem.failures.map(failure => <li><code>{failure.ref}</code>{failure.message ? `: ${failure.message}` : ''}</li>)}
</ul>
</div> :
null
}
</>
}
hideActions
separator
/>;
};
export type {FatalErrors} from './theme-validation-details';

const InvalidThemeModal: React.FC<{
title: string
prompt: ReactNode
fatalErrors?: FatalErrors;
validationDetailsDefaultOpen?: boolean;
onRetry?: (modal?: {
remove: () => void;
}) => void | Promise<void>;
}> = ({title, prompt, fatalErrors, onRetry}) => {
let warningPrompt = null;
if (fatalErrors) {
warningPrompt = <div className="mt-10">
<List title="Errors">
{fatalErrors.map((error) => {
if (typeof error.details === 'object' && error.details.errors && error.details.errors.length > 0) {
return error.details.errors.map(err => <ThemeProblemView problem={err} />);
} else if (typeof error.details === 'string') {
return <ListItem title={error.details} />;
} else {
return null;
}
})}
</List>
</div>;
}
}> = ({title, prompt, fatalErrors, validationDetailsDefaultOpen, onRetry}) => {
const {data: configData} = useBrowseConfig();
const defaultOpen = validationDetailsDefaultOpen ?? configData?.config?.environment === 'development';
const {blockingProblems, secondaryProblems, stringErrors} = getIssuesFromFatalErrors(fatalErrors);
const blockingIssueCount = blockingProblems.length + stringErrors.length;
const promptText = prompt ?? <>Ghost found {blockingIssueCount === 1 ? 'a blocking validation error' : `${blockingIssueCount} blocking validation errors`} and did not save your theme. Fix {blockingIssueCount === 1 ? 'the issue' : 'the issues'} below and try again.</>;

return <ConfirmationModalContent
cancelLabel='Close'
okColor='black'
okLabel={'Retry'}
prompt={<>
{prompt}
{warningPrompt}
<div className='space-y-5'>
<div className='text-sm text-foreground'>{promptText}</div>

{(blockingProblems.length > 0 || stringErrors.length > 0) && (
<div className='space-y-3'>
{blockingProblems.map(problem => (
<ValidationProblemCard key={problem.code} problem={problem} prominent />
))}
{stringErrors.map(error => <ErrorTextCard key={error} message={error} />)}
</div>
)}

<ThemeValidationDetailsDisclosure
defaultOpen={defaultOpen}
problems={secondaryProblems}
/>
</div>
</>}
stickyFooter={true}
title={title}
onOk={onRetry}
/>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -812,8 +812,7 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => {
if (!response.ok) {
if (response.status === 422 && data?.errors) {
NiceModal.show(InvalidThemeModal, {
title: 'Invalid Theme',
prompt: <>Fix the validation errors below and try saving again.</>,
title: 'Theme not saved',
fatalErrors: data.errors as FatalErrors
});
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,89 +1,77 @@
import NiceModal from '@ebay/nice-modal-react';
import React, {type ReactNode, useState} from 'react';
import React, {type ReactNode} from 'react';
import useCustomFonts from '../../../../hooks/use-custom-fonts';
import {Button, ConfirmationModalContent, Heading, List, ListItem, showToast} from '@tryghost/admin-x-design-system';
import {type InstalledTheme, type ThemeProblem, useActivateTheme} from '@tryghost/admin-x-framework/api/themes';
import {ConfirmationModalContent, showToast} from '@tryghost/admin-x-design-system';
import {type InstalledTheme, useActivateTheme} from '@tryghost/admin-x-framework/api/themes';
import {OutcomeBanner, ThemeValidationDetailsDisclosure, getIssuesFromInstalledTheme} from './theme-validation-details';
import {getHomepageUrl, useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {useBrowseConfig} from '@tryghost/admin-x-framework/api/config';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';

export const ThemeProblemView = ({problem}:{problem: ThemeProblem}) => {
const [isExpanded, setExpanded] = useState(false);

return <ListItem
title={
<>
<div className={`${problem.level === 'error' ? 'before:bg-red' : 'before:bg-yellow'} relative px-4 text-sm before:absolute before:top-1.5 before:left-0 before:block before:size-2 before:rounded-full before:content-['']`}>
<strong>{problem.level === 'error' ? 'Error: ' : 'Warning: '}</strong>
<span dangerouslySetInnerHTML={{__html: problem.rule}} />
<div className='absolute top-1 -right-4'>
<Button color="green" icon={isExpanded ? 'chevron-down' : 'chevron-right'} iconColorClass='text-grey-700' size='sm' link onClick={() => setExpanded(!isExpanded)} />
</div>
</div>
{
isExpanded ?
<div className='mt-2 px-4 text-[13px]'>
<div dangerouslySetInnerHTML={{__html: problem.details}} className='mb-4' />
<Heading level={6}>Affected files:</Heading>
<ul className='mt-1'>
{problem.failures.map(failure => <li key={failure.ref}><code>{failure.ref}</code>{failure.message ? `: ${failure.message}` : ''}</li>)}
</ul>
</div> :
null
}
</>
}
hideActions
separator
/>;
};

const ThemeInstalledModal: React.FC<{
title: string
prompt: ReactNode
installedTheme: InstalledTheme;
validationDetailsDefaultOpen?: boolean;
onActivate?: () => void;
}> = ({title, prompt, installedTheme, onActivate}) => {
}> = ({title, installedTheme, validationDetailsDefaultOpen, onActivate}) => {
const {mutateAsync: activateTheme} = useActivateTheme();
const {refreshActiveThemeData} = useCustomFonts();
const handleError = useHandleError();
const {data: configData} = useBrowseConfig();
const {data: siteData} = useBrowseSite();
const defaultOpen = validationDetailsDefaultOpen ?? configData?.config?.environment === 'development';
const secondaryProblems = getIssuesFromInstalledTheme(installedTheme);
const homepageUrl = siteData?.site ? getHomepageUrl(siteData.site) : undefined;

/* eslint-disable react/no-array-index-key */
let errorPrompt = null;
if (installedTheme && installedTheme.errors) {
errorPrompt = <div className="mt-6">
<List hint={<>Highly recommended to fix, functionality <strong>could</strong> be restricted</>} title="Errors">
{installedTheme.errors?.map((error, index) => <ThemeProblemView key={index} problem={error} />)}
</List>
</div>;
}

let warningPrompt = null;
if (installedTheme && installedTheme.warnings) {
warningPrompt = <div className="mt-10">
<List title="Warnings">
{installedTheme.warnings?.map((warning, index) => <ThemeProblemView key={index} problem={warning} />)}
</List>
</div>;
}
/* eslint-enable react/no-array-index-key */

let okLabel = `Activate${installedTheme.errors?.length ? ' with errors' : ''}`;
let okLabel = 'Activate theme';

if (installedTheme.active) {
okLabel = 'OK';
}

const modalTitle = installedTheme.active ? <span className='text-green'>It&apos;s live!</span> : title;
const outcomeTitle = 'Uploaded successfully';
const outcomeCopy = installedTheme.active ? (
<>
Your theme <strong>{installedTheme.name}</strong> was saved successfully and is now visible to your readers.
{homepageUrl ? <>
{' '}<a className='font-semibold text-black hover:underline dark:text-white' href={homepageUrl} rel='noreferrer' target='_blank'>Take a look →</a>
</> : null}
</>
) : (
<>
<strong>{installedTheme.name}</strong> has been uploaded. Activate it to make it live.
</>
);

return <ConfirmationModalContent
cancelLabel='Close'
okColor='black'
okLabel={okLabel}
okRunningLabel='Activating...'
prompt={<>
{prompt}
<div className='space-y-5'>
{installedTheme.active ? (
<div className='space-y-2 text-sm text-foreground'>
<p>{outcomeCopy}</p>
</div>
) : (
<OutcomeBanner title={outcomeTitle} variant='success'>
<div className='space-y-2'>
<p>{outcomeCopy}</p>
</div>
</OutcomeBanner>
)}

{errorPrompt}
{warningPrompt}
<ThemeValidationDetailsDisclosure
defaultOpen={defaultOpen}
problems={secondaryProblems}
/>
</div>
</>}
title={title}
stickyFooter={true}
title={modalTitle}
onOk={async (activateModal) => {
if (!installedTheme.active) {
try {
Expand Down
Loading
Loading