mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
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:
parent
7cd9864a7a
commit
0b07c44797
@ -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>
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user