mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user