mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Filter tweaks (#3772)
* Use debounce hook * Wait until search request complete before refreshing results * Add back null modifiers * Convert old excludes criterion to includes criterion * Display criteria with only excludes items as excludes * Fix depth display * Reset search after selection * Add back is modifier to tag filter * Focus the input dialog after select/unselect * Update unsupported modifiers
This commit is contained in:
parent
de4237e626
commit
09df203bcf
18 changed files with 344 additions and 216 deletions
|
|
@ -77,17 +77,15 @@ const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
|
|||
|
||||
return (
|
||||
<Form.Group className="modifier-options">
|
||||
{modifierOptions.map((c) => (
|
||||
{modifierOptions.map((m) => (
|
||||
<Button
|
||||
className={cx("modifier-option", {
|
||||
selected: criterion.modifier === c.value,
|
||||
selected: criterion.modifier === m,
|
||||
})}
|
||||
key={c.value}
|
||||
onClick={() =>
|
||||
onChangedModifierSelect(c.value as CriterionModifier)
|
||||
}
|
||||
key={m}
|
||||
onClick={() => onChangedModifierSelect(m)}
|
||||
>
|
||||
{c.label ? intl.formatMessage({ id: c.label }) : ""}
|
||||
{Criterion.getModifierLabel(intl, m)}
|
||||
</Button>
|
||||
))}
|
||||
</Form.Group>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||
import { useFindPerformersQuery } from "src/core/generated-graphql";
|
||||
import { ObjectsFilter } from "./SelectableFilter";
|
||||
|
|
@ -9,7 +9,7 @@ interface IPerformersFilter {
|
|||
}
|
||||
|
||||
function usePerformerQuery(query: string) {
|
||||
const results = useFindPerformersQuery({
|
||||
const { data, loading } = useFindPerformersQuery({
|
||||
variables: {
|
||||
filter: {
|
||||
q: query,
|
||||
|
|
@ -18,14 +18,18 @@ function usePerformerQuery(query: string) {
|
|||
},
|
||||
});
|
||||
|
||||
return (
|
||||
results.data?.findPerformers.performers.map((p) => {
|
||||
const results = useMemo(
|
||||
() =>
|
||||
data?.findPerformers.performers.map((p) => {
|
||||
return {
|
||||
id: p.id,
|
||||
label: p.name,
|
||||
};
|
||||
}) ?? []
|
||||
}) ?? [],
|
||||
[data]
|
||||
);
|
||||
|
||||
return { results, loading };
|
||||
}
|
||||
|
||||
const PerformersFilter: React.FC<IPerformersFilter> = ({
|
||||
|
|
@ -36,7 +40,7 @@ const PerformersFilter: React.FC<IPerformersFilter> = ({
|
|||
<ObjectsFilter
|
||||
criterion={criterion}
|
||||
setCriterion={setCriterion}
|
||||
queryHook={usePerformerQuery}
|
||||
useResults={usePerformerQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import {
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
ILabeledId,
|
||||
ILabeledValueListValue,
|
||||
} from "src/models/list-filter/types";
|
||||
import { cloneDeep, debounce } from "lodash-es";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import {
|
||||
Criterion,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
|
|
@ -22,6 +22,8 @@ import {
|
|||
import { defineMessages, MessageDescriptor, useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { keyboardClickHandler } from "src/utils/keyboard";
|
||||
import { useDebouncedSetState } from "src/hooks/debounce";
|
||||
import useFocus from "src/utils/focus";
|
||||
|
||||
interface ISelectedItem {
|
||||
item: ILabeledId;
|
||||
|
|
@ -77,40 +79,29 @@ const SelectedItem: React.FC<ISelectedItem> = ({
|
|||
|
||||
interface ISelectableFilter {
|
||||
query: string;
|
||||
setQuery: (query: string) => void;
|
||||
single: boolean;
|
||||
includeOnly: boolean;
|
||||
onQueryChange: (query: string) => void;
|
||||
modifier: CriterionModifier;
|
||||
inputFocus: ReturnType<typeof useFocus>;
|
||||
canExclude: boolean;
|
||||
queryResults: ILabeledId[];
|
||||
selected: ILabeledId[];
|
||||
excluded: ILabeledId[];
|
||||
onSelect: (value: ILabeledId, include: boolean) => void;
|
||||
onSelect: (value: ILabeledId, exclude: boolean) => void;
|
||||
onUnselect: (value: ILabeledId) => void;
|
||||
}
|
||||
|
||||
const SelectableFilter: React.FC<ISelectableFilter> = ({
|
||||
query,
|
||||
setQuery,
|
||||
single,
|
||||
onQueryChange,
|
||||
modifier,
|
||||
inputFocus,
|
||||
canExclude,
|
||||
queryResults,
|
||||
selected,
|
||||
excluded,
|
||||
includeOnly,
|
||||
onSelect,
|
||||
onUnselect,
|
||||
}) => {
|
||||
const [internalQuery, setInternalQuery] = useState(query);
|
||||
|
||||
const onInputChange = useMemo(() => {
|
||||
return debounce((input: string) => {
|
||||
setQuery(input);
|
||||
}, 250);
|
||||
}, [setQuery]);
|
||||
|
||||
function onInternalInputChange(input: string) {
|
||||
setInternalQuery(input);
|
||||
onInputChange(input);
|
||||
}
|
||||
|
||||
const objects = useMemo(() => {
|
||||
return queryResults.filter(
|
||||
(p) =>
|
||||
|
|
@ -119,8 +110,10 @@ const SelectableFilter: React.FC<ISelectableFilter> = ({
|
|||
);
|
||||
}, [queryResults, selected, excluded]);
|
||||
|
||||
const includingOnly = includeOnly || (selected.length > 0 && single);
|
||||
const excludingOnly = excluded.length > 0 && single;
|
||||
const includingOnly = modifier == CriterionModifier.Equals;
|
||||
const excludingOnly =
|
||||
modifier == CriterionModifier.Excludes ||
|
||||
modifier == CriterionModifier.NotEquals;
|
||||
|
||||
const includeIcon = <Icon className="fa-fw include-button" icon={faPlus} />;
|
||||
const excludeIcon = <Icon className="fa-fw exclude-icon" icon={faMinus} />;
|
||||
|
|
@ -128,13 +121,18 @@ const SelectableFilter: React.FC<ISelectableFilter> = ({
|
|||
return (
|
||||
<div className="selectable-filter">
|
||||
<ClearableInput
|
||||
value={internalQuery}
|
||||
setValue={(v) => onInternalInputChange(v)}
|
||||
focus={inputFocus}
|
||||
value={query}
|
||||
setValue={(v) => onQueryChange(v)}
|
||||
/>
|
||||
<ul>
|
||||
{selected.map((p) => (
|
||||
<li key={p.id} className="selected-object">
|
||||
<SelectedItem item={p} onClick={() => onUnselect(p)} />
|
||||
<SelectedItem
|
||||
item={p}
|
||||
excluded={excludingOnly}
|
||||
onClick={() => onUnselect(p)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{excluded.map((p) => (
|
||||
|
|
@ -144,12 +142,9 @@ const SelectableFilter: React.FC<ISelectableFilter> = ({
|
|||
))}
|
||||
{objects.map((p) => (
|
||||
<li key={p.id} className="unselected-object">
|
||||
{/* if excluding only, clicking on an item also excludes it */}
|
||||
<a
|
||||
onClick={() => onSelect(p, !excludingOnly)}
|
||||
onKeyDown={keyboardClickHandler(() =>
|
||||
onSelect(p, !excludingOnly)
|
||||
)}
|
||||
onClick={() => onSelect(p, false)}
|
||||
onKeyDown={keyboardClickHandler(() => onSelect(p, false))}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div>
|
||||
|
|
@ -159,11 +154,11 @@ const SelectableFilter: React.FC<ISelectableFilter> = ({
|
|||
<div>
|
||||
{/* TODO item count */}
|
||||
{/* <span className="object-count">{p.id}</span> */}
|
||||
{!includingOnly && !excludingOnly && (
|
||||
{canExclude && !includingOnly && !excludingOnly && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect(p, false);
|
||||
onSelect(p, true);
|
||||
}}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="minimal exclude-button"
|
||||
|
|
@ -183,36 +178,62 @@ const SelectableFilter: React.FC<ISelectableFilter> = ({
|
|||
|
||||
interface IObjectsFilter<T extends Criterion<ILabeledValueListValue>> {
|
||||
criterion: T;
|
||||
single?: boolean;
|
||||
setCriterion: (criterion: T) => void;
|
||||
queryHook: (query: string) => ILabeledId[];
|
||||
useResults: (query: string) => { results: ILabeledId[]; loading: boolean };
|
||||
}
|
||||
|
||||
export const ObjectsFilter = <
|
||||
T extends Criterion<ILabeledValueListValue | IHierarchicalLabelValue>
|
||||
>(
|
||||
props: IObjectsFilter<T>
|
||||
) => {
|
||||
const { criterion, setCriterion, queryHook, single = false } = props;
|
||||
|
||||
>({
|
||||
criterion,
|
||||
setCriterion,
|
||||
useResults,
|
||||
}: IObjectsFilter<T>) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [displayQuery, setDisplayQuery] = useState(query);
|
||||
|
||||
const queryResults = queryHook(query);
|
||||
const debouncedSetQuery = useDebouncedSetState(setQuery, 250);
|
||||
const onQueryChange = useCallback(
|
||||
(input: string) => {
|
||||
setDisplayQuery(input);
|
||||
debouncedSetQuery(input);
|
||||
},
|
||||
[debouncedSetQuery, setDisplayQuery]
|
||||
);
|
||||
|
||||
function onSelect(value: ILabeledId, newInclude: boolean) {
|
||||
const [queryResults, setQueryResults] = useState<ILabeledId[]>([]);
|
||||
const { results, loading: resultsLoading } = useResults(query);
|
||||
useEffect(() => {
|
||||
if (!resultsLoading) {
|
||||
setQueryResults(results);
|
||||
}
|
||||
}, [results, resultsLoading]);
|
||||
|
||||
const inputFocus = useFocus();
|
||||
const [, setInputFocus] = inputFocus;
|
||||
|
||||
function onSelect(value: ILabeledId, newExclude: boolean) {
|
||||
let newCriterion: T = cloneDeep(criterion);
|
||||
|
||||
if (newInclude) {
|
||||
newCriterion.value.items.push(value);
|
||||
} else {
|
||||
if (newExclude) {
|
||||
if (newCriterion.value.excluded) {
|
||||
newCriterion.value.excluded.push(value);
|
||||
} else {
|
||||
newCriterion.value.excluded = [value];
|
||||
}
|
||||
} else {
|
||||
newCriterion.value.items.push(value);
|
||||
}
|
||||
|
||||
setCriterion(newCriterion);
|
||||
|
||||
// reset filter query after selecting
|
||||
debouncedSetQuery.cancel();
|
||||
setQuery("");
|
||||
setDisplayQuery("");
|
||||
|
||||
// focus the input box
|
||||
setInputFocus();
|
||||
}
|
||||
|
||||
const onUnselect = useCallback(
|
||||
|
|
@ -229,8 +250,11 @@ export const ObjectsFilter = <
|
|||
);
|
||||
|
||||
setCriterion(newCriterion);
|
||||
|
||||
// focus the input box
|
||||
setInputFocus();
|
||||
},
|
||||
[criterion, setCriterion]
|
||||
[criterion, setCriterion, setInputFocus]
|
||||
);
|
||||
|
||||
const sortedSelected = useMemo(() => {
|
||||
|
|
@ -246,12 +270,19 @@ export const ObjectsFilter = <
|
|||
return ret;
|
||||
}, [criterion]);
|
||||
|
||||
// if excludes is not a valid modifierOption then we can use `value.excluded`
|
||||
const canExclude =
|
||||
criterion.criterionOption.modifierOptions.find(
|
||||
(m) => m === CriterionModifier.Excludes
|
||||
) === undefined;
|
||||
|
||||
return (
|
||||
<SelectableFilter
|
||||
single={single}
|
||||
includeOnly={criterion.modifier === CriterionModifier.Equals}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
query={displayQuery}
|
||||
onQueryChange={onQueryChange}
|
||||
modifier={criterion.modifier}
|
||||
inputFocus={inputFocus}
|
||||
canExclude={canExclude}
|
||||
selected={sortedSelected}
|
||||
queryResults={queryResults}
|
||||
onSelect={onSelect}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useFindStudiosQuery } from "src/core/generated-graphql";
|
||||
import { HierarchicalObjectsFilter } from "./SelectableFilter";
|
||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
|
|
@ -9,7 +9,7 @@ interface IStudiosFilter {
|
|||
}
|
||||
|
||||
function useStudioQuery(query: string) {
|
||||
const results = useFindStudiosQuery({
|
||||
const { data, loading } = useFindStudiosQuery({
|
||||
variables: {
|
||||
filter: {
|
||||
q: query,
|
||||
|
|
@ -18,14 +18,18 @@ function useStudioQuery(query: string) {
|
|||
},
|
||||
});
|
||||
|
||||
return (
|
||||
results.data?.findStudios.studios.map((p) => {
|
||||
const results = useMemo(
|
||||
() =>
|
||||
data?.findStudios.studios.map((p) => {
|
||||
return {
|
||||
id: p.id,
|
||||
label: p.name,
|
||||
};
|
||||
}) ?? []
|
||||
}) ?? [],
|
||||
[data]
|
||||
);
|
||||
|
||||
return { results, loading };
|
||||
}
|
||||
|
||||
const StudiosFilter: React.FC<IStudiosFilter> = ({
|
||||
|
|
@ -36,7 +40,7 @@ const StudiosFilter: React.FC<IStudiosFilter> = ({
|
|||
<HierarchicalObjectsFilter
|
||||
criterion={criterion}
|
||||
setCriterion={setCriterion}
|
||||
queryHook={useStudioQuery}
|
||||
useResults={useStudioQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useFindTagsQuery } from "src/core/generated-graphql";
|
||||
import { HierarchicalObjectsFilter } from "./SelectableFilter";
|
||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
|
|
@ -8,8 +8,8 @@ interface ITagsFilter {
|
|||
setCriterion: (c: StudiosCriterion) => void;
|
||||
}
|
||||
|
||||
function useStudioQuery(query: string) {
|
||||
const results = useFindTagsQuery({
|
||||
function useTagQuery(query: string) {
|
||||
const { data, loading } = useFindTagsQuery({
|
||||
variables: {
|
||||
filter: {
|
||||
q: query,
|
||||
|
|
@ -18,14 +18,18 @@ function useStudioQuery(query: string) {
|
|||
},
|
||||
});
|
||||
|
||||
return (
|
||||
results.data?.findTags.tags.map((p) => {
|
||||
const results = useMemo(
|
||||
() =>
|
||||
data?.findTags.tags.map((p) => {
|
||||
return {
|
||||
id: p.id,
|
||||
label: p.name,
|
||||
};
|
||||
}) ?? []
|
||||
}) ?? [],
|
||||
[data]
|
||||
);
|
||||
|
||||
return { results, loading };
|
||||
}
|
||||
|
||||
const TagsFilter: React.FC<ITagsFilter> = ({ criterion, setCriterion }) => {
|
||||
|
|
@ -33,7 +37,7 @@ const TagsFilter: React.FC<ITagsFilter> = ({ criterion, setCriterion }) => {
|
|||
<HierarchicalObjectsFilter
|
||||
criterion={criterion}
|
||||
setCriterion={setCriterion}
|
||||
queryHook={useStudioQuery}
|
||||
useResults={useTagQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,15 +8,17 @@ import useFocus from "src/utils/focus";
|
|||
interface IClearableInput {
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
focus: ReturnType<typeof useFocus>;
|
||||
}
|
||||
|
||||
export const ClearableInput: React.FC<IClearableInput> = ({
|
||||
value,
|
||||
setValue,
|
||||
focus,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [queryRef, setQueryFocus] = useFocus();
|
||||
const [queryRef, setQueryFocus] = focus;
|
||||
const queryClearShowing = !!value;
|
||||
|
||||
function onChangeQuery(event: React.FormEvent<HTMLInputElement>) {
|
||||
|
|
|
|||
|
|
@ -730,7 +730,9 @@
|
|||
"equals": "is",
|
||||
"excludes": "excludes",
|
||||
"format_string": "{criterion} {modifierString} {valueString}",
|
||||
"format_string_depth": "{criterion} {modifierString} {valueString} (+{depth, plural, =-1 {all} other {{depth}}})",
|
||||
"format_string_excludes": "{criterion} {modifierString} {valueString} (excludes {excludedString})",
|
||||
"format_string_excludes_depth": "{criterion} {modifierString} {valueString} (excludes {excludedString}) (+{depth, plural, =-1 {all} other {{depth}}})",
|
||||
"greater_than": "is greater than",
|
||||
"includes": "includes",
|
||||
"includes_all": "includes all",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class CountryCriterion extends StringCriterion {
|
|||
super(countryCriterionOption);
|
||||
}
|
||||
|
||||
public getLabelValue(intl: IntlShape) {
|
||||
protected getLabelValue(intl: IntlShape) {
|
||||
if (
|
||||
this.modifier === CriterionModifier.Equals ||
|
||||
this.modifier === CriterionModifier.NotEquals
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
CriterionType,
|
||||
IHierarchicalLabelValue,
|
||||
ILabeledId,
|
||||
ILabeledValue,
|
||||
INumberValue,
|
||||
IOptionType,
|
||||
IStashIDValue,
|
||||
|
|
@ -39,6 +38,11 @@ export type CriterionValue =
|
|||
| ITimestampValue
|
||||
| IPhashDistanceValue;
|
||||
|
||||
export interface IEncodedCriterion<T extends CriterionValue> {
|
||||
modifier: CriterionModifier;
|
||||
value: T | undefined;
|
||||
}
|
||||
|
||||
const modifierMessageIDs = {
|
||||
[CriterionModifier.Equals]: "criterion_modifier.equals",
|
||||
[CriterionModifier.NotEquals]: "criterion_modifier.not_equals",
|
||||
|
|
@ -57,15 +61,15 @@ const modifierMessageIDs = {
|
|||
|
||||
// V = criterion value type
|
||||
export abstract class Criterion<V extends CriterionValue> {
|
||||
public static getModifierOption(
|
||||
modifier: CriterionModifier = CriterionModifier.Equals
|
||||
): ILabeledValue {
|
||||
const messageID = modifierMessageIDs[modifier];
|
||||
return { value: modifier, label: messageID };
|
||||
}
|
||||
|
||||
public criterionOption: CriterionOption;
|
||||
public modifier: CriterionModifier;
|
||||
|
||||
protected _modifier!: CriterionModifier;
|
||||
public get modifier(): CriterionModifier {
|
||||
return this._modifier;
|
||||
}
|
||||
public set modifier(value: CriterionModifier) {
|
||||
this._modifier = value;
|
||||
}
|
||||
|
||||
protected _value!: V;
|
||||
public get value(): V {
|
||||
|
|
@ -79,7 +83,7 @@ export abstract class Criterion<V extends CriterionValue> {
|
|||
return true;
|
||||
}
|
||||
|
||||
public abstract getLabelValue(intl: IntlShape): string;
|
||||
protected abstract getLabelValue(intl: IntlShape): string;
|
||||
|
||||
constructor(type: CriterionOption, value: V) {
|
||||
this.criterionOption = type;
|
||||
|
|
@ -140,8 +144,11 @@ export abstract class Criterion<V extends CriterionValue> {
|
|||
return JSON.stringify(encodedCriterion);
|
||||
}
|
||||
|
||||
public setValueFromQueryString(v: V) {
|
||||
this.value = v;
|
||||
public setFromEncodedCriterion(encodedCriterion: IEncodedCriterion<V>) {
|
||||
if (encodedCriterion.value !== undefined) {
|
||||
this.value = encodedCriterion.value;
|
||||
}
|
||||
this.modifier = encodedCriterion.modifier;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
@ -174,7 +181,7 @@ export class CriterionOption {
|
|||
public readonly messageID: string;
|
||||
public readonly type: CriterionType;
|
||||
public readonly parameterName: string;
|
||||
public readonly modifierOptions: ILabeledValue[];
|
||||
public readonly modifierOptions: CriterionModifier[];
|
||||
public readonly defaultModifier: CriterionModifier;
|
||||
public readonly options: Option[] | undefined;
|
||||
public readonly inputType: InputType;
|
||||
|
|
@ -183,9 +190,7 @@ export class CriterionOption {
|
|||
this.messageID = options.messageID;
|
||||
this.type = options.type;
|
||||
this.parameterName = options.parameterName ?? options.type;
|
||||
this.modifierOptions = (options.modifierOptions ?? []).map((o) =>
|
||||
Criterion.getModifierOption(o)
|
||||
);
|
||||
this.modifierOptions = options.modifierOptions ?? [];
|
||||
this.defaultModifier = options.defaultModifier ?? CriterionModifier.Equals;
|
||||
this.options = options.options;
|
||||
this.inputType = options.inputType;
|
||||
|
|
@ -237,7 +242,7 @@ export class StringCriterion extends Criterion<string> {
|
|||
super(type, "");
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape) {
|
||||
protected getLabelValue(_intl: IntlShape) {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +260,7 @@ export class MultiStringCriterion extends Criterion<string[]> {
|
|||
super(type, []);
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape) {
|
||||
protected getLabelValue(_intl: IntlShape) {
|
||||
return this.value.join(", ");
|
||||
}
|
||||
|
||||
|
|
@ -437,7 +442,7 @@ export class NumberCriterion extends Criterion<INumberValue> {
|
|||
};
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape) {
|
||||
protected getLabelValue(_intl: IntlShape) {
|
||||
const { value, value2 } = this.value;
|
||||
if (
|
||||
this.modifier === CriterionModifier.Between ||
|
||||
|
|
@ -509,7 +514,7 @@ export class ILabeledIdCriterionOption extends CriterionOption {
|
|||
}
|
||||
|
||||
export class ILabeledIdCriterion extends Criterion<ILabeledId[]> {
|
||||
public getLabelValue(_intl: IntlShape): string {
|
||||
protected getLabelValue(_intl: IntlShape): string {
|
||||
return this.value.map((v) => v.label).join(", ");
|
||||
}
|
||||
|
||||
|
|
@ -547,15 +552,53 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||
super(type, value);
|
||||
}
|
||||
|
||||
public setValueFromQueryString(v: IHierarchicalLabelValue) {
|
||||
override get modifier(): CriterionModifier {
|
||||
return this._modifier;
|
||||
}
|
||||
override set modifier(value: CriterionModifier) {
|
||||
this._modifier = value;
|
||||
|
||||
// excluded only makes sense for includes and includes all
|
||||
// so reset it for other modifiers
|
||||
if (
|
||||
value !== CriterionModifier.Includes &&
|
||||
value !== CriterionModifier.IncludesAll
|
||||
) {
|
||||
this.value.excluded = [];
|
||||
}
|
||||
}
|
||||
|
||||
public setFromEncodedCriterion(
|
||||
encodedCriterion: IEncodedCriterion<IHierarchicalLabelValue>
|
||||
) {
|
||||
const { modifier, value } = encodedCriterion;
|
||||
|
||||
if (value !== undefined) {
|
||||
this.value = {
|
||||
items: v.items || [],
|
||||
excluded: v.excluded || [],
|
||||
depth: v.depth || 0,
|
||||
items: value.items || [],
|
||||
excluded: value.excluded || [],
|
||||
depth: value.depth || 0,
|
||||
};
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape): string {
|
||||
// if the previous modifier was excludes, replace it with the equivalent includes criterion
|
||||
// this is what is done on the backend
|
||||
// only replace if excludes is not a valid modifierOption
|
||||
if (
|
||||
modifier === CriterionModifier.Excludes &&
|
||||
this.criterionOption.modifierOptions.find(
|
||||
(m) => m === CriterionModifier.Excludes
|
||||
) === undefined
|
||||
) {
|
||||
this.modifier = CriterionModifier.Includes;
|
||||
this.value.excluded = [...this.value.excluded, ...this.value.items];
|
||||
this.value.items = [];
|
||||
} else {
|
||||
this.modifier = modifier;
|
||||
}
|
||||
}
|
||||
|
||||
protected getLabelValue(_intl: IntlShape): string {
|
||||
const labels = (this.value.items ?? []).map((v) => v.label).join(", ");
|
||||
|
||||
if (this.value.depth === 0) {
|
||||
|
|
@ -586,8 +629,7 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||
public isValid(): boolean {
|
||||
if (
|
||||
this.modifier === CriterionModifier.IsNull ||
|
||||
this.modifier === CriterionModifier.NotNull ||
|
||||
this.modifier === CriterionModifier.Equals
|
||||
this.modifier === CriterionModifier.NotNull
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -599,23 +641,34 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||
}
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
const modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
let id = "criterion_modifier.format_string";
|
||||
let modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
let valueString = "";
|
||||
let excludedString = "";
|
||||
|
||||
if (
|
||||
this.modifier !== CriterionModifier.IsNull &&
|
||||
this.modifier !== CriterionModifier.NotNull
|
||||
) {
|
||||
valueString = this.value.items.map((v) => v.label).join(", ");
|
||||
}
|
||||
|
||||
let id = "criterion_modifier.format_string";
|
||||
let excludedString = "";
|
||||
|
||||
if (this.value.excluded && this.value.excluded.length > 0) {
|
||||
if (this.value.items.length === 0) {
|
||||
modifierString = Criterion.getModifierLabel(
|
||||
intl,
|
||||
CriterionModifier.Excludes
|
||||
);
|
||||
valueString = this.value.excluded.map((v) => v.label).join(", ");
|
||||
} else {
|
||||
id = "criterion_modifier.format_string_excludes";
|
||||
excludedString = this.value.excluded.map((v) => v.label).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.value.depth !== 0) {
|
||||
id += "_depth";
|
||||
}
|
||||
}
|
||||
|
||||
return intl.formatMessage(
|
||||
{ id },
|
||||
|
|
@ -624,6 +677,7 @@ export class IHierarchicalLabeledIdCriterion extends Criterion<IHierarchicalLabe
|
|||
modifierString,
|
||||
valueString,
|
||||
excludedString,
|
||||
depth: this.value.depth,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -669,7 +723,7 @@ export class DurationCriterion extends Criterion<INumberValue> {
|
|||
};
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape) {
|
||||
protected getLabelValue(_intl: IntlShape) {
|
||||
return this.modifier === CriterionModifier.Between ||
|
||||
this.modifier === CriterionModifier.NotBetween
|
||||
? `${DurationUtils.secondsToString(
|
||||
|
|
@ -764,7 +818,7 @@ export class DateCriterion extends Criterion<IDateValue> {
|
|||
};
|
||||
}
|
||||
|
||||
public getLabelValue() {
|
||||
protected getLabelValue() {
|
||||
const { value } = this.value;
|
||||
return this.modifier === CriterionModifier.Between ||
|
||||
this.modifier === CriterionModifier.NotBetween
|
||||
|
|
@ -849,7 +903,7 @@ export class TimestampCriterion extends Criterion<ITimestampValue> {
|
|||
};
|
||||
}
|
||||
|
||||
public getLabelValue() {
|
||||
protected getLabelValue() {
|
||||
const { value } = this.value;
|
||||
return this.modifier === CriterionModifier.Between ||
|
||||
this.modifier === CriterionModifier.NotBetween
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { CriterionOption, StringCriterion, Option } from "./criterion";
|
|||
|
||||
export class IsMissingCriterion extends StringCriterion {
|
||||
public modifierOptions = [];
|
||||
public modifier = CriterionModifier.Equals;
|
||||
|
||||
protected toCriterionInput(): string {
|
||||
return this.value;
|
||||
|
|
@ -23,6 +22,7 @@ class IsMissingCriterionOptionClass extends CriterionOption {
|
|||
type: value,
|
||||
parameterName,
|
||||
options,
|
||||
defaultModifier: CriterionModifier.Equals,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ export class NoneCriterion extends Criterion<string> {
|
|||
super(NoneCriterionOption, "none");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
public getLabelValue(): string {
|
||||
protected getLabelValue(): string {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import {
|
|||
MultiCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import { ILabeledId, ILabeledValueListValue } from "../types";
|
||||
import { Criterion, CriterionOption } from "./criterion";
|
||||
import { Criterion, CriterionOption, IEncodedCriterion } from "./criterion";
|
||||
|
||||
const modifierOptions = [
|
||||
CriterionModifier.IncludesAll,
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.Equals,
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
];
|
||||
|
||||
const defaultModifier = CriterionModifier.IncludesAll;
|
||||
|
|
@ -28,20 +30,50 @@ export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
|||
super(PerformersCriterionOption, { items: [], excluded: [] });
|
||||
}
|
||||
|
||||
public setValueFromQueryString(v: ILabeledId[] | ILabeledValueListValue) {
|
||||
// #3619 - the format of performer value was changed from an array
|
||||
// to an object. Check for both formats.
|
||||
if (Array.isArray(v)) {
|
||||
this.value = { items: v, excluded: [] };
|
||||
} else {
|
||||
this.value = {
|
||||
items: v.items || [],
|
||||
excluded: v.excluded || [],
|
||||
};
|
||||
override get modifier(): CriterionModifier {
|
||||
return this._modifier;
|
||||
}
|
||||
override set modifier(value: CriterionModifier) {
|
||||
this._modifier = value;
|
||||
|
||||
// excluded only makes sense for includes and includes all
|
||||
// reset it for other modifiers
|
||||
if (
|
||||
value !== CriterionModifier.Includes &&
|
||||
value !== CriterionModifier.IncludesAll
|
||||
) {
|
||||
this.value.excluded = [];
|
||||
}
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape): string {
|
||||
public setFromEncodedCriterion(
|
||||
encodedCriterion: IEncodedCriterion<ILabeledId[] | ILabeledValueListValue>
|
||||
) {
|
||||
const { modifier, value } = encodedCriterion;
|
||||
|
||||
// #3619 - the format of performer value was changed from an array
|
||||
// to an object. Check for both formats.
|
||||
if (Array.isArray(value)) {
|
||||
this.value = { items: value, excluded: [] };
|
||||
} else if (value !== undefined) {
|
||||
this.value = {
|
||||
items: value.items || [],
|
||||
excluded: value.excluded || [],
|
||||
};
|
||||
}
|
||||
|
||||
// if the previous modifier was excludes, replace it with the equivalent includes criterion
|
||||
// this is what is done on the backend
|
||||
if (modifier === CriterionModifier.Excludes) {
|
||||
this.modifier = CriterionModifier.Includes;
|
||||
this.value.excluded = [...this.value.excluded, ...this.value.items];
|
||||
this.value.items = [];
|
||||
} else {
|
||||
this.modifier = modifier;
|
||||
}
|
||||
}
|
||||
|
||||
protected getLabelValue(_intl: IntlShape): string {
|
||||
return this.value.items.map((v) => v.label).join(", ");
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +92,7 @@ export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
|||
public isValid(): boolean {
|
||||
if (
|
||||
this.modifier === CriterionModifier.IsNull ||
|
||||
this.modifier === CriterionModifier.NotNull ||
|
||||
this.modifier === CriterionModifier.Equals
|
||||
this.modifier === CriterionModifier.NotNull
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -73,23 +104,30 @@ export class PerformersCriterion extends Criterion<ILabeledValueListValue> {
|
|||
}
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
const modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
let id = "criterion_modifier.format_string";
|
||||
let modifierString = Criterion.getModifierLabel(intl, this.modifier);
|
||||
let valueString = "";
|
||||
let excludedString = "";
|
||||
|
||||
if (
|
||||
this.modifier !== CriterionModifier.IsNull &&
|
||||
this.modifier !== CriterionModifier.NotNull
|
||||
) {
|
||||
valueString = this.value.items.map((v) => v.label).join(", ");
|
||||
}
|
||||
|
||||
let id = "criterion_modifier.format_string";
|
||||
let excludedString = "";
|
||||
|
||||
if (this.value.excluded && this.value.excluded.length > 0) {
|
||||
if (this.value.items.length === 0) {
|
||||
modifierString = Criterion.getModifierLabel(
|
||||
intl,
|
||||
CriterionModifier.Excludes
|
||||
);
|
||||
valueString = this.value.excluded.map((v) => v.label).join(", ");
|
||||
} else {
|
||||
id = "criterion_modifier.format_string_excludes";
|
||||
excludedString = this.value.excluded.map((v) => v.label).join(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intl.formatMessage(
|
||||
{ id },
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class PhashCriterion extends Criterion<IPhashDistanceValue> {
|
|||
super(PhashCriterionOption, { value: "", distance: 0 });
|
||||
}
|
||||
|
||||
public getLabelValue() {
|
||||
protected getLabelValue() {
|
||||
const { value, distance } = this.value;
|
||||
if (
|
||||
(this.modifier === CriterionModifier.Equals ||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class RatingCriterion extends Criterion<INumberValue> {
|
|||
};
|
||||
}
|
||||
|
||||
public getLabelValue() {
|
||||
protected getLabelValue() {
|
||||
const { value, value2 } = this.value;
|
||||
if (
|
||||
this.modifier === CriterionModifier.Between ||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export class StashIDCriterion extends Criterion<IStashIDValue> {
|
|||
);
|
||||
}
|
||||
|
||||
public getLabelValue(_intl: IntlShape) {
|
||||
protected getLabelValue(_intl: IntlShape) {
|
||||
let ret = this.value.stashID;
|
||||
if (this.value.endpoint) {
|
||||
ret += " (" + this.value.endpoint + ")";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import {
|
|||
ILabeledIdCriterionOption,
|
||||
} from "./criterion";
|
||||
|
||||
const modifierOptions = [CriterionModifier.Includes];
|
||||
const modifierOptions = [
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
];
|
||||
|
||||
const defaultModifier = CriterionModifier.Includes;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +1,57 @@
|
|||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { CriterionType } from "../types";
|
||||
import { CriterionOption, IHierarchicalLabeledIdCriterion } from "./criterion";
|
||||
|
||||
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}
|
||||
|
||||
const tagsModifierOptions = [
|
||||
CriterionModifier.Includes,
|
||||
const modifierOptions = [
|
||||
CriterionModifier.IncludesAll,
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.Equals,
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
];
|
||||
|
||||
const withoutEqualsModifierOptions = [
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.IncludesAll,
|
||||
CriterionModifier.Includes,
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
];
|
||||
|
||||
class tagsCriterionOption extends CriterionOption {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
parameterName: string,
|
||||
modifierOptions: CriterionModifier[]
|
||||
) {
|
||||
let defaultModifier = CriterionModifier.IncludesAll;
|
||||
const defaultModifier = CriterionModifier.IncludesAll;
|
||||
|
||||
super({
|
||||
messageID,
|
||||
type: value,
|
||||
parameterName,
|
||||
export const TagsCriterionOption = new CriterionOption({
|
||||
messageID: "tags",
|
||||
type: "tags",
|
||||
parameterName: "tags",
|
||||
modifierOptions,
|
||||
defaultModifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
export const SceneTagsCriterionOption = new CriterionOption({
|
||||
messageID: "sceneTags",
|
||||
type: "sceneTags",
|
||||
parameterName: "scene_tags",
|
||||
modifierOptions,
|
||||
defaultModifier,
|
||||
});
|
||||
export const PerformerTagsCriterionOption = new CriterionOption({
|
||||
messageID: "performerTags",
|
||||
type: "performerTags",
|
||||
parameterName: "performer_tags",
|
||||
modifierOptions: withoutEqualsModifierOptions,
|
||||
defaultModifier,
|
||||
});
|
||||
export const ParentTagsCriterionOption = new CriterionOption({
|
||||
messageID: "parent_tags",
|
||||
type: "parentTags",
|
||||
parameterName: "parents",
|
||||
modifierOptions: withoutEqualsModifierOptions,
|
||||
defaultModifier,
|
||||
});
|
||||
export const ChildTagsCriterionOption = new CriterionOption({
|
||||
messageID: "sub_tags",
|
||||
type: "childTags",
|
||||
parameterName: "children",
|
||||
modifierOptions: withoutEqualsModifierOptions,
|
||||
defaultModifier,
|
||||
});
|
||||
|
||||
export const TagsCriterionOption = new tagsCriterionOption(
|
||||
"tags",
|
||||
"tags",
|
||||
"tags",
|
||||
tagsModifierOptions
|
||||
);
|
||||
export const SceneTagsCriterionOption = new tagsCriterionOption(
|
||||
"sceneTags",
|
||||
"sceneTags",
|
||||
"scene_tags",
|
||||
tagsModifierOptions
|
||||
);
|
||||
export const PerformerTagsCriterionOption = new tagsCriterionOption(
|
||||
"performerTags",
|
||||
"performerTags",
|
||||
"performer_tags",
|
||||
withoutEqualsModifierOptions
|
||||
);
|
||||
export const ParentTagsCriterionOption = new tagsCriterionOption(
|
||||
"parent_tags",
|
||||
"parentTags",
|
||||
"parents",
|
||||
withoutEqualsModifierOptions
|
||||
);
|
||||
export const ChildTagsCriterionOption = new tagsCriterionOption(
|
||||
"sub_tags",
|
||||
"childTags",
|
||||
"children",
|
||||
withoutEqualsModifierOptions
|
||||
);
|
||||
export class TagsCriterion extends IHierarchicalLabeledIdCriterion {}
|
||||
|
|
|
|||
|
|
@ -130,10 +130,7 @@ export class ListFilterModel {
|
|||
const criterion = makeCriteria(this.config, encodedCriterion.type);
|
||||
// it's possible that we have unsupported criteria. Just skip if so.
|
||||
if (criterion) {
|
||||
if (encodedCriterion.value !== undefined) {
|
||||
criterion.setValueFromQueryString(encodedCriterion.value);
|
||||
}
|
||||
criterion.modifier = encodedCriterion.modifier;
|
||||
criterion.setFromEncodedCriterion(encodedCriterion);
|
||||
this.criteria.push(criterion);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue