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
This commit is contained in:
WithoutPants
2023-05-19 12:36:53 +10:00
committed by GitHub
parent 0a14394113
commit 702101ecce
5 changed files with 78 additions and 9 deletions

View File

@@ -7,7 +7,7 @@ import React, {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { Accordion, Button, Card, Modal } from "react-bootstrap"; import { Accordion, Button, Card, Form, Modal } from "react-bootstrap";
import cx from "classnames"; import cx from "classnames";
import { import {
CriterionValue, CriterionValue,
@@ -34,6 +34,8 @@ import { useToast } from "src/hooks/Toast";
import { useConfigureUI } from "src/core/StashService"; import { useConfigureUI } from "src/core/StashService";
import { IUIConfig } from "src/core/config"; import { IUIConfig } from "src/core/config";
import { FilterMode } from "src/core/generated-graphql"; import { FilterMode } from "src/core/generated-graphql";
import { useFocusOnce } from "src/utils/focus";
import Mousetrap from "mousetrap";
interface ICriterionList { interface ICriterionList {
criteria: string[]; criteria: string[];
@@ -222,11 +224,14 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
const { configuration } = useContext(ConfigurationContext); const { configuration } = useContext(ConfigurationContext);
const [searchValue, setSearchValue] = useState("");
const [currentFilter, setCurrentFilter] = useState<ListFilterModel>( const [currentFilter, setCurrentFilter] = useState<ListFilterModel>(
cloneDeep(filter) cloneDeep(filter)
); );
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>(); const [criterion, setCriterion] = useState<Criterion<CriterionValue>>();
const [searchRef, setSearchFocus] = useFocusOnce();
const { criteria } = currentFilter; const { criteria } = currentFilter;
const criteriaList = useMemo(() => { const criteriaList = useMemo(() => {
@@ -275,17 +280,31 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
const ui = (configuration?.ui ?? {}) as IUIConfig; const ui = (configuration?.ui ?? {}) as IUIConfig;
const [saveUI] = useConfigureUI(); 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( const pinnedFilters = useMemo(
() => ui.pinnedFilters?.[filterModeToConfigKey(currentFilter.mode)] ?? [], () => ui.pinnedFilters?.[filterModeToConfigKey(currentFilter.mode)] ?? [],
[currentFilter.mode, ui.pinnedFilters] [currentFilter.mode, ui.pinnedFilters]
); );
const pinnedElements = useMemo( const pinnedElements = useMemo(
() => criterionOptions.filter((c) => pinnedFilters.includes(c.messageID)), () => filteredOptions.filter((c) => pinnedFilters.includes(c.messageID)),
[pinnedFilters, criterionOptions] [pinnedFilters, filteredOptions]
); );
const unpinnedElements = useMemo( const unpinnedElements = useMemo(
() => criterionOptions.filter((c) => !pinnedFilters.includes(c.messageID)), () => filteredOptions.filter((c) => !pinnedFilters.includes(c.messageID)),
[pinnedFilters, criterionOptions] [pinnedFilters, filteredOptions]
); );
const editingCriterionChanged = useCompare(editingCriterion); const editingCriterionChanged = useCompare(editingCriterion);
@@ -304,6 +323,17 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
editingCriterionChanged, editingCriterionChanged,
]); ]);
useEffect(() => {
Mousetrap.bind("/", (e) => {
setSearchFocus();
e.preventDefault();
});
return () => {
Mousetrap.unbind("/");
};
});
async function updatePinnedFilters(filters: string[]) { async function updatePinnedFilters(filters: string[]) {
const configKey = filterModeToConfigKey(currentFilter.mode); const configKey = filterModeToConfigKey(currentFilter.mode);
try { try {
@@ -403,7 +433,16 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
<> <>
<Modal show onHide={() => onCancel()} className="edit-filter-dialog"> <Modal show onHide={() => onCancel()} className="edit-filter-dialog">
<Modal.Header> <Modal.Header>
<div>
<FormattedMessage id="search_filter.edit_filter" /> <FormattedMessage id="search_filter.edit_filter" />
</div>
<Form.Control
className="btn-secondary search-input"
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
placeholder={`${intl.formatMessage({ id: "actions.search" })}…`}
ref={searchRef}
/>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<div <div

View File

@@ -193,7 +193,13 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
// set up hotkeys // set up hotkeys
useEffect(() => { 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 () => { return () => {
Mousetrap.unbind("f"); Mousetrap.unbind("f");

View File

@@ -118,6 +118,15 @@ input[type="range"].zoom-slider {
} }
.edit-filter-dialog { .edit-filter-dialog {
.modal-header {
align-items: center;
padding: 0.5rem 1rem;
.search-input {
width: auto;
}
}
.modal-body { .modal-body {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;

View File

@@ -24,7 +24,7 @@
| Keyboard sequence | Action | | Keyboard sequence | Action |
|-------------------|--------| |-------------------|--------|
| `/` | Focus search field | | `/` | Focus search field / focus query field in filter dialog |
| `f` | Show Add Filter dialog | | `f` | Show Add Filter dialog |
| `r` | Reshuffle if sorted by random | | `r` | Reshuffle if sorted by random |
| `v g` | Set view to grid | | `v g` | Set view to grid |

View File

@@ -1,4 +1,4 @@
import { useRef } from "react"; import { useRef, useEffect } from "react";
const useFocus = () => { const useFocus = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -14,4 +14,19 @@ const useFocus = () => {
return [htmlElRef, setFocus] as const; 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; export default useFocus;