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
previewToolbar?: boolean;
previewToolbarURLs?: SelectOption[];
selectedURL?: string;
sidebarButtons?: React.ReactNode;
sidebarHeader?: React.ReactNode;
sidebarPadding?: boolean;
@ -39,8 +40,9 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
cancelLabel = 'Cancel',
okLabel = 'OK',
okColor = 'black',
previewToolbarURLs,
previewToolbar = true,
previewToolbarURLs,
selectedURL,
buttonsDisabled,
sidebarButtons,
sidebarHeader,
@ -69,7 +71,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
let toolbarCenter = (<></>);
if (previewToolbarURLs) {
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;
}
interface SelectProps {
export interface SelectProps {
title?: string;
prompt?: string;
options: SelectOption[];

View File

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

View File

@ -1,13 +1,8 @@
import React from 'react';
import Select, {SelectOption} from './Select';
import Select, {SelectProps} from './Select';
import clsx from 'clsx';
interface URLSelectProps {
options: SelectOption[];
onSelect: (value: string) => void;
}
const URLSelect: React.FC<URLSelectProps> = ({options, onSelect}) => {
const URLSelect: React.FC<SelectProps> = (props) => {
const selectClasses = clsx(
`!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 (
<Select
containerClassName={containerClasses}
options={options}
selectClassName={selectClasses}
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 StickyFooter from '../../../admin-x-ds/global/StickyFooter';
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 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 {SelectOption} from '../../../admin-x-ds/global/Select';
import {ServicesContext} from '../../providers/ServiceProvider';
@ -19,7 +19,8 @@ const Sidebar: React.FC<{
updateBrandSetting: (key: string, value: SettingValue) => void
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
updateThemeSetting: (updated: CustomThemeSetting) => void
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting}) => {
onTabChange: (id: string) => void
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting,onTabChange}) => {
const tabs: Tab[] = [
{
id: 'brand',
@ -36,7 +37,7 @@ const Sidebar: React.FC<{
return (
<>
<div className='p-7'>
<TabView tabs={tabs} />
<TabView tabs={tabs} onTabChange={onTabChange} />
</div>
<StickyFooter>
<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 modal = useModal();
const {api} = useContext(ServicesContext);
const {settings, siteData, saveSettings} = useContext(SettingsContext);
const [themeSettings, setThemeSettings] = useState<Array<CustomThemeSetting>>([]);
const [latestPost, setLatestPost] = useState<Post | null>(null);
const [selectedUrl, setSelectedUrl] = useState(getHomepageUrl(siteData!));
useEffect(() => {
api.customThemeSettings.browse().then((response) => {
@ -60,7 +71,11 @@ const DesignModal: React.FC = () => {
});
}, [api]);
const {settings, saveSettings} = useContext(SettingsContext);
useEffect(() => {
api.latestPost.browse().then((response) => {
setLatestPost(response.posts[0]);
});
}, [api]);
const {
formState,
@ -116,12 +131,20 @@ const DesignModal: React.FC = () => {
}));
const urlOptions: SelectOption[] = [
{value: 'homepage', label: 'Homepage'},
{value: 'post', label: 'Post'}
];
{value: getHomepageUrl(siteData!), label: 'Homepage'},
latestPost && {value: latestPost.url, label: 'Post'}
].filter((option): option is SelectOption => Boolean(option));
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
@ -138,14 +161,17 @@ const DesignModal: React.FC = () => {
coverImage,
themeSettings
}}
url={selectedUrl}
/>
}
previewToolbarURLs={urlOptions}
selectedURL={selectedUrl}
sidebar={<Sidebar
brandSettings={{description, accentColor, icon, logo, coverImage}}
themeSettingSections={themeSettingSections}
updateBrandSetting={updateBrandSetting}
updateThemeSetting={updateThemeSetting}
onTabChange={onTabChange}
/>}
sidebarPadding={false}
testId='design-modal'

View File

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

View File

@ -56,6 +56,11 @@ export type SiteData = {
version: string;
};
export type Post = {
id: string;
url: string;
};
type CustomThemeSettingData =
{ type: 'text', value: string | null, default: string | null } |
{ 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';
interface Meta {
@ -53,6 +53,20 @@ export interface CustomThemeSettingsResponseType {
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 {
site: SiteData;
}
@ -127,6 +141,9 @@ interface API {
customThemeSettings: {
browse: () => 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();
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;
}
}
};