From 702101ecce994e6ec77dabff5998245f5dce51f6 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 19 May 2023 12:36:53 +1000 Subject: [PATCH] Filter query (#3740) * Add search field to filter dialog * Add / shortcut to focus query * Fix f keybind typing f into query field * Document keyboard shortcut --- .../src/components/List/EditFilterDialog.tsx | 51 ++++++++++++++++--- ui/v2.5/src/components/List/ItemList.tsx | 8 ++- ui/v2.5/src/components/List/styles.scss | 9 ++++ .../src/docs/en/Manual/KeyboardShortcuts.md | 2 +- ui/v2.5/src/utils/focus.ts | 17 ++++++- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/ui/v2.5/src/components/List/EditFilterDialog.tsx b/ui/v2.5/src/components/List/EditFilterDialog.tsx index 4443ad805..581fd31fb 100644 --- a/ui/v2.5/src/components/List/EditFilterDialog.tsx +++ b/ui/v2.5/src/components/List/EditFilterDialog.tsx @@ -7,7 +7,7 @@ import React, { useRef, useState, } from "react"; -import { Accordion, Button, Card, Modal } from "react-bootstrap"; +import { Accordion, Button, Card, Form, Modal } from "react-bootstrap"; import cx from "classnames"; import { CriterionValue, @@ -34,6 +34,8 @@ import { useToast } from "src/hooks/Toast"; import { useConfigureUI } from "src/core/StashService"; import { IUIConfig } from "src/core/config"; import { FilterMode } from "src/core/generated-graphql"; +import { useFocusOnce } from "src/utils/focus"; +import Mousetrap from "mousetrap"; interface ICriterionList { criteria: string[]; @@ -222,11 +224,14 @@ export const EditFilterDialog: React.FC = ({ const { configuration } = useContext(ConfigurationContext); + const [searchValue, setSearchValue] = useState(""); const [currentFilter, setCurrentFilter] = useState( cloneDeep(filter) ); const [criterion, setCriterion] = useState>(); + const [searchRef, setSearchFocus] = useFocusOnce(); + const { criteria } = currentFilter; const criteriaList = useMemo(() => { @@ -275,17 +280,31 @@ export const EditFilterDialog: React.FC = ({ const ui = (configuration?.ui ?? {}) as IUIConfig; const [saveUI] = useConfigureUI(); + const filteredOptions = useMemo(() => { + const trimmedSearch = searchValue.trim().toLowerCase(); + if (!trimmedSearch) { + return criterionOptions; + } + + return criterionOptions.filter((c) => { + return intl + .formatMessage({ id: c.messageID }) + .toLowerCase() + .includes(trimmedSearch); + }); + }, [intl, searchValue, criterionOptions]); + const pinnedFilters = useMemo( () => ui.pinnedFilters?.[filterModeToConfigKey(currentFilter.mode)] ?? [], [currentFilter.mode, ui.pinnedFilters] ); const pinnedElements = useMemo( - () => criterionOptions.filter((c) => pinnedFilters.includes(c.messageID)), - [pinnedFilters, criterionOptions] + () => filteredOptions.filter((c) => pinnedFilters.includes(c.messageID)), + [pinnedFilters, filteredOptions] ); const unpinnedElements = useMemo( - () => criterionOptions.filter((c) => !pinnedFilters.includes(c.messageID)), - [pinnedFilters, criterionOptions] + () => filteredOptions.filter((c) => !pinnedFilters.includes(c.messageID)), + [pinnedFilters, filteredOptions] ); const editingCriterionChanged = useCompare(editingCriterion); @@ -304,6 +323,17 @@ export const EditFilterDialog: React.FC = ({ editingCriterionChanged, ]); + useEffect(() => { + Mousetrap.bind("/", (e) => { + setSearchFocus(); + e.preventDefault(); + }); + + return () => { + Mousetrap.unbind("/"); + }; + }); + async function updatePinnedFilters(filters: string[]) { const configKey = filterModeToConfigKey(currentFilter.mode); try { @@ -403,7 +433,16 @@ export const EditFilterDialog: React.FC = ({ <> onCancel()} className="edit-filter-dialog"> - +
+ +
+ setSearchValue(e.target.value)} + value={searchValue} + placeholder={`${intl.formatMessage({ id: "actions.search" })}…`} + ref={searchRef} + />
({ // set up hotkeys useEffect(() => { - Mousetrap.bind("f", () => setShowEditFilter(true)); + Mousetrap.bind("f", (e) => { + setShowEditFilter(true); + // prevent default behavior of typing f in a text field + // otherwise the filter dialog closes, the query field is focused and + // f is typed. + e.preventDefault(); + }); return () => { Mousetrap.unbind("f"); diff --git a/ui/v2.5/src/components/List/styles.scss b/ui/v2.5/src/components/List/styles.scss index cec9da69e..8b4c67827 100644 --- a/ui/v2.5/src/components/List/styles.scss +++ b/ui/v2.5/src/components/List/styles.scss @@ -118,6 +118,15 @@ input[type="range"].zoom-slider { } .edit-filter-dialog { + .modal-header { + align-items: center; + padding: 0.5rem 1rem; + + .search-input { + width: auto; + } + } + .modal-body { padding-left: 0; padding-right: 0; diff --git a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md index 7eee67ef4..98c13cad0 100644 --- a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md +++ b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md @@ -24,7 +24,7 @@ | Keyboard sequence | Action | |-------------------|--------| -| `/` | Focus search field | +| `/` | Focus search field / focus query field in filter dialog | | `f` | Show Add Filter dialog | | `r` | Reshuffle if sorted by random | | `v g` | Set view to grid | diff --git a/ui/v2.5/src/utils/focus.ts b/ui/v2.5/src/utils/focus.ts index 0ab1e3b68..189920752 100644 --- a/ui/v2.5/src/utils/focus.ts +++ b/ui/v2.5/src/utils/focus.ts @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useRef, useEffect } from "react"; const useFocus = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -14,4 +14,19 @@ const useFocus = () => { return [htmlElRef, setFocus] as const; }; +// focuses on the element only once on mount +export const useFocusOnce = () => { + const [htmlElRef, setFocus] = useFocus(); + const focused = useRef(false); + + useEffect(() => { + if (!focused.current) { + setFocus(); + focused.current = true; + } + }, [setFocus]); + + return [htmlElRef, setFocus] as const; +}; + export default useFocus;