mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Regrouped design setting tabs
REF DES-831 - Changed tabs from `Brand`, `Site wide`, `Homepage` and `Post` to `Global` and `Theme settings`. - Added `Site wide`, `Homepage` and `Post` grouping inside the `Theme settings` tab.
This commit is contained in:
parent
4bf7b86ef4
commit
7725d78670
6 changed files with 380 additions and 122 deletions
|
@ -28,7 +28,7 @@ const ToggleGroup: React.FC<ToggleGroupProps> = ({children, gap = 'md', classNam
|
||||||
}
|
}
|
||||||
|
|
||||||
className = clsx(
|
className = clsx(
|
||||||
'flex flex-col gap-3',
|
'flex flex-col',
|
||||||
gapClass,
|
gapClass,
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import BrandSettings, {BrandSettingValues} from './designAndBranding/BrandSettings';
|
import GlobalSettings, {GlobalSettingValues} from './designAndBranding/GlobalSettings';
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import ThemePreview from './designAndBranding/ThemePreview';
|
import ThemePreview from './designAndBranding/ThemePreview';
|
||||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||||
|
@ -13,32 +13,32 @@ import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
|
||||||
const Sidebar: React.FC<{
|
const Sidebar: React.FC<{
|
||||||
brandSettings: BrandSettingValues
|
globalSettings: GlobalSettingValues
|
||||||
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
|
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
|
||||||
updateBrandSetting: (key: string, value: SettingValue) => void
|
updateGlobalSetting: (key: string, value: SettingValue) => void
|
||||||
updateThemeSetting: (updated: CustomThemeSetting) => void
|
updateThemeSetting: (updated: CustomThemeSetting) => void
|
||||||
onTabChange: (id: string) => void
|
onTabChange: (id: string) => void
|
||||||
handleSave: () => Promise<boolean>
|
handleSave: () => Promise<boolean>
|
||||||
}> = ({
|
}> = ({
|
||||||
brandSettings,
|
globalSettings,
|
||||||
themeSettingSections,
|
themeSettingSections,
|
||||||
updateBrandSetting,
|
updateGlobalSetting,
|
||||||
updateThemeSetting,
|
updateThemeSetting,
|
||||||
onTabChange
|
onTabChange
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedTab, setSelectedTab] = useState('brand');
|
const [selectedTab, setSelectedTab] = useState('global');
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
{
|
{
|
||||||
id: 'brand',
|
id: 'global',
|
||||||
title: 'Brand',
|
title: 'Global',
|
||||||
contents: <BrandSettings updateSetting={updateBrandSetting} values={brandSettings} />
|
contents: <GlobalSettings updateSetting={updateGlobalSetting} values={globalSettings} />
|
||||||
},
|
},
|
||||||
...themeSettingSections.map(({id, title, settings}) => ({
|
{
|
||||||
id,
|
id: 'theme-settings',
|
||||||
title,
|
title: 'Theme settings',
|
||||||
contents: <ThemeSettings settings={settings} updateSetting={updateThemeSetting} />
|
contents: <ThemeSettings sections={themeSettingSections} updateSetting={updateThemeSetting} />
|
||||||
}))
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleTabChange = (id: string) => {
|
const handleTabChange = (id: string) => {
|
||||||
|
@ -52,7 +52,7 @@ const Sidebar: React.FC<{
|
||||||
{tabs.length > 1 ?
|
{tabs.length > 1 ?
|
||||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={handleTabChange} />
|
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={handleTabChange} />
|
||||||
:
|
:
|
||||||
<BrandSettings updateSetting={updateBrandSetting} values={brandSettings} />
|
<GlobalSettings updateSetting={updateGlobalSetting} values={globalSettings} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -111,7 +111,7 @@ const DesignModal: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [setFormState, themeSettings]);
|
}, [setFormState, themeSettings]);
|
||||||
|
|
||||||
const updateBrandSetting = (key: string, value: SettingValue) => {
|
const updateGlobalSetting = (key: string, value: SettingValue) => {
|
||||||
updateForm(state => ({...state, settings: state.settings.map(setting => (
|
updateForm(state => ({...state, settings: state.settings.map(setting => (
|
||||||
setting.key === key ? {...setting, value, dirty: true} : setting
|
setting.key === key ? {...setting, value, dirty: true} : setting
|
||||||
))}));
|
))}));
|
||||||
|
@ -188,10 +188,10 @@ const DesignModal: React.FC = () => {
|
||||||
/>;
|
/>;
|
||||||
const sidebarContent =
|
const sidebarContent =
|
||||||
<Sidebar
|
<Sidebar
|
||||||
brandSettings={{description, accentColor, icon, logo, coverImage, headingFont, bodyFont}}
|
globalSettings={{description, accentColor, icon, logo, coverImage, headingFont, bodyFont}}
|
||||||
handleSave={handleSave}
|
handleSave={handleSave}
|
||||||
themeSettingSections={themeSettingSections}
|
themeSettingSections={themeSettingSections}
|
||||||
updateBrandSetting={updateBrandSetting}
|
updateGlobalSetting={updateGlobalSetting}
|
||||||
updateThemeSetting={updateThemeSetting}
|
updateThemeSetting={updateThemeSetting}
|
||||||
onTabChange={onTabChange}
|
onTabChange={onTabChange}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
import UnsplashSelector from '../../../selectors/UnsplashSelector';
|
||||||
|
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
||||||
|
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||||
|
import {CUSTOM_FONTS} from '@tryghost/custom-fonts';
|
||||||
|
import {ColorPickerField, Form, Heading, Hint, ImageUpload, Select} from '@tryghost/admin-x-design-system';
|
||||||
|
import {SettingValue, getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||||
|
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||||
|
import {useFramework} from '@tryghost/admin-x-framework';
|
||||||
|
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||||
|
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||||
|
import type {Font} from '@tryghost/custom-fonts';
|
||||||
|
|
||||||
|
// TODO: create custom types for heading and body fonts in @tryghost/custom-fonts, so we can extend
|
||||||
|
// them separately
|
||||||
|
type BodyFontOption = {
|
||||||
|
value: Font | 'Theme default',
|
||||||
|
label: Font | 'Theme default'
|
||||||
|
};
|
||||||
|
type HeadingFontOption = BodyFontOption;
|
||||||
|
|
||||||
|
export interface GlobalSettingValues {
|
||||||
|
description: string
|
||||||
|
accentColor: string
|
||||||
|
icon: string | null
|
||||||
|
logo: string | null
|
||||||
|
coverImage: string | null
|
||||||
|
headingFont: string
|
||||||
|
bodyFont: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_FONT = 'Theme default';
|
||||||
|
|
||||||
|
const GlobalSettings: React.FC<{ values: GlobalSettingValues, updateSetting: (key: string, value: SettingValue) => void }> = ({values,updateSetting}) => {
|
||||||
|
const {mutateAsync: uploadImage} = useUploadImage();
|
||||||
|
const {settings} = useGlobalData();
|
||||||
|
const [unsplashEnabled] = getSettingValues<boolean>(settings, ['unsplash']);
|
||||||
|
const [showUnsplash, setShowUnsplash] = useState<boolean>(false);
|
||||||
|
const {unsplashConfig} = useFramework();
|
||||||
|
const handleError = useHandleError();
|
||||||
|
|
||||||
|
const editor = usePinturaEditor();
|
||||||
|
|
||||||
|
const [headingFont, setHeadingFont] = useState(values.headingFont || DEFAULT_FONT);
|
||||||
|
const [bodyFont, setBodyFont] = useState(values.bodyFont || DEFAULT_FONT);
|
||||||
|
|
||||||
|
// TODO: replace with getCustomFonts() once custom-fonts is updated and differentiates
|
||||||
|
// between heading and body fonts
|
||||||
|
const customHeadingFonts: HeadingFontOption[] = CUSTOM_FONTS.map(x => ({label: x, value: x}));
|
||||||
|
customHeadingFonts.unshift({label: DEFAULT_FONT, value: DEFAULT_FONT});
|
||||||
|
|
||||||
|
const customBodyFonts: BodyFontOption[] = CUSTOM_FONTS.map(x => ({label: x, value: x}));
|
||||||
|
customBodyFonts.unshift({label: DEFAULT_FONT, value: DEFAULT_FONT});
|
||||||
|
|
||||||
|
const selectedHeadingFont = {label: headingFont, value: headingFont};
|
||||||
|
const selectedBodyFont = {label: bodyFont, value: bodyFont};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form className='mt-4' gap='sm' margins='lg' title=''>
|
||||||
|
<ColorPickerField
|
||||||
|
debounceMs={200}
|
||||||
|
direction='rtl'
|
||||||
|
title={<Heading className='mt-[3px]' level={6}>Accent color</Heading>}
|
||||||
|
value={values.accentColor}
|
||||||
|
// we debounce this because the color picker fires a lot of events.
|
||||||
|
onChange={value => updateSetting('accent_color', value)}
|
||||||
|
/>
|
||||||
|
<div className='flex items-start justify-between'>
|
||||||
|
<div>
|
||||||
|
<Heading level={6}>Publication icon</Heading>
|
||||||
|
<Hint className='!mt-0 mr-5 max-w-[160px]'>A square, social icon, at least 60x60px</Hint>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-3'>
|
||||||
|
<ImageUpload
|
||||||
|
deleteButtonClassName='!top-1 !right-1'
|
||||||
|
editButtonClassName='!top-1 !right-1'
|
||||||
|
height={values.icon ? '66px' : '36px'}
|
||||||
|
id='logo'
|
||||||
|
imageBWCheckedBg={true}
|
||||||
|
imageURL={values.icon || ''}
|
||||||
|
width={values.icon ? '66px' : '160px'}
|
||||||
|
onDelete={() => updateSetting('icon', null)}
|
||||||
|
onUpload={async (file) => {
|
||||||
|
try {
|
||||||
|
updateSetting('icon', getImageUrl(await uploadImage({file})));
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as APIError;
|
||||||
|
if (error.response!.status === 415) {
|
||||||
|
error.message = 'Unsupported file type';
|
||||||
|
}
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upload icon
|
||||||
|
</ImageUpload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-start justify-between ${values.icon && 'mt-2'}`}>
|
||||||
|
<div>
|
||||||
|
<Heading level={6}>Publication logo</Heading>
|
||||||
|
<Hint className='!mt-0 mr-5 max-w-[160px]'>Appears usually in the main header of your theme</Hint>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ImageUpload
|
||||||
|
deleteButtonClassName='!top-1 !right-1'
|
||||||
|
height='60px'
|
||||||
|
id='site-logo'
|
||||||
|
imageBWCheckedBg={true}
|
||||||
|
imageFit='contain'
|
||||||
|
imageURL={values.logo || ''}
|
||||||
|
width='160px'
|
||||||
|
onDelete={() => updateSetting('logo', null)}
|
||||||
|
onUpload={async (file) => {
|
||||||
|
try {
|
||||||
|
updateSetting('logo', getImageUrl(await uploadImage({file})));
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as APIError;
|
||||||
|
if (error.response!.status === 415) {
|
||||||
|
error.message = 'Unsupported file type';
|
||||||
|
}
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upload logo
|
||||||
|
</ImageUpload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mt-2 flex items-start justify-between'>
|
||||||
|
<div>
|
||||||
|
<Heading level={6}>Publication cover</Heading>
|
||||||
|
<Hint className='!mt-0 mr-5 max-w-[160px]'>Usually as a large banner image on your index pages</Hint>
|
||||||
|
</div>
|
||||||
|
<ImageUpload
|
||||||
|
deleteButtonClassName='!top-1 !right-1'
|
||||||
|
editButtonClassName='!top-1 !right-10'
|
||||||
|
height='95px'
|
||||||
|
id='cover'
|
||||||
|
imageURL={values.coverImage || ''}
|
||||||
|
openUnsplash={() => setShowUnsplash(true)}
|
||||||
|
pintura={
|
||||||
|
{
|
||||||
|
isEnabled: editor.isEnabled,
|
||||||
|
openEditor: async () => editor.openEditor({
|
||||||
|
image: values.coverImage || '',
|
||||||
|
handleSave: async (file:File) => {
|
||||||
|
try {
|
||||||
|
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unsplashButtonClassName='!bg-transparent !h-6 !top-1.5 !w-6 !right-1.5 z-50'
|
||||||
|
unsplashEnabled={unsplashEnabled}
|
||||||
|
width='160px'
|
||||||
|
onDelete={() => updateSetting('cover_image', null)}
|
||||||
|
onUpload={async (file: any) => {
|
||||||
|
try {
|
||||||
|
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||||
|
} catch (e) {
|
||||||
|
const error = e as APIError;
|
||||||
|
if (error.response!.status === 415) {
|
||||||
|
error.message = 'Unsupported file type';
|
||||||
|
}
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upload cover
|
||||||
|
</ImageUpload>
|
||||||
|
{
|
||||||
|
showUnsplash && unsplashConfig && unsplashEnabled && (
|
||||||
|
<UnsplashSelector
|
||||||
|
unsplashProviderConfig={unsplashConfig}
|
||||||
|
onClose={() => {
|
||||||
|
setShowUnsplash(false);
|
||||||
|
}}
|
||||||
|
onImageInsert={(image) => {
|
||||||
|
if (image.src) {
|
||||||
|
updateSetting('cover_image', image.src);
|
||||||
|
}
|
||||||
|
setShowUnsplash(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
<Form className='-mt-4' gap='sm' margins='lg' title='Typography'>
|
||||||
|
<Select
|
||||||
|
hint={''}
|
||||||
|
options={customHeadingFonts}
|
||||||
|
selectedOption={selectedHeadingFont}
|
||||||
|
title={'Heading font'}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option?.value === DEFAULT_FONT) {
|
||||||
|
setHeadingFont(DEFAULT_FONT);
|
||||||
|
updateSetting('heading_font', '');
|
||||||
|
} else {
|
||||||
|
setHeadingFont(option?.value || '');
|
||||||
|
updateSetting('heading_font', option?.value || '');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
hint={''}
|
||||||
|
options={customBodyFonts}
|
||||||
|
selectedOption={selectedBodyFont}
|
||||||
|
title={'Body font'}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option?.value === DEFAULT_FONT) {
|
||||||
|
setBodyFont(DEFAULT_FONT);
|
||||||
|
updateSetting('body_font', '');
|
||||||
|
} else {
|
||||||
|
setBodyFont(option?.value || '');
|
||||||
|
updateSetting('body_font', option?.value || '');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlobalSettings;
|
|
@ -3,7 +3,7 @@ import React, {useCallback} from 'react';
|
||||||
import {CustomThemeSetting, hiddenCustomThemeSettingValue} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
import {CustomThemeSetting, hiddenCustomThemeSettingValue} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
||||||
import {isCustomThemeSettingVisible} from '../../../../utils/isCustomThemeSettingsVisible';
|
import {isCustomThemeSettingVisible} from '../../../../utils/isCustomThemeSettingsVisible';
|
||||||
|
|
||||||
type BrandSettings = {
|
type GlobalSettings = {
|
||||||
description: string;
|
description: string;
|
||||||
accentColor: string;
|
accentColor: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
@ -15,7 +15,7 @@ type BrandSettings = {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThemePreviewProps {
|
interface ThemePreviewProps {
|
||||||
settings: BrandSettings
|
settings: GlobalSettings
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {ColorPickerField, Heading, Hint, ImageUpload, Select, TextField, Toggle} from '@tryghost/admin-x-design-system';
|
||||||
|
import {CustomThemeSetting} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
||||||
|
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||||
|
import {humanizeSettingKey} from '@tryghost/admin-x-framework/api/settings';
|
||||||
|
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||||
|
|
||||||
|
interface ThemeSettingProps {
|
||||||
|
setting: CustomThemeSetting;
|
||||||
|
setSetting: <Setting extends CustomThemeSetting>(value: Setting['value']) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeSetting: React.FC<ThemeSettingProps> = ({setting, setSetting}) => {
|
||||||
|
const [fieldValues, setFieldValues] = useState<{ [key: string]: string | null }>({});
|
||||||
|
useEffect(() => {
|
||||||
|
const valueAsString = setting.value === null ? '' : String(setting.value);
|
||||||
|
setFieldValues(values => ({...values, [setting.key]: valueAsString}));
|
||||||
|
}, [setting]);
|
||||||
|
|
||||||
|
const handleBlur = (key: string) => {
|
||||||
|
if (fieldValues[key] !== undefined) {
|
||||||
|
setSetting(fieldValues[key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (key: string, value: string) => {
|
||||||
|
setFieldValues(values => ({...values, [key]: value}));
|
||||||
|
};
|
||||||
|
const {mutateAsync: uploadImage} = useUploadImage();
|
||||||
|
const handleError = useHandleError();
|
||||||
|
|
||||||
|
const handleImageUpload = async (file: File) => {
|
||||||
|
try {
|
||||||
|
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||||
|
setSetting(imageUrl);
|
||||||
|
} catch (e) {
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (setting.type) {
|
||||||
|
case 'text':
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
hint={setting.description}
|
||||||
|
title={humanizeSettingKey(setting.key)}
|
||||||
|
value={fieldValues[setting.key] || ''}
|
||||||
|
onBlur={() => handleBlur(setting.key)}
|
||||||
|
onChange={event => handleChange(setting.key, event.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<Toggle
|
||||||
|
checked={setting.value}
|
||||||
|
direction="rtl"
|
||||||
|
hint={setting.description}
|
||||||
|
label={humanizeSettingKey(setting.key)}
|
||||||
|
onChange={event => setSetting(event.target.checked)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'select':
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
hint={setting.description}
|
||||||
|
options={setting.options.map(option => ({label: option, value: option}))}
|
||||||
|
selectedOption={{label: setting.value, value: setting.value}}
|
||||||
|
testId={`setting-select-${setting.key}`}
|
||||||
|
title={humanizeSettingKey(setting.key)}
|
||||||
|
onSelect={option => setSetting(option?.value || null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'color':
|
||||||
|
return (
|
||||||
|
<ColorPickerField
|
||||||
|
debounceMs={200}
|
||||||
|
direction='rtl'
|
||||||
|
hint={setting.description}
|
||||||
|
title={humanizeSettingKey(setting.key)}
|
||||||
|
value={setting.value || ''}
|
||||||
|
onChange={value => setSetting(value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'image':
|
||||||
|
return <>
|
||||||
|
<Heading useLabelTag>{humanizeSettingKey(setting.key)}</Heading>
|
||||||
|
<ImageUpload
|
||||||
|
height={setting.value ? '100px' : '32px'}
|
||||||
|
id={`custom-${setting.key}`}
|
||||||
|
imageURL={setting.value || ''}
|
||||||
|
onDelete={() => setSetting(null)}
|
||||||
|
onUpload={file => handleImageUpload(file)}
|
||||||
|
>Upload image</ImageUpload>
|
||||||
|
{setting.description && <Hint>{setting.description}</Hint>}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeSetting;
|
|
@ -1,109 +1,38 @@
|
||||||
import React, {useEffect, useState} from 'react';
|
import React from 'react';
|
||||||
import {ColorPickerField, Heading, Hint, ImageUpload, Select, SettingGroupContent, TextField, Toggle} from '@tryghost/admin-x-design-system';
|
import ThemeSetting from './ThemeSetting';
|
||||||
import {CustomThemeSetting} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
import {CustomThemeSetting} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
||||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
import {Form} from '@tryghost/admin-x-design-system';
|
||||||
import {humanizeSettingKey} from '@tryghost/admin-x-framework/api/settings';
|
|
||||||
import {isCustomThemeSettingVisible} from '../../../../utils/isCustomThemeSettingsVisible';
|
import {isCustomThemeSettingVisible} from '../../../../utils/isCustomThemeSettingsVisible';
|
||||||
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
|
|
||||||
|
|
||||||
const ThemeSetting: React.FC<{
|
interface ThemeSettingsProps {
|
||||||
setting: CustomThemeSetting,
|
sections: Array<{
|
||||||
setSetting: <Setting extends CustomThemeSetting>(value: Setting['value']) => void
|
id: string;
|
||||||
}> = ({setting, setSetting}) => {
|
title: string;
|
||||||
const [fieldValues, setFieldValues] = useState<{ [key: string]: string | null }>({});
|
settings: CustomThemeSetting[];
|
||||||
useEffect(() => {
|
}>;
|
||||||
const valueAsString = setting.value === null ? '' : String(setting.value);
|
updateSetting: (setting: CustomThemeSetting) => void;
|
||||||
setFieldValues(values => ({...values, [setting.key]: valueAsString}));
|
}
|
||||||
}, [setting]);
|
|
||||||
|
|
||||||
const handleBlur = (key: string) => {
|
|
||||||
if (fieldValues[key] !== undefined) {
|
|
||||||
setSetting(fieldValues[key]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (key: string, value: string) => {
|
|
||||||
setFieldValues(values => ({...values, [key]: value}));
|
|
||||||
};
|
|
||||||
const {mutateAsync: uploadImage} = useUploadImage();
|
|
||||||
const handleError = useHandleError();
|
|
||||||
|
|
||||||
const handleImageUpload = async (file: File) => {
|
|
||||||
try {
|
|
||||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
|
||||||
setSetting(imageUrl);
|
|
||||||
} catch (e) {
|
|
||||||
handleError(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (setting.type) {
|
|
||||||
case 'text':
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
hint={setting.description}
|
|
||||||
title={humanizeSettingKey(setting.key)}
|
|
||||||
value={fieldValues[setting.key] || ''}
|
|
||||||
onBlur={() => handleBlur(setting.key)}
|
|
||||||
onChange={event => handleChange(setting.key, event.target.value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'boolean':
|
|
||||||
return (
|
|
||||||
<Toggle
|
|
||||||
checked={setting.value}
|
|
||||||
direction="rtl"
|
|
||||||
hint={setting.description}
|
|
||||||
label={humanizeSettingKey(setting.key)}
|
|
||||||
onChange={event => setSetting(event.target.checked)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'select':
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
hint={setting.description}
|
|
||||||
options={setting.options.map(option => ({label: option, value: option}))}
|
|
||||||
selectedOption={{label: setting.value, value: setting.value}}
|
|
||||||
testId={`setting-select-${setting.key}`}
|
|
||||||
title={humanizeSettingKey(setting.key)}
|
|
||||||
onSelect={option => setSetting(option?.value || null)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'color':
|
|
||||||
return (
|
|
||||||
<ColorPickerField
|
|
||||||
debounceMs={200}
|
|
||||||
direction='rtl'
|
|
||||||
hint={setting.description}
|
|
||||||
title={humanizeSettingKey(setting.key)}
|
|
||||||
value={setting.value || ''}
|
|
||||||
onChange={value => setSetting(value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'image':
|
|
||||||
return <>
|
|
||||||
<Heading useLabelTag>{humanizeSettingKey(setting.key)}</Heading>
|
|
||||||
<ImageUpload
|
|
||||||
height={setting.value ? '100px' : '32px'}
|
|
||||||
id={`custom-${setting.key}`}
|
|
||||||
imageURL={setting.value || ''}
|
|
||||||
onDelete={() => setSetting(null)}
|
|
||||||
onUpload={file => handleImageUpload(file)}
|
|
||||||
>Upload image</ImageUpload>
|
|
||||||
{setting.description && <Hint>{setting.description}</Hint>}
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ThemeSettings: React.FC<{ settings: CustomThemeSetting[], updateSetting: (setting: CustomThemeSetting) => void }> = ({settings, updateSetting}) => {
|
|
||||||
// Filter out custom theme settings that should not be visible
|
|
||||||
const settingsKeyValueObj = settings.reduce((obj, {key, value}) => ({...obj, [key]: value}), {});
|
|
||||||
const filteredSettings = settings.filter(setting => isCustomThemeSettingVisible(setting, settingsKeyValueObj));
|
|
||||||
|
|
||||||
|
const ThemeSettings: React.FC<ThemeSettingsProps> = ({sections, updateSetting}) => {
|
||||||
return (
|
return (
|
||||||
<SettingGroupContent className='mt-7'>
|
<>
|
||||||
{filteredSettings.map(setting => <ThemeSetting key={setting.key} setSetting={value => updateSetting({...setting, value} as CustomThemeSetting)} setting={setting} />)}
|
{sections.map((section) => {
|
||||||
</SettingGroupContent>
|
const filteredSettings = section.settings.filter(setting => isCustomThemeSettingVisible(setting, section.settings.reduce((obj, {key, value}) => ({...obj, [key]: value}), {}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form key={section.id} className='first-of-type:mt-6' gap='sm' margins='lg' title={section.title}>
|
||||||
|
{filteredSettings.map(setting => (
|
||||||
|
<ThemeSetting
|
||||||
|
key={setting.key}
|
||||||
|
setSetting={value => updateSetting({...setting, value} as CustomThemeSetting)}
|
||||||
|
setting={setting}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue