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:
DingDongSoLong4 2023-06-06 06:10:14 +02:00 committed by GitHub
parent de4237e626
commit 09df203bcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 344 additions and 216 deletions

View file

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

View file

@ -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}
/>
);
};

View file

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

View file

@ -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}
/>
);
};

View file

@ -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}
/>
);
};

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
});
}
}

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@ export class RatingCriterion extends Criterion<INumberValue> {
};
}
public getLabelValue() {
protected getLabelValue() {
const { value, value2 } = this.value;
if (
this.modifier === CriterionModifier.Between ||

View file

@ -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 + ")";

View file

@ -6,7 +6,11 @@ import {
ILabeledIdCriterionOption,
} from "./criterion";
const modifierOptions = [CriterionModifier.Includes];
const modifierOptions = [
CriterionModifier.Includes,
CriterionModifier.IsNull,
CriterionModifier.NotNull,
];
const defaultModifier = CriterionModifier.Includes;

View file

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

View file

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