AdminX searchfield keyboard shortcut (#18244)

refs. https://github.com/TryGhost/Product/issues/3349

- it's a bit cumbersome to always click in the searchfield to use it.
`/` is a standard keyboard shortcut to focus on a searchfield in
apps. Also, by auto-focusing on the searchfield it's even faster to find
settings.
This commit is contained in:
Peter Zimon 2023-09-20 15:32:18 +03:00 committed by GitHub
parent 7cd9864a7a
commit 0b07c44797
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 93 additions and 61 deletions

View File

@ -51,15 +51,15 @@ const MainContent: React.FC = () => {
{loadingModal && <div className={`fixed inset-0 z-40 h-[100vh] w-[100vw] ${topLevelBackdropClasses}`} />}
{/* Sidebar */}
<div className="sticky top-[-42px] z-20 min-w-[260px] grow-0 md:top-[-52px] tablet:fixed tablet:top-[8vmin] tablet:basis-[260px]">
<div className="sticky top-[-47px] z-30 min-w-[260px] grow-0 md:top-[-52px] tablet:fixed tablet:top-[8vmin] tablet:basis-[260px]">
<div className='-mx-6 h-[84px] bg-white px-6 tablet:m-0 tablet:bg-transparent tablet:p-0'>
<Heading>Settings</Heading>
</div>
<div className="relative mt-[-32px] w-full overflow-x-hidden">
<div className="relative mt-[-32px] w-full overflow-x-hidden bg-white dark:bg-black">
<Sidebar />
</div>
</div>
<div className="relative flex-auto pt-[10vmin] tablet:ml-[300px] tablet:pt-[85px]">
<div className="relative flex-auto pt-[10vmin] tablet:ml-[300px] tablet:pt-[94px]">
<Settings />
</div>
</Page>

View File

@ -6,7 +6,7 @@ interface Props {
}
const SettingSectionHeader: React.FC<Props> = ({title, sticky = false}) => {
let styles = 'pb-4 text-2xs font-semibold uppercase tracking-wider text-grey-700 z-20 ';
let styles = 'pb-[10px] text-2xs font-semibold uppercase tracking-wider text-grey-700 z-20 ';
if (sticky) {
styles += ' sticky top-0 -mt-4 pt-4 bg-white dark:bg-black';
}

View File

@ -1,5 +1,6 @@
import Button from '../admin-x-ds/global/Button';
import Icon from '../admin-x-ds/global/Icon';
import React from 'react';
import React, {useEffect, useRef} 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';
@ -17,6 +18,30 @@ import {useSearch} from './providers/ServiceProvider';
const Sidebar: React.FC = () => {
const {filter, setFilter} = useSearch();
const {updateRoute} = useRouting();
const searchInputRef = useRef<HTMLInputElement | null>(null);
// Focus in on search field when pressing CMD+K/CTRL+K
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === '/') {
e?.preventDefault();
if (searchInputRef.current) {
searchInputRef.current.focus();
}
}
};
window.addEventListener('keydown', handleKeyPress);
return () => {
window.removeEventListener('keydown', handleKeyPress);
};
}, []);
// Auto-focus on searchfield on page load
useEffect(() => {
if (searchInputRef.current) {
searchInputRef.current.focus();
}
}, []);
const {settings, config} = useGlobalData();
const [newslettersEnabled] = getSettingValues(settings, ['editor_default_email_recipients']) as [string];
@ -41,7 +66,10 @@ const Sidebar: React.FC = () => {
<div>
<div className='relative md:pt-4 tablet:h-[64px] tablet:pt-[32px]'>
<Icon className='absolute top-2 md:top-6 tablet:top-10' colorClass='text-grey-500' name='magnifying-glass' size='sm' />
<TextField autoComplete="off" className='border-b border-grey-500 bg-transparent px-3 py-1.5 pl-[24px] text-sm dark:text-white' placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={updateSearch} />
<TextField autoComplete="off" className='border-b border-grey-500 bg-transparent px-3 py-1.5 pl-[24px] text-sm dark:text-white' inputRef={searchInputRef} placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={updateSearch} />
{filter ? <Button className='absolute -right-1 top-1 p-1 tablet:top-9' icon='close' iconColorClass='text-grey-700 !w-3 !h-3' size='sm' unstyled onClick={() => {
setFilter('');
}} /> : <div className='absolute right-0 top-[22px] hidden rounded-sm bg-grey-200 px-1 py-0.5 text-2xs font-semibold uppercase tracking-wider text-grey-600 dark:bg-grey-800 dark:text-grey-500 tablet:!visible tablet:top-[38px] tablet:!block'>/</div>}
</div>
<div className="no-scrollbar hidden pt-10 tablet:!visible tablet:!block tablet:h-[calc(100vh-5vmin-84px-64px)] tablet:w-[240px] tablet:overflow-y-auto" id='admin-x-settings-sidebar'>
<SettingNavSection keywords={Object.values(generalSearchKeywords).flat()} title="General">

View File

@ -35,7 +35,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
const {mutateAsync: updateTier} = useEditTier();
const {mutateAsync: createTier} = useAddTier();
const [hasFreeTrial, setHasFreeTrial] = React.useState(!!tier?.trial_days);
const {localSettings} = useSettingGroup();
const {localSettings, siteData} = useSettingGroup();
const siteTitle = getSettingValues(localSettings, ['title']) as string[];
const [errors, setErrors] = useState<{ [key in keyof Tier]?: string }>({}); // eslint-disable-line no-unused-vars
@ -208,65 +208,69 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
value={formState.description || ''}
onChange={e => updateForm(state => ({...state, description: e.target.value}))}
/>
{!isFreeTier && <div className='flex flex-col gap-10 md:flex-row'>
<div className='basis-1/2'>
<div className='mb-1 flex h-6 items-center justify-between'>
<Heading level={6}>Prices</Heading>
<div className='w-10'>
<Select
border={false}
containerClassName='font-medium'
controlClasses={{menu: 'w-14'}}
options={currencySelectGroups()}
selectedOption={formState.currency}
size='xs'
onSelect={currency => updateForm(state => ({...state, currency}))}
{!isFreeTier &&
(<>
<div className='flex flex-col gap-10 md:flex-row'>
<div className='basis-1/2'>
<div className='mb-1 flex h-6 items-center justify-between'>
<Heading level={6}>Prices</Heading>
<div className='w-10'>
<Select
border={false}
containerClassName='font-medium'
controlClasses={{menu: 'w-14'}}
options={currencySelectGroups()}
selectedOption={formState.currency}
size='xs'
onSelect={currency => updateForm(state => ({...state, currency}))}
/>
</div>
</div>
<div className='flex flex-col gap-2'>
<CurrencyField
error={Boolean(errors.monthly_price)}
hint={errors.monthly_price}
placeholder='1'
rightPlaceholder={`${formState.currency}/month`}
title='Monthly price'
valueInCents={formState.monthly_price || ''}
hideTitle
onBlur={() => validators.monthly_price()}
onChange={price => updateForm(state => ({...state, monthly_price: price}))}
/>
<CurrencyField
error={Boolean(errors.yearly_price)}
hint={errors.yearly_price}
placeholder='10'
rightPlaceholder={`${formState.currency}/year`}
title='Yearly price'
valueInCents={formState.yearly_price || ''}
hideTitle
onBlur={() => validators.yearly_price()}
onChange={price => updateForm(state => ({...state, yearly_price: price}))}
/>
</div>
</div>
<div className='flex flex-col gap-2'>
<CurrencyField
error={Boolean(errors.monthly_price)}
hint={errors.monthly_price}
placeholder='1'
rightPlaceholder={`${formState.currency}/month`}
title='Monthly price'
valueInCents={formState.monthly_price || ''}
hideTitle
onBlur={() => validators.monthly_price()}
onChange={price => updateForm(state => ({...state, monthly_price: price}))}
/>
<CurrencyField
error={Boolean(errors.yearly_price)}
hint={errors.yearly_price}
placeholder='10'
rightPlaceholder={`${formState.currency}/year`}
title='Yearly price'
valueInCents={formState.yearly_price || ''}
hideTitle
onBlur={() => validators.yearly_price()}
onChange={price => updateForm(state => ({...state, yearly_price: price}))}
/>
</div>
</div>
<div className='basis-1/2'>
<div className='mb-1 flex h-6 flex-col justify-center'>
<Toggle checked={hasFreeTrial} label='Add a free trial' labelStyle='heading' onChange={toggleFreeTrial} />
</div>
<TextField
disabled={!hasFreeTrial}
hint={<div className='mt-1'>
<div className='basis-1/2'>
<div className='mb-1 flex h-6 flex-col justify-center'>
<Toggle checked={hasFreeTrial} label='Add a free trial' labelStyle='heading' onChange={toggleFreeTrial} />
</div>
<TextField
disabled={!hasFreeTrial}
hint={<div className='mt-1'>
Members will be subscribed at full price once the trial ends. <a className='text-green' href="https://ghost.org/" rel="noreferrer" target="_blank">Learn more</a>
</div>}
placeholder='0'
rightPlaceholder='days'
title='Trial days'
value={formState.trial_days}
hideTitle
onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/[^\d]/, '')}))}
/>
</div>}
placeholder='0'
rightPlaceholder='days'
title='Trial days'
value={formState.trial_days}
hideTitle
onChange={e => updateForm(state => ({...state, trial_days: e.target.value.replace(/[^\d]/, '')}))}
/>
</div>
</div>
</div>}
<TextField hint='Redirect to this URL after signup for premium membership' placeholder={siteData?.url} title='Welcome page' />
</>)}
</Form>
<Form gap='none' title='Benefits' grouped>