Settings keyboard shortcuts (#19836)
ref DES-166 Accessing settings via a keyboard shortcut is a great productivity booster for advanced users and it is missing from Ghost today.
This commit is contained in:
parent
19da5c6af4
commit
9d9707e6f4
@ -127,6 +127,9 @@ const Select: React.FC<SelectProps> = ({
|
||||
document.activeElement.blur();
|
||||
}
|
||||
setFocusState(false);
|
||||
|
||||
// Prevent the event from bubbling up to the window level
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -107,6 +107,9 @@ const Modal: React.FC<ModalProps> = ({
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent the event from bubbling up to the window level
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ import ExitSettingsButton from './components/ExitSettingsButton';
|
||||
import Settings from './components/Settings';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Users from './components/settings/general/Users';
|
||||
import {Heading, topLevelBackdropClasses} from '@tryghost/admin-x-design-system';
|
||||
import {Heading, confirmIfDirty, topLevelBackdropClasses, useGlobalDirtyState} from '@tryghost/admin-x-design-system';
|
||||
import {ReactNode, useEffect} from 'react';
|
||||
import {canAccessSettings, isEditorUser} from '@tryghost/admin-x-framework/api/users';
|
||||
import {toast} from 'react-hot-toast';
|
||||
@ -11,7 +11,7 @@ import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
const Page: React.FC<{children: ReactNode}> = ({children}) => {
|
||||
return <>
|
||||
<div className='sticky top-0 z-30 bg-white px-[5vmin] py-4 dark:bg-grey-975 tablet:fixed tablet:bg-transparent tablet:px-6 dark:tablet:bg-transparent xl:p-12' id="done-button-container">
|
||||
<div className='fixed right-0 top-2 z-50 flex justify-end p-8 dark:bg-grey-975 tablet:fixed tablet:top-0 tablet:bg-transparent tablet:px-8 dark:tablet:bg-transparent' id="done-button-container">
|
||||
<ExitSettingsButton />
|
||||
</div>
|
||||
<div className="w-full dark:bg-grey-975 tablet:fixed tablet:left-0 tablet:top-0 tablet:flex tablet:h-full" id="admin-x-settings-content">
|
||||
@ -23,6 +23,25 @@ const Page: React.FC<{children: ReactNode}> = ({children}) => {
|
||||
const MainContent: React.FC = () => {
|
||||
const {currentUser} = useGlobalData();
|
||||
const {route, updateRoute, loadingModal} = useRouting();
|
||||
const {isDirty} = useGlobalDirtyState();
|
||||
|
||||
const navigateAway = () => {
|
||||
window.location.hash = '/dashboard';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
confirmIfDirty(isDirty, navigateAway);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// resets any toasts that may have been left open on initial load
|
||||
@ -53,12 +72,12 @@ const MainContent: React.FC = () => {
|
||||
return (
|
||||
<Page>
|
||||
{loadingModal && <div className={`fixed inset-0 z-40 h-[calc(100vh-55px)] w-[100vw] tablet:h-[100vh] ${topLevelBackdropClasses}`} />}
|
||||
<div className="no-scrollbar fixed inset-x-0 top-[52px] z-[35] flex-1 basis-[320px] bg-white px-8 pb-8 dark:bg-grey-975 tablet:relative tablet:inset-x-auto tablet:top-auto tablet:h-full tablet:overflow-y-scroll tablet:bg-grey-50 tablet:pb-0 dark:tablet:bg-black" id="admin-x-settings-sidebar-scroller">
|
||||
<div className="no-scrollbar fixed inset-x-0 top-0 z-[35] flex-1 basis-[320px] bg-white p-8 dark:bg-grey-975 tablet:relative tablet:inset-x-auto tablet:top-auto tablet:h-full tablet:overflow-y-scroll tablet:bg-grey-50 tablet:py-0 dark:tablet:bg-black" id="admin-x-settings-sidebar-scroller">
|
||||
<div className="relative w-full">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-full flex-1 overflow-y-scroll pt-11 tablet:basis-[800px]" id="admin-x-settings-scroller">
|
||||
<div className="relative h-full flex-1 overflow-y-scroll bg-white pt-11 dark:bg-black tablet:basis-[800px]" id="admin-x-settings-scroller">
|
||||
<Settings />
|
||||
</div>
|
||||
</Page>
|
||||
|
@ -9,7 +9,7 @@ const ExitSettingsButton: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button data-testid="exit-settings" id="done-button" label='← Done' link={true} onClick={() => confirmIfDirty(isDirty, navigateAway)} />
|
||||
<Button className='text-grey-700 hover:!text-black' data-testid="exit-settings" icon='close' id="done-button" label='' link={true} title='Close (ESC)' onClick={() => confirmIfDirty(isDirty, navigateAway)} />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -10,7 +10,7 @@ import SiteSettings from './settings/site/SiteSettings';
|
||||
const Settings: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='mb-[40vh] px-8 pt-8 tablet:max-w-[760px] tablet:px-14 xl:pt-0'>
|
||||
<div className='mb-[40vh] px-8 pt-16 tablet:max-w-[760px] tablet:px-14 tablet:pt-0'>
|
||||
<GeneralSettings />
|
||||
<SiteSettings />
|
||||
<MembershipSettings />
|
||||
|
@ -78,6 +78,26 @@ const Sidebar: React.FC = () => {
|
||||
}
|
||||
}, [checkVisible, setNoResult, filter]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && filter) {
|
||||
// Blur the field
|
||||
searchInputRef.current?.blur();
|
||||
|
||||
// Prevent the event from bubbling up to the window level
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
// Add the event listener to the searchInputRef field
|
||||
searchInputRef.current?.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// Clean up the event listener when the component unmounts
|
||||
return () => {
|
||||
searchInputRef.current?.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [filter]);
|
||||
|
||||
const {settings, config} = useGlobalData();
|
||||
const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string];
|
||||
const hasStripeEnabled = checkStripeEnabled(settings || [], config || {});
|
||||
@ -107,12 +127,12 @@ const Sidebar: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className='ml-auto flex w-full flex-col pt-0 tablet:max-w-[240px]' data-testid="sidebar">
|
||||
<div className='sticky top-0 flex content-stretch items-end bg-grey-50 dark:bg-grey-975 tablet:h-28 dark:tablet:bg-black xl:h-20'>
|
||||
<div className='sticky top-0 flex content-stretch items-end dark:bg-grey-975 tablet:h-20 tablet:bg-grey-50 dark:tablet:bg-black xl:h-20'>
|
||||
<div className='relative w-full'>
|
||||
<Icon className='absolute left-3 top-3 z-10' colorClass='text-grey-500' name='magnifying-glass' size='sm' />
|
||||
<TextField
|
||||
autoComplete="off"
|
||||
className='flex h-10 w-full items-center rounded-lg border border-transparent bg-white px-[33px] py-1.5 text-[14px] shadow-[0_0_1px_rgba(21,23,26,0.25),0_1px_3px_rgba(0,0,0,0.03),0_8px_10px_-12px_rgba(0,0,0,.1)] transition-colors hover:shadow-sm focus:border-green focus:bg-white focus:shadow-[0_0_0_2px_rgba(48,207,67,0.25)] focus:outline-2 dark:border-transparent dark:bg-grey-925 dark:text-white dark:placeholder:text-grey-800 dark:focus:border-green dark:focus:bg-grey-950'
|
||||
className='mr-12 flex h-10 w-full items-center rounded-lg border border-transparent bg-white px-[33px] py-1.5 text-[14px] shadow-[0_0_1px_rgba(21,23,26,0.25),0_1px_3px_rgba(0,0,0,0.03),0_8px_10px_-12px_rgba(0,0,0,.1)] transition-colors hover:shadow-sm focus:border-green focus:bg-white focus:shadow-[0_0_0_2px_rgba(48,207,67,0.25)] focus:outline-2 dark:border-transparent dark:bg-grey-925 dark:text-white dark:placeholder:text-grey-800 dark:focus:border-green dark:focus:bg-grey-950 tablet:mr-0'
|
||||
containerClassName='w-100'
|
||||
inputRef={searchInputRef}
|
||||
placeholder="Search settings"
|
||||
|
@ -11,11 +11,41 @@
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.gh-update-banner ~ * #admin-x-settings-sidebar-scroller {
|
||||
position: sticky;
|
||||
margin-bottom: -60px;
|
||||
position: fixed;
|
||||
top: 48px;
|
||||
}
|
||||
|
||||
.gh-update-banner ~ * #done-button-container {
|
||||
top: 0;
|
||||
top: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.gh-update-banner ~ * #admin-x-settings-sidebar-scroller {
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
}
|
||||
|
||||
.gh-update-banner ~ * #done-button-container {
|
||||
top: 84px;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-x-base {
|
||||
opacity: 1;
|
||||
animation-name: openAnimation;
|
||||
animation-iteration-count: 1;
|
||||
animation-timing-function: ease;
|
||||
animation-duration: 0.35s;
|
||||
}
|
||||
|
||||
@keyframes openAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px) scale(1.0);
|
||||
}
|
||||
}
|
@ -100,7 +100,7 @@
|
||||
</div>
|
||||
<div class="flex items-center pe-all">
|
||||
{{#if (or (gh-user-can-admin this.session.user) this.session.user.isEditor)}}
|
||||
<LinkTo class="gh-nav-bottom-tabicon" @route="settings-x" @current-when={{this.isSettingsRoute}} data-test-nav="settings">{{svg-jar "settings"}}</LinkTo>
|
||||
<LinkTo class="gh-nav-bottom-tabicon" @route="settings-x" @current-when={{this.isSettingsRoute}} data-test-nav="settings">{{svg-jar "settings" title="Settings (CTRL/⌘ + ,)"}}</LinkTo>
|
||||
{{/if}}
|
||||
<div class="nightshift-toggle-container">
|
||||
<div class="nightshift-toggle {{if this.feature.nightShift "on"}}" {{action (toggle "nightShift" this.feature)}}>
|
||||
|
@ -57,6 +57,7 @@ export default class Main extends Component.extend(ShortcutsMixin) {
|
||||
let shortcuts = {};
|
||||
|
||||
shortcuts[`${ctrlOrCmd}+k`] = {action: 'openSearchModal'};
|
||||
shortcuts[`${ctrlOrCmd}+,`] = {action: 'openSettings'};
|
||||
this.shortcuts = shortcuts;
|
||||
}
|
||||
|
||||
@ -98,6 +99,11 @@ export default class Main extends Component.extend(ShortcutsMixin) {
|
||||
return this.modals.open(SearchModal);
|
||||
}
|
||||
|
||||
@action
|
||||
openSettings() {
|
||||
this.router.transitionTo('settings-x');
|
||||
}
|
||||
|
||||
@action
|
||||
toggleBillingModal() {
|
||||
this.billing.openBillingWindow(this.router.currentURL);
|
||||
|
Loading…
Reference in New Issue
Block a user