mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-29 13:52:10 +03:00
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:
parent
3dbc2ee507
commit
36ced958f8
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user