mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-30 21:40:39 +03:00
Added homepage/post URL selection in AdminX design settings
refs https://github.com/TryGhost/Team/issues/3354
This commit is contained in:
parent
ba2436e834
commit
345d90bbba
@ -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 : () => {}} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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[];
|
||||||
|
@ -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 (
|
||||||
|
@ -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,10 +15,9 @@ 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
@ -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 } |
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user