mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Updated design settings to be editable and show theme settings
refs https://github.com/TryGhost/Team/issues/3354
This commit is contained in:
parent
567422174d
commit
446841ce9c
@ -14,12 +14,13 @@ export interface PreviewModalProps {
|
||||
okColor?: string;
|
||||
onCancel?: () => void;
|
||||
onOk?: () => void;
|
||||
buttonsDisabled?: boolean
|
||||
customButtons?: React.ReactNode;
|
||||
customHeader?: React.ReactNode;
|
||||
sidebarPadding?: boolean;
|
||||
}
|
||||
|
||||
const PreviewModal: React.FC<PreviewModalProps> = ({
|
||||
export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
title,
|
||||
sidebar,
|
||||
preview,
|
||||
@ -28,6 +29,7 @@ const PreviewModal: React.FC<PreviewModalProps> = ({
|
||||
okColor = 'black',
|
||||
onCancel,
|
||||
onOk,
|
||||
buttonsDisabled,
|
||||
customButtons,
|
||||
customHeader,
|
||||
sidebarPadding = true
|
||||
@ -41,7 +43,8 @@ const PreviewModal: React.FC<PreviewModalProps> = ({
|
||||
label: cancelLabel,
|
||||
onClick: (onCancel ? onCancel : () => {
|
||||
modal.remove();
|
||||
})
|
||||
}),
|
||||
disabled: buttonsDisabled
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
@ -49,7 +52,8 @@ const PreviewModal: React.FC<PreviewModalProps> = ({
|
||||
label: okLabel,
|
||||
color: okColor,
|
||||
className: 'min-w-[80px]',
|
||||
onClick: onOk
|
||||
onClick: onOk,
|
||||
disabled: buttonsDisabled
|
||||
});
|
||||
}
|
||||
|
||||
@ -82,4 +86,4 @@ const PreviewModal: React.FC<PreviewModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default NiceModal.create(PreviewModal);
|
||||
export default NiceModal.create(PreviewModalContent);
|
||||
|
@ -3,13 +3,11 @@ import Hint from './Hint';
|
||||
import React, {useId} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type TextFieldType = 'text' | 'number' | 'email' | 'password' | 'file' | 'date' | 'time' | 'search';
|
||||
|
||||
interface TextFieldProps {
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
title?: string;
|
||||
hideTitle?: boolean;
|
||||
type?: TextFieldType;
|
||||
type?: React.InputHTMLAttributes<HTMLInputElement>['type'];
|
||||
value?: string;
|
||||
error?: boolean;
|
||||
placeholder?: string;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import React, {createContext} from 'react';
|
||||
import setupGhostApi from '../../utils/api';
|
||||
import {createContext} from 'react';
|
||||
|
||||
export interface FileService {
|
||||
uploadImage: (file: File) => Promise<string>;
|
||||
@ -21,6 +20,7 @@ const ServicesContext = createContext<ServicesContextProps>({
|
||||
});
|
||||
|
||||
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion}) => {
|
||||
// TODO: Will this re-render? (if so, will it make duplicate requests because the api object is different?)
|
||||
const apiService = setupGhostApi({ghostVersion});
|
||||
const fileService = {
|
||||
uploadImage: async (file: File): Promise<string> => {
|
||||
@ -38,4 +38,4 @@ const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersi
|
||||
);
|
||||
};
|
||||
|
||||
export {ServicesContext, ServicesProvider};
|
||||
export {ServicesContext, ServicesProvider};
|
||||
|
@ -1,37 +1,33 @@
|
||||
import BrandSettings from './designAndBranding/BrandSettings';
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import PreviewModal from '../../../admin-x-ds/global/PreviewModal';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import BrandSettings, {BrandSettingValues} from './designAndBranding/BrandSettings';
|
||||
import ConfirmationModal from '../../../admin-x-ds/global/ConfirmationModal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import StickyFooter from '../../../admin-x-ds/global/StickyFooter';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import ThemePreview from './designAndBranding/ThemePreivew';
|
||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
import {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {CustomThemeSetting, SettingValue} from '../../../types/api';
|
||||
import {PreviewModalContent} from '../../../admin-x-ds/global/PreviewModal';
|
||||
import {ServicesContext} from '../../providers/ServiceProvider';
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const Sidebar: React.FC<{
|
||||
brandSettings: BrandSettingValues
|
||||
updateBrandSetting: (key: string, value: SettingValue) => void
|
||||
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
|
||||
updateThemeSetting: (updated: CustomThemeSetting) => void
|
||||
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting}) => {
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
id: 'brand',
|
||||
title: 'Brand',
|
||||
contents: <BrandSettings />
|
||||
contents: <BrandSettings updateSetting={updateBrandSetting} values={brandSettings} />
|
||||
},
|
||||
{
|
||||
id: 'site-wide',
|
||||
title: 'Site wide',
|
||||
contents: <ThemeSettings settingSection='site-wide' />
|
||||
},
|
||||
{
|
||||
id: 'homepage',
|
||||
title: 'Homepage',
|
||||
contents: <ThemeSettings settingSection='homepage' />
|
||||
},
|
||||
{
|
||||
id: 'post',
|
||||
title: 'Post',
|
||||
contents: <ThemeSettings settingSection='post' />
|
||||
}
|
||||
...themeSettingSections.map(({id, title, settings}) => ({
|
||||
id,
|
||||
title,
|
||||
contents: <ThemeSettings settings={settings} updateSetting={updateThemeSetting} />
|
||||
}))
|
||||
];
|
||||
|
||||
return (
|
||||
@ -50,24 +46,93 @@ const Sidebar: React.FC = () => {
|
||||
};
|
||||
|
||||
const DesignModal: React.FC = () => {
|
||||
const openPreviewModal = () => {
|
||||
NiceModal.show(PreviewModal, {
|
||||
title: 'Design',
|
||||
okLabel: 'Save',
|
||||
preview: <ThemePreview />,
|
||||
sidebar: <Sidebar />,
|
||||
sidebarPadding: false
|
||||
const modal = useModal();
|
||||
|
||||
const {api} = useContext(ServicesContext);
|
||||
const [themeSettings, setThemeSettings] = useState<Array<CustomThemeSetting & { dirty?: boolean }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
api.customThemeSettings.browse().then((response) => {
|
||||
setThemeSettings(response.custom_theme_settings);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const {
|
||||
saveState,
|
||||
handleSave,
|
||||
updateSetting,
|
||||
getSettingValues,
|
||||
dirty
|
||||
} = useSettingGroup({
|
||||
onSave: async () => {
|
||||
if (themeSettings.some(setting => setting.dirty)) {
|
||||
const response = await api.customThemeSettings.edit(themeSettings);
|
||||
setThemeSettings(response.custom_theme_settings);
|
||||
}
|
||||
}
|
||||
});
|
||||
const [description, accentColor, icon, logo, coverImage] = getSettingValues(['description', 'accent_color', 'icon', 'logo', 'cover_image']) as string[];
|
||||
|
||||
const themeSettingGroups = themeSettings.reduce((groups, setting) => {
|
||||
const group = (setting.group === 'homepage' || setting.group === 'post') ? setting.group : 'site-wide';
|
||||
|
||||
return {
|
||||
...groups,
|
||||
[group]: (groups[group] || []).concat(setting)
|
||||
};
|
||||
}, {} as {[key: string]: CustomThemeSetting[] | undefined});
|
||||
|
||||
const themeSettingSections = Object.entries(themeSettingGroups).map(([id, settings]) => ({
|
||||
id,
|
||||
settings: settings || [],
|
||||
title: id === 'site-wide' ? 'Site wide' : (id === 'homepage' ? 'Homepage' : 'Post')
|
||||
}));
|
||||
|
||||
const updateThemeSetting = (updated: CustomThemeSetting) => {
|
||||
setThemeSettings(themeSettings.map(setting => (
|
||||
setting.key === updated.key ? {...updated, dirty: true} : setting
|
||||
)));
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
|
||||
description="Customize your site and manage themes"
|
||||
navid='branding-and-design'
|
||||
title="Branding and design"
|
||||
/>
|
||||
);
|
||||
return <PreviewModalContent
|
||||
buttonsDisabled={saveState === 'saving'}
|
||||
okLabel='Save'
|
||||
preview={<ThemePreview />}
|
||||
sidebar={<Sidebar
|
||||
brandSettings={{description, accentColor, icon, logo, coverImage}}
|
||||
themeSettingSections={themeSettingSections}
|
||||
updateBrandSetting={updateSetting}
|
||||
updateThemeSetting={updateThemeSetting}
|
||||
/>}
|
||||
sidebarPadding={false}
|
||||
title='Design'
|
||||
onCancel={() => {
|
||||
if (dirty || themeSettings.some(setting => setting.dirty)) {
|
||||
NiceModal.show(ConfirmationModal, {
|
||||
title: 'Are you sure you want to leave this page?',
|
||||
prompt: (
|
||||
<>
|
||||
<p>Hey there! It looks like you didn‘t save the changes you made.</p>
|
||||
<p>Save before you go!</p>
|
||||
</>
|
||||
),
|
||||
okLabel: 'Leave',
|
||||
okColor: 'red',
|
||||
onOk: (confirmModal) => {
|
||||
confirmModal?.remove();
|
||||
modal.remove();
|
||||
},
|
||||
cancelLabel: 'Stay'
|
||||
});
|
||||
} else {
|
||||
modal.remove();
|
||||
}
|
||||
}}
|
||||
onOk={async () => {
|
||||
await handleSave();
|
||||
modal.remove();
|
||||
}}
|
||||
/>;
|
||||
};
|
||||
|
||||
export default DesignModal;
|
||||
export default NiceModal.create(DesignModal);
|
||||
|
@ -0,0 +1,22 @@
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import DesignModal from './DesignModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
|
||||
const DesignSetting: React.FC = () => {
|
||||
const openPreviewModal = () => {
|
||||
NiceModal.show(DesignModal);
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Customize' link onClick={openPreviewModal}/>}
|
||||
description="Customize your site and manage themes"
|
||||
navid='branding-and-design'
|
||||
title="Branding and design"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesignSetting;
|
@ -1,4 +1,4 @@
|
||||
import DesignModal from './DesignModal';
|
||||
import DesignSetting from './DesignSetting';
|
||||
import React from 'react';
|
||||
import SettingSection from '../../../admin-x-ds/settings/SettingSection';
|
||||
|
||||
@ -6,10 +6,10 @@ const SiteSettings: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<SettingSection title="Site">
|
||||
<DesignModal />
|
||||
<DesignSetting />
|
||||
</SettingSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteSettings;
|
||||
export default SiteSettings;
|
||||
|
@ -1,17 +1,22 @@
|
||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import Hint from '../../../../admin-x-ds/global/Hint';
|
||||
import ImageUpload from '../../../../admin-x-ds/global/ImageUpload';
|
||||
import React from 'react';
|
||||
import React, {useContext} from 'react';
|
||||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../../admin-x-ds/global/TextField';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
import {ServicesContext} from '../../../providers/ServiceProvider';
|
||||
import {SettingValue} from '../../../../types/api';
|
||||
|
||||
const BrandSettings: React.FC = () => {
|
||||
const {
|
||||
getSettingValues
|
||||
} = useSettingGroup();
|
||||
export interface BrandSettingValues {
|
||||
description: string
|
||||
accentColor: string
|
||||
icon: string | null
|
||||
logo: string | null
|
||||
coverImage: string | null
|
||||
}
|
||||
|
||||
const [description, accentColor] = getSettingValues(['description', 'accent_color']) as string[];
|
||||
const BrandSettings: React.FC<{ values: BrandSettingValues, updateSetting: (key: string, value: SettingValue) => void }> = ({values,updateSetting}) => {
|
||||
const {fileService} = useContext(ServicesContext);
|
||||
|
||||
return (
|
||||
<div className='mt-7'>
|
||||
@ -21,7 +26,8 @@ const BrandSettings: React.FC = () => {
|
||||
clearBg={true}
|
||||
hint='Used in your theme, meta data and search results'
|
||||
title='Site description'
|
||||
value={description}
|
||||
value={values.description}
|
||||
onChange={event => updateSetting('description', event.target.value)}
|
||||
/>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<Heading level={6}>Accent color</Heading>
|
||||
@ -32,7 +38,9 @@ const BrandSettings: React.FC = () => {
|
||||
className='text-right'
|
||||
clearBg={true}
|
||||
maxLength={7}
|
||||
value={accentColor}
|
||||
type='color'
|
||||
value={values.accentColor}
|
||||
onChange={event => updateSetting('accent_color', event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,8 +52,10 @@ const BrandSettings: React.FC = () => {
|
||||
height='36px'
|
||||
id='logo'
|
||||
width='150px'
|
||||
onDelete={() => {}}
|
||||
onUpload={() => {}}
|
||||
onDelete={() => updateSetting('icon', null)}
|
||||
onUpload={async (file) => {
|
||||
updateSetting('icon', await fileService!.uploadImage(file));
|
||||
}}
|
||||
>
|
||||
Upload icon
|
||||
</ImageUpload>
|
||||
@ -56,8 +66,10 @@ const BrandSettings: React.FC = () => {
|
||||
<ImageUpload
|
||||
height='80px'
|
||||
id='logo'
|
||||
onDelete={() => {}}
|
||||
onUpload={() => {}}
|
||||
onDelete={() => updateSetting('logo', null)}
|
||||
onUpload={async (file) => {
|
||||
updateSetting('logo', await fileService!.uploadImage(file));
|
||||
}}
|
||||
>
|
||||
Upload logo
|
||||
</ImageUpload>
|
||||
@ -67,8 +79,10 @@ const BrandSettings: React.FC = () => {
|
||||
<ImageUpload
|
||||
height='140px'
|
||||
id='cover'
|
||||
onDelete={() => {}}
|
||||
onUpload={() => {}}
|
||||
onDelete={() => updateSetting('cover_image', null)}
|
||||
onUpload={async (file) => {
|
||||
updateSetting('cover_image', await fileService!.uploadImage(file));
|
||||
}}
|
||||
>
|
||||
Upload cover
|
||||
</ImageUpload>
|
||||
@ -78,4 +92,4 @@ const BrandSettings: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BrandSettings;
|
||||
export default BrandSettings;
|
||||
|
@ -1,17 +1,81 @@
|
||||
import React from 'react';
|
||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import ImageUpload from '../../../../admin-x-ds/global/ImageUpload';
|
||||
import React, {useContext} from 'react';
|
||||
import Select from '../../../../admin-x-ds/global/Select';
|
||||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../../admin-x-ds/global/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/Toggle';
|
||||
import {CustomThemeSetting} from '../../../../types/api';
|
||||
import {ServicesContext} from '../../../providers/ServiceProvider';
|
||||
import {humanizeSettingKey} from '../../../../utils/helpers';
|
||||
|
||||
type ThemeSettingSection = 'site-wide' | 'homepage' | 'post';
|
||||
const ThemeSetting: React.FC<{
|
||||
setting: CustomThemeSetting,
|
||||
setSetting: <Setting extends CustomThemeSetting>(value: Setting['value']) => void
|
||||
}> = ({setting, setSetting}) => {
|
||||
const {fileService} = useContext(ServicesContext);
|
||||
|
||||
interface ThemeSettingsProps {
|
||||
settingSection: ThemeSettingSection;
|
||||
}
|
||||
const handleImageUpload = async (file: File) => {
|
||||
const imageUrl = await fileService!.uploadImage(file);
|
||||
setSetting(imageUrl);
|
||||
};
|
||||
|
||||
const ThemeSettings: React.FC<ThemeSettingsProps> = ({settingSection}) => {
|
||||
switch (setting.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<TextField
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
value={setting.value || ''}
|
||||
onChange={event => setSetting(event.target.value)}
|
||||
/>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<Toggle
|
||||
direction="rtl"
|
||||
id={`theme-setting-${setting.key}`}
|
||||
label={humanizeSettingKey(setting.key)}
|
||||
onChange={event => setSetting(event.target.checked)}
|
||||
/>
|
||||
);
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
defaultSelectedOption={setting.value}
|
||||
options={setting.options.map(option => ({label: option, value: option}))}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
onSelect={value => setSetting(value)}
|
||||
/>
|
||||
);
|
||||
case 'color':
|
||||
return (
|
||||
<TextField
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
type='color'
|
||||
value={setting.value || ''}
|
||||
onChange={event => setSetting(event.target.value)}
|
||||
/>
|
||||
);
|
||||
case 'image':
|
||||
return <>
|
||||
<Heading useLabelTag>{humanizeSettingKey(setting.key)}</Heading>
|
||||
<ImageUpload
|
||||
height={setting.value ? '100px' : '32px'}
|
||||
id='cover-image'
|
||||
imageURL={setting.value || ''}
|
||||
onDelete={() => setSetting(null)}
|
||||
onUpload={file => handleImageUpload(file)}
|
||||
>Upload image</ImageUpload>
|
||||
</>;
|
||||
}
|
||||
};
|
||||
|
||||
const ThemeSettings: React.FC<{ settings: CustomThemeSetting[], updateSetting: (setting: CustomThemeSetting) => void }> = ({settings, updateSetting}) => {
|
||||
return (
|
||||
<>
|
||||
{settingSection}
|
||||
</>
|
||||
<SettingGroupContent className='mt-7'>
|
||||
{settings.map(setting => <ThemeSetting key={setting.key} setSetting={(value: any) => updateSetting({...setting, value})} setting={setting} />)}
|
||||
</SettingGroupContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSettings;
|
||||
export default ThemeSettings;
|
||||
|
@ -12,11 +12,12 @@ export interface SettingGroupHook {
|
||||
saveState: SaveState;
|
||||
siteData: SiteData | null;
|
||||
focusRef: React.RefObject<HTMLInputElement>;
|
||||
handleSave: () => void;
|
||||
handleSave: () => Promise<void>;
|
||||
handleCancel: () => void;
|
||||
updateSetting: (key: string, value: SettingValue) => void;
|
||||
getSettingValues: (keys: string[]) => (SettingValue|undefined)[];
|
||||
handleStateChange: (newState: TSettingGroupStates) => void;
|
||||
dirty: boolean
|
||||
}
|
||||
|
||||
type UpdateAction = {
|
||||
@ -54,7 +55,7 @@ function settingsReducer(state: Setting[], action: ActionType) {
|
||||
}
|
||||
}
|
||||
|
||||
const useSettingGroup = (): SettingGroupHook => {
|
||||
const useSettingGroup = ({onSave}: { onSave?: () => void | Promise<void> } = {}): SettingGroupHook => {
|
||||
// create a ref to focus the input field
|
||||
const focusRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@ -96,20 +97,29 @@ const useSettingGroup = (): SettingGroupHook => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [settings]);
|
||||
|
||||
// function to save the changed settings via API
|
||||
const handleSave = async () => {
|
||||
const changedSettings = localSettings?.filter(setting => setting.dirty)
|
||||
const changedSettings = () => {
|
||||
return localSettings?.filter(setting => setting.dirty)
|
||||
?.map((setting) => {
|
||||
return {
|
||||
key: setting.key,
|
||||
value: setting.value
|
||||
};
|
||||
});
|
||||
if (!changedSettings?.length) {
|
||||
};
|
||||
|
||||
// function to save the changed settings via API
|
||||
const handleSave = async () => {
|
||||
if (!changedSettings()?.length && !onSave) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSaveState('saving');
|
||||
await saveSettings?.(changedSettings);
|
||||
if (changedSettings()?.length) {
|
||||
await saveSettings?.(changedSettings());
|
||||
}
|
||||
if (onSave) {
|
||||
await onSave();
|
||||
}
|
||||
setSaveState('saved');
|
||||
setCurrentState('view');
|
||||
};
|
||||
@ -156,7 +166,11 @@ const useSettingGroup = (): SettingGroupHook => {
|
||||
handleCancel,
|
||||
updateSetting,
|
||||
getSettingValues,
|
||||
handleStateChange
|
||||
handleStateChange,
|
||||
|
||||
get dirty() {
|
||||
return !!changedSettings()?.length;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -54,4 +54,23 @@ export type SiteData = {
|
||||
url: string;
|
||||
locale: string;
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CustomThemeSettingData =
|
||||
{ type: 'text', value: string | null, default: string | null } |
|
||||
{ type: 'color', value: string, default: string } |
|
||||
{ type: 'image', value: string | null } |
|
||||
{ type: 'boolean', value: boolean, default: boolean } |
|
||||
{
|
||||
type: 'select',
|
||||
value: string
|
||||
default: string
|
||||
options: string[]
|
||||
};
|
||||
|
||||
export type CustomThemeSetting = CustomThemeSettingData & {
|
||||
id: string
|
||||
key: string
|
||||
// homepage and post are the only two groups we handle, but technically theme authors can put other things in package.json
|
||||
group?: 'homepage' | 'post' | string
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Setting, SiteData, User, UserRole} from '../types/api';
|
||||
import {CustomThemeSetting, Setting, SiteData, User, UserRole} from '../types/api';
|
||||
import {getGhostPaths} from './helpers';
|
||||
|
||||
interface Meta {
|
||||
@ -49,6 +49,10 @@ export interface InvitesResponseType {
|
||||
invites: UserInvite[];
|
||||
}
|
||||
|
||||
export interface CustomThemeSettingsResponseType {
|
||||
custom_theme_settings: CustomThemeSetting[];
|
||||
}
|
||||
|
||||
export interface SiteResponseType {
|
||||
site: SiteData;
|
||||
}
|
||||
@ -119,6 +123,10 @@ interface API {
|
||||
token?: string;
|
||||
}) => Promise<InvitesResponseType>;
|
||||
delete: (inviteId: string) => Promise<void>;
|
||||
};
|
||||
customThemeSettings: {
|
||||
browse: () => Promise<CustomThemeSettingsResponseType>
|
||||
edit: (newSettings: CustomThemeSetting[]) => Promise<CustomThemeSettingsResponseType>
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,7 +137,7 @@ interface GhostApiOptions {
|
||||
function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
||||
const {apiRoot} = getGhostPaths();
|
||||
|
||||
function fetcher(url: string, options: RequestOptions) {
|
||||
function fetcher(url: string, options: RequestOptions = {}) {
|
||||
const endpoint = `${apiRoot}${url}`;
|
||||
// By default, we set the Content-Type header to application/json
|
||||
const defaultHeaders = {
|
||||
@ -300,6 +308,23 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
customThemeSettings: {
|
||||
browse: async () => {
|
||||
const response = await fetcher('/custom_theme_settings/');
|
||||
|
||||
const data: CustomThemeSettingsResponseType = await response.json();
|
||||
return data;
|
||||
},
|
||||
edit: async (newSettings) => {
|
||||
const response = await fetcher('/custom_theme_settings/', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({custom_theme_settings: newSettings})
|
||||
});
|
||||
|
||||
const data: CustomThemeSettingsResponseType = await response.json();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -59,6 +59,15 @@ export function generateAvatarColor(name: string) {
|
||||
return 'hsl(' + h + ', ' + s + '%, ' + l + '%)';
|
||||
}
|
||||
|
||||
export function humanizeSettingKey(key: string) {
|
||||
const allCaps = ['API', 'CTA', 'RSS'];
|
||||
|
||||
return key
|
||||
.replace(/^[a-z]/, char => char.toUpperCase())
|
||||
.replace(/_/g, ' ')
|
||||
.replace(new RegExp(`\\b(${allCaps.join('|')})\\b`, 'ig'), match => match.toUpperCase());
|
||||
}
|
||||
|
||||
export function isOwnerUser(user: User) {
|
||||
return user.roles.some(role => role.name === 'Owner');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user