Add tab to Settings dialog to view keyboard shortcuts (#2567)

* Add keyboard custom icon

* Refactor Settings to be more modular

* Add basic keybindings view to settings

* Add more shortcuts

* Add link to see keyboard shortcuts tab

* Little more bottom padding

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Add keybindings to settings search

* Add a playwright test for opening the the keyboard shortcuts

* fmt

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2024-06-04 13:56:20 -04:00
committed by GitHub
parent 9564890b29
commit e46aca4992
12 changed files with 718 additions and 295 deletions

View File

@ -3,11 +3,23 @@ import { CustomIcon } from 'components/CustomIcon'
import decamelize from 'decamelize'
import Fuse from 'fuse.js'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { interactionMap } from 'lib/settings/initialKeybindings'
import { Setting } from 'lib/settings/initialSettings'
import { SettingsLevel } from 'lib/settings/settingsTypes'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useNavigate } from 'react-router-dom'
type ExtendedSettingsLevel = SettingsLevel | 'keybindings'
export type SettingsSearchItem = {
name: string
displayName: string
description: string
category: string
level: ExtendedSettingsLevel
}
export function SettingsSearchBar() {
const inputRef = useRef<HTMLInputElement>(null)
useHotkeys(
@ -21,29 +33,40 @@ export function SettingsSearchBar() {
const navigate = useNavigate()
const [query, setQuery] = useState('')
const { settings } = useSettingsAuthContext()
const settingsAsSearchable = useMemo(
() =>
Object.entries(settings.state.context).flatMap(
const settingsAsSearchable: SettingsSearchItem[] = useMemo(
() => [
...Object.entries(settings.state.context).flatMap(
([category, categorySettings]) =>
Object.entries(categorySettings).flatMap(([settingName, setting]) => {
const s = setting as Setting
return ['project', 'user']
return (['project', 'user'] satisfies SettingsLevel[])
.filter((l) => s.hideOnLevel !== l)
.map((l) => ({
category: decamelize(category, { separator: ' ' }),
settingName: settingName,
settingNameDisplay: decamelize(settingName, { separator: ' ' }),
setting: s,
level: l,
name: settingName,
description: s.description ?? '',
displayName: decamelize(settingName, { separator: ' ' }),
level: l as ExtendedSettingsLevel,
}))
})
),
...Object.entries(interactionMap).flatMap(
([category, categoryKeybindings]) =>
categoryKeybindings.map((keybinding) => ({
name: keybinding.name,
displayName: keybinding.title,
description: keybinding.description,
category: category,
level: 'keybindings' as ExtendedSettingsLevel,
}))
),
],
[settings.state.context]
)
const [searchResults, setSearchResults] = useState(settingsAsSearchable)
const fuse = new Fuse(settingsAsSearchable, {
keys: ['category', 'settingNameDisplay', 'setting.description'],
keys: ['category', 'displayName', 'description'],
includeScore: true,
})
@ -52,16 +75,8 @@ export function SettingsSearchBar() {
setSearchResults(query.length > 0 ? results : settingsAsSearchable)
}, [query])
function handleSelection({
level,
settingName,
}: {
category: string
settingName: string
setting: Setting<unknown>
level: string
}) {
navigate(`?tab=${level}#${settingName}`)
function handleSelection({ level, name }: SettingsSearchItem) {
navigate(`?tab=${level}#${name}`)
}
return (
@ -87,18 +102,18 @@ export function SettingsSearchBar() {
<Combobox.Options className="absolute top-full mt-2 right-0 w-80 overflow-y-auto z-50 max-h-96 cursor-pointer bg-chalkboard-10 dark:bg-chalkboard-100 border border-solid border-primary dark:border-chalkboard-30 rounded">
{searchResults?.map((option) => (
<Combobox.Option
key={`${option.category}-${option.settingName}-${option.level}`}
key={`${option.category}-${option.name}-${option.level}`}
value={option}
className="flex flex-col items-start gap-2 px-4 py-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
>
<p className="flex-grow text-base capitalize m-0 leading-none">
{option.level} ·{' '}
{decamelize(option.category, { separator: ' ' })} ·{' '}
{option.settingNameDisplay}
{option.displayName}
</p>
{option.setting.description && (
{option.description && (
<p className="text-xs leading-tight text-chalkboard-70 dark:text-chalkboard-50">
{option.setting.description}
{option.description}
</p>
)}
</Combobox.Option>