Added homepage/post URL selection in AdminX design settings

refs https://github.com/TryGhost/Team/issues/3354
This commit is contained in:
Jono Mingard 2023-06-12 13:46:14 +12:00
parent ba2436e834
commit 345d90bbba
8 changed files with 83 additions and 45 deletions

View File

@ -20,6 +20,7 @@ export interface PreviewModalProps {
buttonsDisabled?: boolean buttonsDisabled?: boolean
previewToolbar?: boolean; previewToolbar?: boolean;
previewToolbarURLs?: SelectOption[]; previewToolbarURLs?: SelectOption[];
selectedURL?: string;
sidebarButtons?: React.ReactNode; sidebarButtons?: React.ReactNode;
sidebarHeader?: React.ReactNode; sidebarHeader?: React.ReactNode;
sidebarPadding?: boolean; sidebarPadding?: boolean;
@ -39,8 +40,9 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
cancelLabel = 'Cancel', cancelLabel = 'Cancel',
okLabel = 'OK', okLabel = 'OK',
okColor = 'black', okColor = 'black',
previewToolbarURLs,
previewToolbar = true, previewToolbar = true,
previewToolbarURLs,
selectedURL,
buttonsDisabled, buttonsDisabled,
sidebarButtons, sidebarButtons,
sidebarHeader, sidebarHeader,
@ -69,7 +71,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
let toolbarCenter = (<></>); let toolbarCenter = (<></>);
if (previewToolbarURLs) { if (previewToolbarURLs) {
toolbarCenter = ( toolbarCenter = (
<URLSelect options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} /> <URLSelect defaultSelectedOption={selectedURL} options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} />
); );
} }

View File

@ -9,7 +9,7 @@ export interface SelectOption {
label: string; label: string;
} }
interface SelectProps { export interface SelectProps {
title?: string; title?: string;
prompt?: string; prompt?: string;
options: SelectOption[]; options: SelectOption[];

View File

@ -9,10 +9,11 @@ export type Tab = {
interface TabViewProps { interface TabViewProps {
tabs: Tab[]; tabs: Tab[];
onTabChange?: (id: string) => void;
defaultSelected?: string; defaultSelected?: string;
} }
const TabView: React.FC<TabViewProps> = ({tabs, defaultSelected}) => { const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) => {
if (tabs.length !== 0 && defaultSelected === undefined) { if (tabs.length !== 0 && defaultSelected === undefined) {
defaultSelected = tabs[0].id; defaultSelected = tabs[0].id;
} }
@ -26,6 +27,7 @@ const TabView: React.FC<TabViewProps> = ({tabs, defaultSelected}) => {
const handleTabChange = (e: React.MouseEvent<HTMLButtonElement>) => { const handleTabChange = (e: React.MouseEvent<HTMLButtonElement>) => {
const newTab = e.currentTarget.id; const newTab = e.currentTarget.id;
setSelectedTab(newTab); setSelectedTab(newTab);
onTabChange?.(newTab);
}; };
return ( return (

View File

@ -1,13 +1,8 @@
import React from 'react'; import React from 'react';
import Select, {SelectOption} from './Select'; import Select, {SelectProps} from './Select';
import clsx from 'clsx'; import clsx from 'clsx';
interface URLSelectProps { const URLSelect: React.FC<SelectProps> = (props) => {
options: SelectOption[];
onSelect: (value: string) => void;
}
const URLSelect: React.FC<URLSelectProps> = ({options, onSelect}) => {
const selectClasses = clsx( const selectClasses = clsx(
`!h-[unset] w-full appearance-none rounded-full border border-grey-100 bg-white px-3 py-1 text-sm` `!h-[unset] w-full appearance-none rounded-full border border-grey-100 bg-white px-3 py-1 text-sm`
); );
@ -20,12 +15,11 @@ const URLSelect: React.FC<URLSelectProps> = ({options, onSelect}) => {
return ( return (
<Select <Select
containerClassName={containerClasses} containerClassName={containerClasses}
options={options}
selectClassName={selectClasses} selectClassName={selectClasses}
unstyled={true} unstyled={true}
onSelect={onSelect} {...props}
/> />
); );
}; };
export default URLSelect; export default URLSelect;

View File

@ -4,10 +4,10 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useContext, useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import StickyFooter from '../../../admin-x-ds/global/StickyFooter'; import StickyFooter from '../../../admin-x-ds/global/StickyFooter';
import TabView, {Tab} from '../../../admin-x-ds/global/TabView'; import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
import ThemePreview from './designAndBranding/ThemePreivew'; import ThemePreview from './designAndBranding/ThemePreview';
import ThemeSettings from './designAndBranding/ThemeSettings'; import ThemeSettings from './designAndBranding/ThemeSettings';
import useForm from '../../../hooks/useForm'; import useForm from '../../../hooks/useForm';
import {CustomThemeSetting, Setting, SettingValue} from '../../../types/api'; import {CustomThemeSetting, Post, Setting, SettingValue, SiteData} from '../../../types/api';
import {PreviewModalContent} from '../../../admin-x-ds/global/PreviewModal'; import {PreviewModalContent} from '../../../admin-x-ds/global/PreviewModal';
import {SelectOption} from '../../../admin-x-ds/global/Select'; import {SelectOption} from '../../../admin-x-ds/global/Select';
import {ServicesContext} from '../../providers/ServiceProvider'; import {ServicesContext} from '../../providers/ServiceProvider';
@ -19,7 +19,8 @@ const Sidebar: React.FC<{
updateBrandSetting: (key: string, value: SettingValue) => void updateBrandSetting: (key: string, value: SettingValue) => void
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}> themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
updateThemeSetting: (updated: CustomThemeSetting) => void updateThemeSetting: (updated: CustomThemeSetting) => void
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting}) => { onTabChange: (id: string) => void
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting,onTabChange}) => {
const tabs: Tab[] = [ const tabs: Tab[] = [
{ {
id: 'brand', id: 'brand',
@ -36,7 +37,7 @@ const Sidebar: React.FC<{
return ( return (
<> <>
<div className='p-7'> <div className='p-7'>
<TabView tabs={tabs} /> <TabView tabs={tabs} onTabChange={onTabChange} />
</div> </div>
<StickyFooter> <StickyFooter>
<button className='flex w-full cursor-pointer flex-col px-7' type='button' onClick={() => {}}> <button className='flex w-full cursor-pointer flex-col px-7' type='button' onClick={() => {}}>
@ -48,11 +49,21 @@ const Sidebar: React.FC<{
); );
}; };
function getHomepageUrl(siteData: SiteData): string {
const url = new URL(siteData.url);
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
return `${url.origin}${subdir}`;
}
const DesignModal: React.FC = () => { const DesignModal: React.FC = () => {
const modal = useModal(); const modal = useModal();
const {api} = useContext(ServicesContext); const {api} = useContext(ServicesContext);
const {settings, siteData, saveSettings} = useContext(SettingsContext);
const [themeSettings, setThemeSettings] = useState<Array<CustomThemeSetting>>([]); const [themeSettings, setThemeSettings] = useState<Array<CustomThemeSetting>>([]);
const [latestPost, setLatestPost] = useState<Post | null>(null);
const [selectedUrl, setSelectedUrl] = useState(getHomepageUrl(siteData!));
useEffect(() => { useEffect(() => {
api.customThemeSettings.browse().then((response) => { api.customThemeSettings.browse().then((response) => {
@ -60,7 +71,11 @@ const DesignModal: React.FC = () => {
}); });
}, [api]); }, [api]);
const {settings, saveSettings} = useContext(SettingsContext); useEffect(() => {
api.latestPost.browse().then((response) => {
setLatestPost(response.posts[0]);
});
}, [api]);
const { const {
formState, formState,
@ -116,12 +131,20 @@ const DesignModal: React.FC = () => {
})); }));
const urlOptions: SelectOption[] = [ const urlOptions: SelectOption[] = [
{value: 'homepage', label: 'Homepage'}, {value: getHomepageUrl(siteData!), label: 'Homepage'},
{value: 'post', label: 'Post'} latestPost && {value: latestPost.url, label: 'Post'}
]; ].filter((option): option is SelectOption => Boolean(option));
const onSelectURL = (url: string) => { const onSelectURL = (url: string) => {
alert(url); setSelectedUrl(url);
};
const onTabChange = (id: string) => {
if (id === 'post' && latestPost) {
setSelectedUrl(latestPost.url);
} else {
setSelectedUrl(getHomepageUrl(siteData!));
}
}; };
return <PreviewModalContent return <PreviewModalContent
@ -138,14 +161,17 @@ const DesignModal: React.FC = () => {
coverImage, coverImage,
themeSettings themeSettings
}} }}
url={selectedUrl}
/> />
} }
previewToolbarURLs={urlOptions} previewToolbarURLs={urlOptions}
selectedURL={selectedUrl}
sidebar={<Sidebar sidebar={<Sidebar
brandSettings={{description, accentColor, icon, logo, coverImage}} brandSettings={{description, accentColor, icon, logo, coverImage}}
themeSettingSections={themeSettingSections} themeSettingSections={themeSettingSections}
updateBrandSetting={updateBrandSetting} updateBrandSetting={updateBrandSetting}
updateThemeSetting={updateThemeSetting} updateThemeSetting={updateThemeSetting}
onTabChange={onTabChange}
/>} />}
sidebarPadding={false} sidebarPadding={false}
testId='design-modal' testId='design-modal'

View File

@ -1,5 +1,4 @@
import React, {useEffect, useRef} from 'react'; import React, {useEffect, useRef} from 'react';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import {CustomThemeSetting} from '../../../../types/api'; import {CustomThemeSetting} from '../../../../types/api';
type BrandSettings = { type BrandSettings = {
@ -13,17 +12,7 @@ type BrandSettings = {
interface ThemePreviewProps { interface ThemePreviewProps {
settings: BrandSettings settings: BrandSettings
} url: string
function getApiUrl({siteUrl, path = ''}: {
siteUrl: string;
path?: string;
}): string {
const url = new URL(siteUrl);
const subdir = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
const fullPath = `${subdir}${path.replace(/^\//, '')}`;
return `${url.origin}${fullPath}`;
} }
function getPreviewData({ function getPreviewData({
@ -58,20 +47,16 @@ function getPreviewData({
return params.toString(); return params.toString();
} }
const ThemePreview: React.FC<ThemePreviewProps> = ({settings}) => { const ThemePreview: React.FC<ThemePreviewProps> = ({settings,url}) => {
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const {siteData} = useSettingGroup();
const apiEndpoint = getApiUrl({
siteUrl: siteData?.url || '',
path: ''
});
useEffect(() => { useEffect(() => {
if (!apiEndpoint) { if (!url) {
return; return;
} }
// Fetch theme preview HTML // Fetch theme preview HTML
fetch(apiEndpoint, { fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/html;charset=utf-8', 'Content-Type': 'text/html;charset=utf-8',
@ -111,7 +96,7 @@ const ThemePreview: React.FC<ThemePreviewProps> = ({settings}) => {
.catch(() => { .catch(() => {
// handle error in fetching data // handle error in fetching data
}); });
}, [apiEndpoint, settings]); }, [url, settings]);
return ( return (
<> <>
<iframe <iframe

View File

@ -56,6 +56,11 @@ export type SiteData = {
version: string; version: string;
}; };
export type Post = {
id: string;
url: string;
};
type CustomThemeSettingData = type CustomThemeSettingData =
{ type: 'text', value: string | null, default: string | null } | { type: 'text', value: string | null, default: string | null } |
{ type: 'color', value: string, default: string } | { type: 'color', value: string, default: string } |

View File

@ -1,4 +1,4 @@
import {CustomThemeSetting, Setting, SiteData, User, UserRole} from '../types/api'; import {CustomThemeSetting, Post, Setting, SiteData, User, UserRole} from '../types/api';
import {getGhostPaths} from './helpers'; import {getGhostPaths} from './helpers';
interface Meta { interface Meta {
@ -53,6 +53,20 @@ export interface CustomThemeSettingsResponseType {
custom_theme_settings: CustomThemeSetting[]; custom_theme_settings: CustomThemeSetting[];
} }
export interface PostsResponseType {
meta: {
pagination: {
page: number
limit: number
pages: number
total: number
next: number | null
prev: number | null
}
}
posts: Post[];
}
export interface SiteResponseType { export interface SiteResponseType {
site: SiteData; site: SiteData;
} }
@ -127,6 +141,9 @@ interface API {
customThemeSettings: { customThemeSettings: {
browse: () => Promise<CustomThemeSettingsResponseType> browse: () => Promise<CustomThemeSettingsResponseType>
edit: (newSettings: CustomThemeSetting[]) => Promise<CustomThemeSettingsResponseType> edit: (newSettings: CustomThemeSetting[]) => Promise<CustomThemeSettingsResponseType>
};
latestPost: {
browse: () => Promise<PostsResponseType>
} }
} }
@ -325,6 +342,13 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
const data: CustomThemeSettingsResponseType = await response.json(); const data: CustomThemeSettingsResponseType = await response.json();
return data; return data;
} }
},
latestPost: {
browse: async () => {
const response = await fetcher('/posts/?filter=status%3Apublished&order=published_at%20DESC&limit=1&fields=id,url');
const data: PostsResponseType = await response.json();
return data;
}
} }
}; };