Wired theme change UI in adminX

refs https://github.com/TryGhost/Team/issues/3432

- wires list of installed themes
- allows actions on themes like upload, activate, delete and download
- cleans up theme modal content and toolbar into granular components
This commit is contained in:
Rishabh 2023-06-13 22:00:40 +05:30
parent 3dbc2ee507
commit 36ced958f8
3 changed files with 267 additions and 35 deletions

View File

@ -4,38 +4,43 @@ import ButtonGroup from '../../../admin-x-ds/global/ButtonGroup';
import FileUpload from '../../../admin-x-ds/global/FileUpload';
import Modal from '../../../admin-x-ds/global/Modal';
import NewThemePreview from './theme/ThemePreview';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
import OfficialThemes from './theme/OfficialThemes';
import React, {useState} from 'react';
import TabView from '../../../admin-x-ds/global/TabView';
import {useState} from 'react';
import {Theme} from '../../../types/api';
import {showToast} from '../../../admin-x-ds/global/Toast';
import {useApi} from '../../providers/ServiceProvider';
import {useThemes} from '../../../hooks/useThemes';
const ChangeThemeModal = NiceModal.create(() => {
const [currentTab, setCurrentTab] = useState('official');
const [selectedTheme, setSelectedTheme] = useState('');
interface ThemeToolbarProps {
selectedTheme: string;
setCurrentTab: (tab: string) => void;
setSelectedTheme: (theme: string) => void;
modal: NiceModalHandler<Record<string, unknown>>;
themes: Theme[];
setThemes: (themes: Theme[]) => void;
}
const modal = useModal();
interface ThemeModalContentProps {
selectedTheme: string;
onSelectTheme: (theme: string) => void;
currentTab: string;
themes: Theme[];
setThemes: (themes: Theme[]) => void;
}
const onSelectTheme = (theme: string) => {
setSelectedTheme(theme);
};
let content;
switch (currentTab) {
case 'official':
if (selectedTheme) {
content = <NewThemePreview selectedTheme={selectedTheme} />;
} else {
content = <OfficialThemes onSelectTheme={onSelectTheme} />;
}
break;
case 'installed':
content = <AdvancedThemeSettings />;
break;
}
let toolBar;
const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
selectedTheme,
setCurrentTab,
setSelectedTheme,
modal,
themes,
setThemes
}) => {
const api = useApi();
if (selectedTheme) {
toolBar =
return (
<div className='sticky top-0 flex justify-between gap-3 bg-white p-5 px-7'>
<div className='flex w-[33%] items-center gap-2'>
<button
@ -59,9 +64,10 @@ const ChangeThemeModal = NiceModal.create(() => {
/>
<Button color='green' label={`Install ${selectedTheme}`} />
</div>
</div>;
</div>
);
} else {
toolBar =
return (
<div className='sticky top-0 flex justify-between gap-3 bg-white p-5 px-7'>
<TabView
border={false}
@ -75,8 +81,13 @@ const ChangeThemeModal = NiceModal.create(() => {
/>
<div className='flex items-center gap-3'>
<FileUpload id='theme-uplaod' onUpload={(file: File) => {
alert(file.name);
<FileUpload id='theme-uplaod' onUpload={async (file: File) => {
const data = await api.themes.upload({file});
const uploadedTheme = data.themes[0];
setThemes([...themes, uploadedTheme]);
showToast({
message: `Theme uploaded - ${uploadedTheme.name}`
});
}}>Upload theme</FileUpload>
<Button
className='min-w-[75px]'
@ -86,8 +97,50 @@ const ChangeThemeModal = NiceModal.create(() => {
modal.remove();
}} />
</div>
</div>;
</div>
);
}
};
const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
currentTab,
selectedTheme,
onSelectTheme,
themes,
setThemes
}) => {
switch (currentTab) {
case 'official':
if (selectedTheme) {
return (
<NewThemePreview selectedTheme={selectedTheme} />
);
} else {
return (
<OfficialThemes onSelectTheme={onSelectTheme} />
);
}
case 'installed':
return (
<AdvancedThemeSettings
setThemes={setThemes}
themes={themes}
/>
);
}
return null;
};
const ChangeThemeModal = NiceModal.create(() => {
const [currentTab, setCurrentTab] = useState('official');
const [selectedTheme, setSelectedTheme] = useState('');
const modal = useModal();
const {themes, setThemes} = useThemes();
const onSelectTheme = (theme: string) => {
setSelectedTheme(theme);
};
return (
<Modal
@ -99,8 +152,21 @@ const ChangeThemeModal = NiceModal.create(() => {
>
<div className='flex h-full justify-between'>
<div className='grow'>
{toolBar}
{content}
<ThemeToolbar
modal={modal}
selectedTheme={selectedTheme}
setCurrentTab={setCurrentTab}
setSelectedTheme={setSelectedTheme}
setThemes={setThemes}
themes={themes}
/>
<ThemeModalContent
currentTab={currentTab}
selectedTheme={selectedTheme}
setThemes={setThemes}
themes={themes}
onSelectTheme={onSelectTheme}
/>
</div>
</div>
</Modal>

View File

@ -1,12 +1,165 @@
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 React from 'react';
import {Theme} from '../../../../types/api';
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
import {isActiveTheme, isDefaultTheme, isDeletableTheme} from '../../../../models/themes';
import {useApi} from '../../../providers/ServiceProvider';
const AdvancedThemeSettings: React.FC = () => {
interface ThemeActionProps {
theme: Theme;
themes: Theme[];
updateThemes: (themes: Theme[]) => void;
}
interface ThemeSettingProps {
themes: Theme[];
setThemes: (themes: Theme[]) => void;
}
function getThemeLabel(theme: Theme): string {
let label = theme.package?.name || theme.name;
if (isDefaultTheme(theme)) {
label += ' (default)';
} else {
label += ` (${theme.name})`;
}
if (isActiveTheme(theme)) {
label += ' (active)';
}
return label;
}
function getThemeVersion(theme: Theme): string {
return theme.package?.version || '1.0';
}
const ThemeActions: React.FC<ThemeActionProps> = ({
theme,
themes,
updateThemes
}) => {
const api = useApi();
const handleActivate = async () => {
const data = await api.themes.activate(theme.name);
const updatedTheme = data.themes[0];
const updatedThemes: Theme[] = themes.map((t) => {
if (t.name === updatedTheme.name) {
return updatedTheme;
}
return {
...t,
active: false
};
});
updateThemes(updatedThemes);
};
const handleDelete = async () => {
await api.themes.delete(theme.name);
const updatedThemes = themes.filter(t => t.name !== theme.name);
updateThemes(updatedThemes);
};
const handleDownload = async () => {
const {apiRoot} = getGhostPaths();
downloadFile(`${apiRoot}/themes/${theme.name}/download`);
};
let actions = [];
if (isDeletableTheme(theme)) {
actions.push(
<Button
key='delete'
color='red'
label={'Delete'}
link={true}
onClick={handleDelete}
/>
);
}
if (!isActiveTheme(theme)) {
actions.push(
<Button
key='activate'
className='ml-2'
color='green'
label={'Activate'}
link={true}
onClick={handleActivate}
/>
);
}
actions.push(
<Button
key='download'
className='ml-2'
color='green'
label={'Download'}
link={true}
onClick={handleDownload}
/>
);
return (
<div className='flex gap-2'>
{actions}
</div>
);
};
const ThemeList:React.FC<ThemeSettingProps> = ({
themes,
setThemes
}) => {
return (
<List
title='Installed themes'
>
{themes.map((theme) => {
const label = getThemeLabel(theme);
const detail = getThemeVersion(theme);
return (
<ListItem
key={theme.name}
action={
<ThemeActions
theme={theme}
themes={themes}
updateThemes={setThemes}
/>
}
detail={detail}
id={`theme-${theme.name}`}
title={label}
/>
);
})}
</List>
);
};
const AdvancedThemeSettings: React.FC<ThemeSettingProps> = ({
themes,
setThemes
}) => {
return (
<div className='p-[8vmin] pt-5'>
<Heading>Installed themes</Heading>
<div className='mt-5'>
List of installed themes
<ThemeList
setThemes={setThemes}
themes={themes}
/>
</div>
</div>
);

View File

@ -79,3 +79,16 @@ export function isOwnerUser(user: User) {
export function isAdminUser(user: User) {
return user.roles.some(role => role.name === 'Administrator');
}
export function downloadFile(url: string) {
let iframe = document.getElementById('iframeDownload');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'iframeDownload';
iframe.style.display = 'none';
document.body.append(iframe);
}
iframe.setAttribute('src', url);
}