diff --git a/frontend/src/ts/components/pages/settings/Setting.tsx b/frontend/src/ts/components/pages/settings/Setting.tsx index 4de64430c3d5..7a3d1d97cf1f 100644 --- a/frontend/src/ts/components/pages/settings/Setting.tsx +++ b/frontend/src/ts/components/pages/settings/Setting.tsx @@ -6,6 +6,7 @@ import { showErrorNotification, showSuccessNotification, } from "../../../states/notifications"; +import { getSettingsSearch } from "../../../states/settings-search"; import { cn } from "../../../utils/cn"; import { Button } from "../../common/Button"; import { FaProps } from "../../common/Fa"; @@ -18,67 +19,84 @@ type Props = { description: string | JSXElement; inputs?: JSXElement; fullWidthInputs?: JSXElement; + // extra text (e.g. option labels) the search filter also matches against + searchTerms?: string; }; export function Setting(props: Props): JSXElement { + const matchesSearch = (): boolean => { + const query = getSettingsSearch().trim().toLowerCase(); + if (query === "") return true; + const haystack = [ + props.title, + typeof props.description === "string" ? props.description : "", + props.searchTerms ?? "", + ] + .join(" ") + .toLowerCase(); + return haystack.includes(query); + }; + return ( -
-
-

-

- -
- -
{props.description}
-
-
{props.inputs}
+ }} + />
-
- {props.fullWidthInputs} -
+ +
+ +
{props.description}
+
+
{props.inputs}
+
+
+ {props.fullWidthInputs} + + ); } diff --git a/frontend/src/ts/components/pages/settings/SettingsPage.tsx b/frontend/src/ts/components/pages/settings/SettingsPage.tsx index 16da1cc15277..dfdf8f179300 100644 --- a/frontend/src/ts/components/pages/settings/SettingsPage.tsx +++ b/frontend/src/ts/components/pages/settings/SettingsPage.tsx @@ -5,6 +5,10 @@ import { z } from "zod"; import { resetConfig } from "../../../config/lifecycle"; import { configMetadata, OptionMetadata } from "../../../config/metadata"; +import { + getOptionLabel, + getOptionSearchTerms, +} from "../../../config/option-strings"; import { setConfig } from "../../../config/setters"; import { getConfig } from "../../../config/store"; import { @@ -16,6 +20,7 @@ import { useLocalStorage } from "../../../hooks/useLocalStorage"; import { useSavedIndicator } from "../../../hooks/useSavedIndicator"; import { isAuthenticated } from "../../../states/core"; import { showModal } from "../../../states/modals"; +import { isSettingsSearchActive } from "../../../states/settings-search"; import { showSimpleModal } from "../../../states/simple-modal"; import { cn } from "../../../utils/cn"; import fileStorage from "../../../utils/file-storage"; @@ -52,6 +57,7 @@ import { Tags } from "./custom-setting/Tags"; import { Theme } from "./custom-setting/Theme"; import { QuickNav } from "./QuickNav"; import { Setting } from "./Setting"; +import { SettingsSearch } from "./SettingsSearch"; export function SettingsPage(): JSXElement { const [hasLocalBg] = createResource( @@ -62,16 +68,23 @@ export function SettingsPage(): JSXElement { return (
- - + + {/* while filtering, show only the matching settings (hide everything else) */} + + + +
tip: You can also change all these settings quickly using the command line
( )
- -
+ + + + {/* while filtering, lay the matching sections out with a uniform gap */} +
@@ -248,7 +261,9 @@ export function SettingsPage(): JSXElement {
- + + +
); @@ -287,25 +302,39 @@ function Section(props: { title: string; children: JSXElement }): JSXElement { const [isOpen, setIsOpen] = createSignal(true); return ( -
- - + + + + {props.title} + + + {props.children} -
+ +
+
); @@ -405,39 +434,18 @@ function AutoSetting(props: { )} > - {(option) => { - const text = () => { - const optionsMeta = configMetadata[props.key] - .optionsMetadata as - | Record - | undefined; - const match = optionsMeta?.[String(option)]; - if (match?.displayString !== undefined) { - return match.displayString; - } - - if (option === true) { - return "on"; - } - if (option === false) { - return "off"; - } - - return (option as string).toString().replace(/_/g, " "); - }; - return ( - - ); - }} + {(option) => ( + + )}
); @@ -451,6 +459,7 @@ function AutoSetting(props: { title={configMetadata[props.key].displayString ?? props.key} fa={configMetadata[props.key].fa} description={configMetadata[props.key].description} + searchTerms={getOptionSearchTerms(props.key)} inputs={!props.wide ? autoInputs() : props.inputs} fullWidthInputs={ props.wide ? (autoInputs() ?? props.inputs) : props.inputs diff --git a/frontend/src/ts/components/pages/settings/SettingsSearch.tsx b/frontend/src/ts/components/pages/settings/SettingsSearch.tsx new file mode 100644 index 000000000000..40c74540da13 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/SettingsSearch.tsx @@ -0,0 +1,44 @@ +import { JSXElement, onCleanup, Show } from "solid-js"; + +import { + getSettingsSearch, + setSettingsSearch, +} from "../../../states/settings-search"; +import { cn } from "../../../utils/cn"; +import { Button } from "../../common/Button"; +import { Fa } from "../../common/Fa"; + +export function SettingsSearch(): JSXElement { + // reset the filter when leaving the settings page + onCleanup(() => setSettingsSearch("")); + + return ( +
+ + setSettingsSearch(e.currentTarget.value)} + /> + +
+ ); +} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx index 9f2b526696ea..b7551fbf461f 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx @@ -6,6 +6,7 @@ import { createForm } from "@tanstack/solid-form"; import { createResource, JSXElement, For, Show } from "solid-js"; import { configMetadata } from "../../../../config/metadata"; +import { getOptionSearchTerms } from "../../../../config/option-strings"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { applyCustomBackground } from "../../../../controllers/theme-controller"; @@ -56,6 +57,7 @@ export function CustomBackground(): JSXElement { configMetadata.customBackground.displayString ?? "custom background" } fa={configMetadata.customBackground.fa} + searchTerms={getOptionSearchTerms("customBackgroundSize")} description={ <> {configMetadata.customBackground.description} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx b/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx index 19ace4b8a7c4..a0f4d82d2c1b 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx @@ -3,6 +3,7 @@ import { createResource, For, JSXElement, Show } from "solid-js"; import { z } from "zod"; import { configMetadata } from "../../../../config/metadata"; +import { getOptionSearchTerms } from "../../../../config/option-strings"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { showNoticeNotification } from "../../../../states/notifications"; @@ -38,6 +39,7 @@ export function FontFamily(): JSXElement { key="fontFamily" title={configMetadata.fontFamily.displayString ?? "font family"} fa={configMetadata.fontFamily.fa} + searchTerms={getOptionSearchTerms("fontFamily")} description={ <> {configMetadata.fontFamily.description} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx b/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx index 89226f20319e..166a92eae826 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx @@ -6,6 +6,7 @@ import { createForm } from "@tanstack/solid-form"; import { For, JSXElement } from "solid-js"; import { configMetadata } from "../../../../config/metadata"; +import { getOptionSearchTerms } from "../../../../config/option-strings"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; import { useSavedIndicator } from "../../../../hooks/useSavedIndicator"; @@ -40,6 +41,7 @@ export function PaceCaret(): JSXElement { key="paceCaret" title={configMetadata.paceCaret.displayString ?? "pace caret"} fa={configMetadata.paceCaret.fa} + searchTerms={getOptionSearchTerms("paceCaret")} description={configMetadata.paceCaret.description} inputs={
diff --git a/frontend/src/ts/config/option-strings.ts b/frontend/src/ts/config/option-strings.ts new file mode 100644 index 000000000000..fd5a4280723d --- /dev/null +++ b/frontend/src/ts/config/option-strings.ts @@ -0,0 +1,39 @@ +import { Config, ConfigSchema } from "@monkeytype/schemas/configs"; + +import { getOptions } from "../utils/zod"; +import { configMetadata, OptionMetadata } from "./metadata"; + +// the label shown for a single option (and used to match it while searching) +export function getOptionLabel( + key: T, + option: Config[T], +): string { + const optionMeta = ( + configMetadata[key] as { + optionsMetadata?: Record | undefined; + } + ).optionsMetadata?.[String(option)]; + + if (optionMeta?.displayString !== undefined) return optionMeta.displayString; + if (option === true) return "on"; + if (option === false) return "off"; + return String(option).replace(/_/g, " "); +} + +// all of a setting's option labels joined, so the settings search can match on them +export function getOptionSearchTerms(key: T): string { + const optionsMeta = ( + configMetadata[key] as { + optionsMetadata?: Record | undefined; + } + ).optionsMetadata; + + const options = getOptions(ConfigSchema.shape[key])?.filter( + (option) => optionsMeta?.[String(option)]?.visible !== false, + ); + if (options === undefined) return ""; + + return options + .map((option) => getOptionLabel(key, option as Config[T])) + .join(" "); +} diff --git a/frontend/src/ts/states/settings-search.ts b/frontend/src/ts/states/settings-search.ts new file mode 100644 index 000000000000..9e7f21e2a315 --- /dev/null +++ b/frontend/src/ts/states/settings-search.ts @@ -0,0 +1,8 @@ +import { createSignal } from "solid-js"; + +// the current settings filter query, shared between the search input and the +// settings/sections that hide themselves when they don't match +export const [getSettingsSearch, setSettingsSearch] = createSignal(""); + +export const isSettingsSearchActive = (): boolean => + getSettingsSearch().trim() !== "";