mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-27 21:03:29 +03:00
Added basic routing for adminX settings
refs https://github.com/TryGhost/Product/issues/3349 - adds basic routing in adminX settings based on hash url path - adds new routing provider that manages hash routing across adminX - updates sidebar navigation to update route directly - updates theme/design settings to update route for opening edit modals
This commit is contained in:
parent
56fcba1d27
commit
79a7f8c408
@ -2,6 +2,7 @@ import Button from './admin-x-ds/global/Button';
|
||||
import DataProvider from './components/providers/DataProvider';
|
||||
import Heading from './admin-x-ds/global/Heading';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import RoutingProvider from './components/providers/RoutingProvider';
|
||||
import Settings from './components/Settings';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState';
|
||||
@ -19,33 +20,35 @@ function App({ghostVersion, officialThemes, setDirty}: AppProps) {
|
||||
return (
|
||||
<ServicesProvider ghostVersion={ghostVersion} officialThemes={officialThemes}>
|
||||
<DataProvider>
|
||||
<GlobalDirtyStateProvider setDirty={setDirty}>
|
||||
<div className="admin-x-settings">
|
||||
<Toaster />
|
||||
<NiceModal.Provider>
|
||||
<div className='fixed left-6 top-4'>
|
||||
<Button label='← Done' link={true} onClick={() => window.history.back()} />
|
||||
</div>
|
||||
<RoutingProvider>
|
||||
<GlobalDirtyStateProvider setDirty={setDirty}>
|
||||
<div className="admin-x-settings">
|
||||
<Toaster />
|
||||
<NiceModal.Provider>
|
||||
<div className='fixed left-6 top-4'>
|
||||
<Button label='← Done' link={true} onClick={() => window.history.back()} />
|
||||
</div>
|
||||
|
||||
{/* Main container */}
|
||||
<div className="mx-auto flex max-w-[1080px] flex-col px-[5vmin] py-[12vmin] md:flex-row md:items-start md:gap-x-10 md:py-[8vmin]">
|
||||
{/* Main container */}
|
||||
<div className="mx-auto flex max-w-[1080px] flex-col px-[5vmin] py-[12vmin] md:flex-row md:items-start md:gap-x-10 md:py-[8vmin]">
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="relative min-w-[260px] grow-0 md:fixed md:top-[8vmin] md:basis-[260px]">
|
||||
<div className='h-[84px]'>
|
||||
<Heading>Settings</Heading>
|
||||
{/* Sidebar */}
|
||||
<div className="relative min-w-[260px] grow-0 md:fixed md:top-[8vmin] md:basis-[260px]">
|
||||
<div className='h-[84px]'>
|
||||
<Heading>Settings</Heading>
|
||||
</div>
|
||||
<div className="relative mt-[-32px] w-[260px] overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:block after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-['']">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-[-32px] w-[260px] overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:block after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-['']">
|
||||
<Sidebar />
|
||||
<div className="flex-auto pt-[3vmin] md:ml-[300px] md:pt-[85px]">
|
||||
<Settings />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-auto pt-[3vmin] md:ml-[300px] md:pt-[85px]">
|
||||
<Settings />
|
||||
</div>
|
||||
</div>
|
||||
</NiceModal.Provider>
|
||||
</div>
|
||||
</GlobalDirtyStateProvider>
|
||||
</NiceModal.Provider>
|
||||
</div>
|
||||
</GlobalDirtyStateProvider>
|
||||
</RoutingProvider>
|
||||
</DataProvider>
|
||||
</ServicesProvider>
|
||||
);
|
||||
|
@ -3,16 +3,15 @@ import React from 'react';
|
||||
import SettingNavItem from '../admin-x-ds/settings/SettingNavItem';
|
||||
import SettingNavSection from '../admin-x-ds/settings/SettingNavSection';
|
||||
import TextField from '../admin-x-ds/global/form/TextField';
|
||||
import useRouting from '../hooks/useRouting';
|
||||
import {useSearch} from './providers/ServiceProvider';
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const {filter, setFilter} = useSearch();
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const handleSectionClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const element = document.getElementById(e.currentTarget.name);
|
||||
if (element) {
|
||||
element.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
updateRoute(e.currentTarget.name);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -0,0 +1,106 @@
|
||||
import ChangeThemeModal from '../settings/site/ThemeModal';
|
||||
import DesignModal from '../settings/site/DesignModal';
|
||||
import InviteUserModal from '../settings/general/InviteUserModal';
|
||||
import NavigationModal from '../settings/site/NavigationModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
|
||||
import {SettingsContext} from './SettingsProvider';
|
||||
|
||||
type RoutingContextProps = {
|
||||
route: string;
|
||||
updateRoute: (newPath: string) => void;
|
||||
};
|
||||
|
||||
export const RouteContext = createContext<RoutingContextProps>({
|
||||
route: '',
|
||||
updateRoute: () => {}
|
||||
});
|
||||
|
||||
function getHashPath(urlPath: string | undefined) {
|
||||
if (!urlPath) {
|
||||
return null;
|
||||
}
|
||||
const regex = /\/settings-x\/(.*)/;
|
||||
const match = urlPath?.match(regex);
|
||||
|
||||
if (match) {
|
||||
const afterSettingsX = match[1];
|
||||
return afterSettingsX;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleNavigation() {
|
||||
// Get the hash from the URL
|
||||
let hash = window.location.hash;
|
||||
|
||||
// Remove the leading '#' character from the hash
|
||||
hash = hash.substring(1);
|
||||
|
||||
// Get the path name from the hash
|
||||
const pathName = getHashPath(hash);
|
||||
|
||||
if (pathName) {
|
||||
if (pathName === 'themes/manage') {
|
||||
NiceModal.show(ChangeThemeModal);
|
||||
} else if (pathName === 'branding-and-design/edit') {
|
||||
NiceModal.show(DesignModal);
|
||||
} else if (pathName === 'navigation/edit') {
|
||||
NiceModal.show(NavigationModal);
|
||||
} else if (pathName === 'users/invite') {
|
||||
NiceModal.show(InviteUserModal);
|
||||
}
|
||||
const element = document.getElementById(pathName);
|
||||
if (element) {
|
||||
element.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
return pathName;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
type RouteProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const RoutingProvider: React.FC<RouteProviderProps> = ({children}) => {
|
||||
const [route, setRoute] = useState<string>('');
|
||||
|
||||
const {settingsLoaded} = useContext(SettingsContext) || {};
|
||||
|
||||
const updateRoute = useCallback((newPath: string) => {
|
||||
if (newPath) {
|
||||
window.location.hash = `/settings-x/${newPath}`;
|
||||
} else {
|
||||
window.location.hash = `/settings-x`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
const matchedRoute = handleNavigation();
|
||||
setRoute(matchedRoute);
|
||||
};
|
||||
if (settingsLoaded) {
|
||||
const matchedRoute = handleNavigation();
|
||||
setRoute(matchedRoute);
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
};
|
||||
}, [settingsLoaded]);
|
||||
|
||||
return (
|
||||
<RouteContext.Provider value={{
|
||||
updateRoute,
|
||||
route
|
||||
}}>
|
||||
{children}
|
||||
</RouteContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoutingProvider;
|
@ -8,6 +8,7 @@ interface SettingsContextProps {
|
||||
saveSettings: (updatedSettings: Setting[]) => Promise<Setting[]>;
|
||||
siteData: SiteData | null;
|
||||
config: Config | null;
|
||||
settingsLoaded: boolean;
|
||||
}
|
||||
|
||||
interface SettingsProviderProps {
|
||||
@ -18,6 +19,7 @@ const SettingsContext = createContext<SettingsContextProps>({
|
||||
settings: null,
|
||||
siteData: null,
|
||||
config: null,
|
||||
settingsLoaded: false,
|
||||
saveSettings: async () => []
|
||||
});
|
||||
|
||||
@ -84,6 +86,7 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
||||
const [settings, setSettings] = useState<Setting[] | null> (null);
|
||||
const [siteData, setSiteData] = useState<SiteData | null> (null);
|
||||
const [config, setConfig] = useState<Config | null> (null);
|
||||
const [settingsLoaded, setSettingsLoaded] = useState<boolean> (false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSettings = async (): Promise<void> => {
|
||||
@ -98,6 +101,7 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
||||
setSettings(serialiseSettingsData(settingsData.settings));
|
||||
setSiteData(siteDataResponse.site);
|
||||
setConfig(configData.config);
|
||||
setSettingsLoaded(true);
|
||||
} catch (error) {
|
||||
// Log error in settings API
|
||||
}
|
||||
@ -127,7 +131,7 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
||||
// Provide the settings and the saveSettings function to the children components
|
||||
return (
|
||||
<SettingsContext.Provider value={{
|
||||
settings, saveSettings, siteData, config
|
||||
settings, saveSettings, siteData, config, settingsLoaded
|
||||
}}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
@ -135,4 +139,3 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
|
||||
};
|
||||
|
||||
export {SettingsContext, SettingsProvider};
|
||||
|
||||
|
@ -3,6 +3,7 @@ import NiceModal from '@ebay/nice-modal-react';
|
||||
import Radio from '../../../admin-x-ds/global/form/Radio';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import useRoles from '../../../hooks/useRoles';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
import validator from 'validator';
|
||||
import {ServicesContext} from '../../providers/ServiceProvider';
|
||||
@ -15,6 +16,7 @@ const InviteUserModal = NiceModal.create(() => {
|
||||
const {api} = useContext(ServicesContext);
|
||||
const {roles, assignableRoles, getRoleId} = useRoles();
|
||||
const {invites, setInvites} = useStaffUsers();
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const focusRef = useRef<HTMLInputElement>(null);
|
||||
const [email, setEmail] = useState<string>('');
|
||||
@ -116,6 +118,9 @@ const InviteUserModal = NiceModal.create(() => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => {
|
||||
updateRoute('users');
|
||||
}}
|
||||
cancelLabel=''
|
||||
okLabel={okLabel}
|
||||
size={540}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import Avatar from '../../../admin-x-ds/global/Avatar';
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import InviteUserModal from './InviteUserModal';
|
||||
import List from '../../../admin-x-ds/global/List';
|
||||
import ListItem from '../../../admin-x-ds/global/ListItem';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
@ -9,6 +8,7 @@ import React, {useContext, useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import UserDetailModal from './UserDetailModal';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
import {ServicesContext} from '../../providers/ServiceProvider';
|
||||
import {User} from '../../../types/api';
|
||||
@ -190,9 +190,9 @@ const Users: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
invites,
|
||||
updateUser
|
||||
} = useStaffUsers();
|
||||
|
||||
const {updateRoute} = useRouting();
|
||||
const showInviteModal = () => {
|
||||
NiceModal.show(InviteUserModal);
|
||||
updateRoute('users/invite');
|
||||
};
|
||||
|
||||
const buttons = (
|
||||
|
@ -5,6 +5,7 @@ import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||
import ThemePreview from './designAndBranding/ThemePreview';
|
||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
import useForm from '../../../hooks/useForm';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {CustomThemeSetting, Post, Setting, SettingValue} from '../../../types/api';
|
||||
import {PreviewModalContent} from '../../../admin-x-ds/global/modal/PreviewModal';
|
||||
import {ServicesContext} from '../../providers/ServiceProvider';
|
||||
@ -61,6 +62,7 @@ const DesignModal: React.FC = () => {
|
||||
const [themeSettings, setThemeSettings] = useState<Array<CustomThemeSetting>>([]);
|
||||
const [latestPost, setLatestPost] = useState<Post | null>(null);
|
||||
const [selectedPreviewTab, setSelectedPreviewTab] = useState('homepage');
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
useEffect(() => {
|
||||
api.latestPost.browse().then((response) => {
|
||||
@ -183,6 +185,9 @@ const DesignModal: React.FC = () => {
|
||||
/>;
|
||||
|
||||
return <PreviewModalContent
|
||||
afterClose={() => {
|
||||
updateRoute('branding-and-design');
|
||||
}}
|
||||
buttonsDisabled={saveState === 'saving'}
|
||||
defaultTab='homepage'
|
||||
dirty={saveState === 'unsaved'}
|
||||
@ -198,6 +203,7 @@ const DesignModal: React.FC = () => {
|
||||
onOk={async () => {
|
||||
await handleSave();
|
||||
modal.remove();
|
||||
updateRoute('branding-and-design');
|
||||
}}
|
||||
onSelectURL={onSelectURL}
|
||||
/>;
|
||||
|
@ -1,12 +1,12 @@
|
||||
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';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
|
||||
const DesignSetting: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
const openPreviewModal = () => {
|
||||
NiceModal.show(DesignModal);
|
||||
updateRoute('branding-and-design/edit');
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,12 +1,12 @@
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import NavigationModal from './NavigationModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
|
||||
const Navigation: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
const openPreviewModal = () => {
|
||||
NiceModal.show(NavigationModal);
|
||||
updateRoute('navigation/edit');
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -3,13 +3,14 @@ import NavigationEditForm from './navigation/NavigationEditForm';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import useNavigationEditor, {NavigationItem} from '../../../hooks/site/useNavigationEditor';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {getSettingValues} from '../../../utils/helpers';
|
||||
import {useState} from 'react';
|
||||
|
||||
const NavigationModal = NiceModal.create(() => {
|
||||
const modal = useModal();
|
||||
|
||||
const {updateRoute} = useRouting();
|
||||
const {
|
||||
localSettings,
|
||||
updateSetting,
|
||||
@ -39,6 +40,9 @@ const NavigationModal = NiceModal.create(() => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => {
|
||||
updateRoute('navigation');
|
||||
}}
|
||||
buttonsDisabled={saveState === 'saving'}
|
||||
dirty={localSettings.some(setting => setting.dirty)}
|
||||
scrolling={true}
|
||||
@ -50,6 +54,7 @@ const NavigationModal = NiceModal.create(() => {
|
||||
if (navigation.validate() && secondaryNavigation.validate()) {
|
||||
await handleSave();
|
||||
modal.remove();
|
||||
updateRoute('navigation');
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import ChangeThemeModal from './ThemeModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
|
||||
const Theme: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Manage themes' link onClick={() => {
|
||||
NiceModal.show(ChangeThemeModal);
|
||||
updateRoute('themes/manage');
|
||||
}}/>}
|
||||
description="Change or upload themes"
|
||||
keywords={keywords}
|
||||
|
@ -10,6 +10,7 @@ import React, {useState} from 'react';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import ThemeInstalledModal from './theme/ThemeInstalledModal';
|
||||
import ThemePreview from './theme/ThemePreview';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {API} from '../../../utils/api';
|
||||
import {OfficialTheme} from '../../../models/themes';
|
||||
import {Theme} from '../../../types/api';
|
||||
@ -107,6 +108,7 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
|
||||
themes,
|
||||
setThemes
|
||||
}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
const api = useApi();
|
||||
const left =
|
||||
<TabView
|
||||
@ -154,6 +156,7 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
|
||||
color='black'
|
||||
label='OK'
|
||||
onClick = {() => {
|
||||
updateRoute('theme');
|
||||
modal.remove();
|
||||
}} />
|
||||
</div>;
|
||||
@ -188,6 +191,7 @@ const ChangeThemeModal = NiceModal.create(() => {
|
||||
const [selectedTheme, setSelectedTheme] = useState<OfficialTheme|null>(null);
|
||||
const [previewMode, setPreviewMode] = useState('desktop');
|
||||
const [isInstalling, setInstalling] = useState(false);
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const modal = useModal();
|
||||
const {themes, setThemes} = useThemes();
|
||||
@ -251,6 +255,9 @@ const ChangeThemeModal = NiceModal.create(() => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => {
|
||||
updateRoute('theme');
|
||||
}}
|
||||
cancelLabel=''
|
||||
footer={false}
|
||||
noPadding={true}
|
||||
|
18
apps/admin-x-settings/src/hooks/useRouting.tsx
Normal file
18
apps/admin-x-settings/src/hooks/useRouting.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import {RouteContext} from '../components/providers/RoutingProvider';
|
||||
import {useContext} from 'react';
|
||||
|
||||
export type RoutingHook = {
|
||||
route: string;
|
||||
updateRoute: (newPath: string) => void
|
||||
};
|
||||
|
||||
const useRouting = (): RoutingHook => {
|
||||
const {route, updateRoute} = useContext(RouteContext);
|
||||
|
||||
return {
|
||||
route,
|
||||
updateRoute
|
||||
};
|
||||
};
|
||||
|
||||
export default useRouting;
|
@ -53,8 +53,9 @@ Router.map(function () {
|
||||
this.route('collections');
|
||||
this.route('collection.new', {path: '/collections/new'});
|
||||
this.route('collection', {path: '/collections/:collection_slug'});
|
||||
|
||||
this.route('settings-x');
|
||||
this.route('settings-x', function () {
|
||||
this.route('settings-x', {path: '/*sub'});
|
||||
});
|
||||
this.route('settings');
|
||||
this.route('settings.general', {path: '/settings/general'});
|
||||
this.route('settings.membership', {path: '/settings/members'});
|
||||
|
Loading…
Reference in New Issue
Block a user