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,
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<IEditFilterProps> = ({
const { configuration } = useContext(ConfigurationContext);
const [searchValue, setSearchValue] = useState("");
const [currentFilter, setCurrentFilter] = useState<ListFilterModel>(
cloneDeep(filter)
);
const [criterion, setCriterion] = useState<Criterion<CriterionValue>>();
const [searchRef, setSearchFocus] = useFocusOnce();
const { criteria } = currentFilter;
const criteriaList = useMemo(() => {
@@ -275,17 +280,31 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
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<IEditFilterProps> = ({
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<IEditFilterProps> = ({
<>
<Modal show onHide={() => onCancel()} className="edit-filter-dialog">
<Modal.Header>
<FormattedMessage id="search_filter.edit_filter" />
<div>
<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.Body>
<div

View File

@@ -193,7 +193,13 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
// 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");

View File

@@ -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;

View File

@@ -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 |

View File

@@ -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;