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,
|
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>
|
||||||
<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.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user