Filter improvement exploration

Changed the rating filter to allow for more than just an equality check.  This progresses #29.
This commit is contained in:
Stash Dev
2019-03-24 15:11:58 -07:00
parent c1f1a6ccff
commit b1db98bd1f
22 changed files with 339 additions and 54 deletions

File diff suppressed because one or more lines are too long

View File

@@ -10858,6 +10858,35 @@ func (e *executableSchema) GenerateMetadataInputMiddleware(ctx context.Context,
return obj, nil
}
func UnmarshalIntCriterionInput(v interface{}) (IntCriterionInput, error) {
var it IntCriterionInput
var asMap = v.(map[string]interface{})
for k, v := range asMap {
switch k {
case "value":
var err error
it.Value, err = graphql.UnmarshalInt(v)
if err != nil {
return it, err
}
case "modifier":
var err error
err = (&it.Modifier).UnmarshalGQL(v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (e *executableSchema) IntCriterionInputMiddleware(ctx context.Context, obj *IntCriterionInput) (*IntCriterionInput, error) {
return obj, nil
}
func UnmarshalPerformerCreateInput(v interface{}) (PerformerCreateInput, error) {
var it PerformerCreateInput
var asMap = v.(map[string]interface{})
@@ -11303,9 +11332,9 @@ func UnmarshalSceneFilterType(v interface{}) (SceneFilterType, error) {
switch k {
case "rating":
var err error
var ptr1 int
var ptr1 IntCriterionInput
if v != nil {
ptr1, err = graphql.UnmarshalInt(v)
ptr1, err = UnmarshalIntCriterionInput(v)
it.Rating = &ptr1
}
@@ -11392,6 +11421,14 @@ func UnmarshalSceneFilterType(v interface{}) (SceneFilterType, error) {
func (e *executableSchema) SceneFilterTypeMiddleware(ctx context.Context, obj *SceneFilterType) (*SceneFilterType, error) {
if obj.Rating != nil {
var err error
obj.Rating, err = e.IntCriterionInputMiddleware(ctx, obj.Rating)
if err != nil {
return nil, err
}
}
return obj, nil
}
@@ -12301,7 +12338,7 @@ input SceneMarkerFilterType {
input SceneFilterType {
"""Filter by rating"""
rating: Int
rating: IntCriterionInput
"""Filter by resolution"""
resolution: ResolutionEnum
"""Filter to only include scenes which have markers. ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `"""
@@ -12316,6 +12353,28 @@ input SceneFilterType {
performer_id: ID
}
enum CriterionModifier {
"""="""
EQUALS,
"""!="""
NOT_EQUALS,
""">"""
GREATER_THAN,
"""<"""
LESS_THAN,
"""IS NULL"""
IS_NULL,
"""IS NOT NULL"""
NOT_NULL,
INCLUDES,
EXCLUDES,
}
input IntCriterionInput {
value: Int!
modifier: CriterionModifier!
}
#######################################
# Config
#######################################

View File

@@ -77,6 +77,11 @@ type GenerateMetadataInput struct {
Transcodes bool `json:"transcodes"`
}
type IntCriterionInput struct {
Value int `json:"value"`
Modifier CriterionModifier `json:"modifier"`
}
type MarkerStringsResultType struct {
Count int `json:"count"`
ID string `json:"id"`
@@ -144,7 +149,7 @@ type SceneFileType struct {
type SceneFilterType struct {
// Filter by rating
Rating *int `json:"rating"`
Rating *IntCriterionInput `json:"rating"`
// Filter by resolution
Resolution *ResolutionEnum `json:"resolution"`
// Filter to only include scenes which have markers. `true` or `false`
@@ -270,6 +275,65 @@ type TagUpdateInput struct {
Name string `json:"name"`
}
type CriterionModifier string
const (
// =
CriterionModifierEquals CriterionModifier = "EQUALS"
// !=
CriterionModifierNotEquals CriterionModifier = "NOT_EQUALS"
// >
CriterionModifierGreaterThan CriterionModifier = "GREATER_THAN"
// <
CriterionModifierLessThan CriterionModifier = "LESS_THAN"
// IS NULL
CriterionModifierIsNull CriterionModifier = "IS_NULL"
// IS NOT NULL
CriterionModifierNotNull CriterionModifier = "NOT_NULL"
CriterionModifierIncludes CriterionModifier = "INCLUDES"
CriterionModifierExcludes CriterionModifier = "EXCLUDES"
)
var AllCriterionModifier = []CriterionModifier{
CriterionModifierEquals,
CriterionModifierNotEquals,
CriterionModifierGreaterThan,
CriterionModifierLessThan,
CriterionModifierIsNull,
CriterionModifierNotNull,
CriterionModifierIncludes,
CriterionModifierExcludes,
}
func (e CriterionModifier) IsValid() bool {
switch e {
case CriterionModifierEquals, CriterionModifierNotEquals, CriterionModifierGreaterThan, CriterionModifierLessThan, CriterionModifierIsNull, CriterionModifierNotNull, CriterionModifierIncludes, CriterionModifierExcludes:
return true
}
return false
}
func (e CriterionModifier) String() string {
return string(e)
}
func (e *CriterionModifier) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = CriterionModifier(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid CriterionModifier", str)
}
return nil
}
func (e CriterionModifier) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type ResolutionEnum string
const (

View File

@@ -164,8 +164,11 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
}
if rating := sceneFilter.Rating; rating != nil {
whereClauses = append(whereClauses, "rating = ?")
args = append(args, *sceneFilter.Rating)
clause, count := getIntCriterionWhereClause("rating", *sceneFilter.Rating)
whereClauses = append(whereClauses, clause)
if count == 1 {
args = append(args, sceneFilter.Rating.Value)
}
}
if resolutionFilter := sceneFilter.Resolution; resolutionFilter != nil {

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/logger"
"reflect"
"strconv"
"strings"
@@ -104,6 +105,48 @@ func getInBinding(length int) string {
return "(" + bindings + ")"
}
func getCriterionModifierBinding(criterionModifier CriterionModifier, value interface{}) (string, int) {
var length int
switch x := value.(type) {
case []string:
length = len(x)
case []int:
length = len(x)
default:
length = 1
logger.Debugf("unsupported type: %T\n", x)
}
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
switch modifier {
case "EQUALS":
return "= ?", 1
case "NOT_EQUALS":
return "!= ?", 1
case "GREATER_THAN":
return "> ?", 1
case "LESS_THAN":
return "< ?", 1
case "IS_NULL":
return "IS NULL", 0
case "NOT_NULL":
return "IS NOT NULL", 0
case "INCLUDES":
return "IN "+getInBinding(length), length // TODO?
case "EXCLUDES":
return "NOT IN "+getInBinding(length), length // TODO?
default:
logger.Errorf("todo")
return "= ?", 1 // TODO
}
}
return "= ?", 1 // TODO
}
func getIntCriterionWhereClause(column string, input IntCriterionInput) (string, int) {
binding, count := getCriterionModifierBinding(input.Modifier, input.Value)
return column+" "+binding, count
}
func runIdsQuery(query string, args []interface{}) ([]int, error) {
var result []struct {
Int int `db:"id"`

View File

@@ -357,7 +357,7 @@ input SceneMarkerFilterType {
input SceneFilterType {
"""Filter by rating"""
rating: Int
rating: IntCriterionInput
"""Filter by resolution"""
resolution: ResolutionEnum
"""Filter to only include scenes which have markers. `true` or `false`"""
@@ -372,6 +372,28 @@ input SceneFilterType {
performer_id: ID
}
enum CriterionModifier {
"""="""
EQUALS,
"""!="""
NOT_EQUALS,
""">"""
GREATER_THAN,
"""<"""
LESS_THAN,
"""IS NULL"""
IS_NULL,
"""IS NOT NULL"""
NOT_NULL,
INCLUDES,
EXCLUDES,
}
input IntCriterionInput {
value: Int!
modifier: CriterionModifier!
}
#######################################
# Config
#######################################

View File

@@ -8,6 +8,7 @@ import {
import _ from "lodash";
import React, { FunctionComponent, useEffect, useRef, useState } from "react";
import { isArray } from "util";
import { CriterionModifier } from "../../core/generated-graphql";
import { Criterion, CriterionType } from "../../models/list-filter/criteria/criterion";
import { NoneCriterion } from "../../models/list-filter/criteria/none";
import { PerformersCriterion } from "../../models/list-filter/criteria/performers";
@@ -18,7 +19,7 @@ import { ListFilterModel } from "../../models/list-filter/filter";
import { FilterMultiSelect } from "../select/FilterMultiSelect";
interface IAddFilterProps {
onAddCriterion: (criterion: Criterion) => void;
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
onCancel: () => void;
filter: ListFilterModel;
editingCriterion?: Criterion;
@@ -43,6 +44,12 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
setCriterion(newCriterion);
}
function onChangedModifierSelect(event: React.ChangeEvent<HTMLSelectElement>) {
const newCriterion = _.cloneDeep(criterion);
newCriterion.modifier = event.target.value as any;
setCriterion(newCriterion);
}
function onChangedSingleSelect(event: React.ChangeEvent<HTMLSelectElement>) {
const newCriterion = _.cloneDeep(criterion);
newCriterion.value = event.target.value;
@@ -54,7 +61,8 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
const value = singleValueSelect.current.props.defaultValue;
if (value === undefined || value === "" || typeof value === "number") { criterion.value = criterion.options[0]; }
}
props.onAddCriterion(criterion);
const oldId = !!props.editingCriterion ? props.editingCriterion.getId() : undefined;
props.onAddCriterion(criterion, oldId);
onToggle();
}
@@ -69,7 +77,25 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
const maybeRenderFilterPopoverContents = () => {
if (criterion.type === "none") { return; }
function renderModifier() {
if (criterion.modifierOptions.length === 0) { return; }
return (
<div>
<HTMLSelect
options={criterion.modifierOptions}
onChange={onChangedModifierSelect}
defaultValue={criterion.modifier}
/>
</div>
);
}
function renderSelect() {
// Hide the value select if the modifier is "IsNull" or "NotNull"
if (criterion.modifier === CriterionModifier.IsNull || criterion.modifier === CriterionModifier.NotNull) {
return;
}
if (isArray(criterion.value)) {
let type: "performers" | "studios" | "tags" | "" = "";
if (criterion instanceof PerformersCriterion) {
@@ -103,7 +129,12 @@ export const AddFilter: FunctionComponent<IAddFilterProps> = (props: IAddFilterP
);
}
}
return <FormGroup>{renderSelect()}</FormGroup>;
return (
<FormGroup>
{renderModifier()}
{renderSelect()}
</FormGroup>
);
};
function maybeRenderFilterSelect() {

View File

@@ -23,7 +23,7 @@ interface IListFilterProps {
onChangeSortDirection: (sortDirection: "asc" | "desc") => void;
onChangeSortBy: (sortBy: string) => void;
onChangeDisplayMode: (displayMode: DisplayMode) => void;
onAddCriterion: (criterion: Criterion) => void;
onAddCriterion: (criterion: Criterion, oldId?: string) => void;
onRemoveCriterion: (criterion: Criterion) => void;
filter: ListFilterModel;
}
@@ -67,8 +67,8 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
props.onChangeDisplayMode(displayMode);
}
function onAddCriterion(criterion: Criterion) {
props.onAddCriterion(criterion);
function onAddCriterion(criterion: Criterion, oldId?: string) {
props.onAddCriterion(criterion, oldId);
}
function onCancelAddCriterion() {
@@ -122,7 +122,7 @@ export const ListFilter: FunctionComponent<IListFilterProps> = (props: IListFilt
function renderFilterTags() {
return props.filter.criteria.map((criterion) => (
<Tag
key={criterion.type}
key={criterion.getId()}
className="tag-item"
itemID={criterion.getId()}
interactive={true}

View File

@@ -1,10 +1,10 @@
/* tslint:disable */
// Generated in 2019-03-24T09:16:39-07:00
// Generated in 2019-03-24T13:29:34-07:00
export type Maybe<T> = T | undefined;
export interface SceneFilterType {
/** Filter by rating */
rating?: Maybe<number>;
rating?: Maybe<IntCriterionInput>;
/** Filter by resolution */
resolution?: Maybe<ResolutionEnum>;
/** Filter to only include scenes which have markers. `true` or `false` */
@@ -19,6 +19,12 @@ export interface SceneFilterType {
performer_id?: Maybe<string>;
}
export interface IntCriterionInput {
value: number;
modifier: CriterionModifier;
}
export interface FindFilterType {
q?: Maybe<string>;
@@ -222,6 +228,17 @@ export interface ConfigGeneralInput {
generatedPath?: Maybe<string>;
}
export enum CriterionModifier {
Equals = "EQUALS",
NotEquals = "NOT_EQUALS",
GreaterThan = "GREATER_THAN",
LessThan = "LESS_THAN",
IsNull = "IS_NULL",
NotNull = "NOT_NULL",
Includes = "INCLUDES",
Excludes = "EXCLUDES"
}
export enum ResolutionEnum {
Low = "LOW",
Standard = "STANDARD",

View File

@@ -117,14 +117,26 @@ export class ListHook {
setFilter(newFilter);
}
function onAddCriterion(criterion: Criterion) {
function onAddCriterion(criterion: Criterion, oldId?: string) {
const newFilter = _.cloneDeep(filter);
const existingIndex = newFilter.criteria.findIndex((c) => c.getId() === criterion.getId());
// Find if we are editing an existing criteria, then modify that. Or create a new one.
const existingIndex = newFilter.criteria.findIndex((c) => {
// If we modified an existing criterion, then look for the old id.
const id = !!oldId ? oldId : criterion.getId();
return c.getId() === id;
});
if (existingIndex === -1) {
newFilter.criteria.push(criterion);
} else {
newFilter.criteria[existingIndex] = criterion;
}
// Remove duplicate modifiers
newFilter.criteria = newFilter.criteria.filter((obj, pos, arr) => {
return arr.map((mapObj: any) => mapObj.getId()).indexOf(obj.getId()) === pos;
});
newFilter.currentPage = 1;
setFilter(newFilter);
}

View File

@@ -1,5 +1,6 @@
import { isArray } from "util";
import { ILabeledId } from "../types";
import { CriterionModifier } from "../../../core/generated-graphql";
import { ILabeledId, ILabeledValue } from "../types";
export type CriterionType =
"none" |
@@ -13,15 +14,6 @@ export type CriterionType =
"performers" |
"studios";
export enum CriterionModifier {
Equals,
NotEquals,
GreaterThan,
LessThan,
Inclusive,
Exclusive,
}
export abstract class Criterion<Option = any, Value = any> {
public static getLabel(type: CriterionType = "none"): string {
switch (type) {
@@ -38,9 +30,23 @@ export abstract class Criterion<Option = any, Value = any> {
}
}
public static getModifierOption(modifier: CriterionModifier = CriterionModifier.Equals): ILabeledValue {
switch (modifier) {
case CriterionModifier.Equals: return {value: CriterionModifier.Equals, label: "Equals"};
case CriterionModifier.NotEquals: return {value: CriterionModifier.NotEquals, label: "Not Equals"};
case CriterionModifier.GreaterThan: return {value: CriterionModifier.GreaterThan, label: "Greater Than"};
case CriterionModifier.LessThan: return {value: CriterionModifier.LessThan, label: "Less Than"};
case CriterionModifier.IsNull: return {value: CriterionModifier.IsNull, label: "Is NULL"};
case CriterionModifier.NotNull: return {value: CriterionModifier.NotNull, label: "Not NULL"};
case CriterionModifier.Includes: return {value: CriterionModifier.Includes, label: "Includes"};
case CriterionModifier.Excludes: return {value: CriterionModifier.Excludes, label: "Excludes"};
}
}
public abstract type: CriterionType;
public abstract parameterName: string;
public abstract modifier: CriterionModifier;
public abstract modifierOptions: ILabeledValue[];
public abstract options: Option[];
public abstract value: Value;
@@ -51,13 +57,17 @@ export abstract class Criterion<Option = any, Value = any> {
case CriterionModifier.NotEquals: modifierString = "is not"; break;
case CriterionModifier.GreaterThan: modifierString = "is greater than"; break;
case CriterionModifier.LessThan: modifierString = "is less than"; break;
case CriterionModifier.Inclusive: modifierString = "includes"; break;
case CriterionModifier.Exclusive: modifierString = "exculdes"; break;
case CriterionModifier.IsNull: modifierString = "is null"; break;
case CriterionModifier.NotNull: modifierString = "is not null"; break;
case CriterionModifier.Includes: modifierString = "includes"; break;
case CriterionModifier.Excludes: modifierString = "exculdes"; break;
default: modifierString = "";
}
let valueString: string;
if (isArray(this.value) && this.value.length > 0) {
if (this.modifier === CriterionModifier.IsNull || this.modifier === CriterionModifier.NotNull) {
valueString = "";
} else if (isArray(this.value) && this.value.length > 0) {
let items = this.value;
if ((this.value as ILabeledId[])[0].label) {
items = this.value.map((item) => item.label) as any;

View File

@@ -1,6 +1,6 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -9,6 +9,7 @@ export class FavoriteCriterion extends Criterion<string, string> {
public type: CriterionType = "favorite";
public parameterName: string = "filter_favorites";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}

View File

@@ -1,6 +1,6 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -9,6 +9,7 @@ export class HasMarkersCriterion extends Criterion<string, string> {
public type: CriterionType = "hasMarkers";
public parameterName: string = "has_markers";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = [true.toString(), false.toString()];
public value: string = "";
}

View File

@@ -1,6 +1,6 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -9,6 +9,7 @@ export class IsMissingCriterion extends Criterion<string, string> {
public type: CriterionType = "isMissing";
public parameterName: string = "is_missing";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = ["title", "url", "date", "gallery", "studio", "performers"];
public value: string = "";
}

View File

@@ -1,6 +1,6 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -9,6 +9,7 @@ export class NoneCriterion extends Criterion<any, any> {
public type: CriterionType = "none";
public parameterName: string = "";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: any;
public value: any;
}

View File

@@ -1,7 +1,7 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { ILabeledId } from "../types";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -16,6 +16,7 @@ export class PerformersCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "performers";
public parameterName: string = "performers";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
}

View File

@@ -1,6 +1,6 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -9,6 +9,14 @@ export class RatingCriterion extends Criterion<number, number> { // TODO <number
public type: CriterionType = "rating";
public parameterName: string = "rating";
public modifier = CriterionModifier.Equals;
public modifierOptions = [
Criterion.getModifierOption(CriterionModifier.Equals),
Criterion.getModifierOption(CriterionModifier.NotEquals),
Criterion.getModifierOption(CriterionModifier.GreaterThan),
Criterion.getModifierOption(CriterionModifier.LessThan),
Criterion.getModifierOption(CriterionModifier.IsNull),
Criterion.getModifierOption(CriterionModifier.NotNull),
];
public options: number[] = [1, 2, 3, 4, 5];
public value: number = 0;
}

View File

@@ -1,6 +1,6 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -9,6 +9,7 @@ export class ResolutionCriterion extends Criterion<string, string> { // TODO <st
public type: CriterionType = "resolution";
public parameterName: string = "resolution";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: string[] = ["240p", "480p", "720p", "1080p", "4k"];
public value: string = "";
}

View File

@@ -1,7 +1,7 @@
import { CriterionModifier } from "../../../core/generated-graphql";
import { ILabeledId } from "../types";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -16,6 +16,7 @@ export class StudiosCriterion extends Criterion<IOptionType, ILabeledId[]> {
public type: CriterionType = "studios";
public parameterName: string = "studios";
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: IOptionType[] = [];
public value: ILabeledId[] = [];
}

View File

@@ -1,8 +1,8 @@
import * as GQL from "../../../core/generated-graphql";
import { CriterionModifier } from "../../../core/generated-graphql";
import { ILabeledId } from "../types";
import {
Criterion,
CriterionModifier,
CriterionType,
ICriterionOption,
} from "./criterion";
@@ -11,6 +11,7 @@ export class TagsCriterion extends Criterion<GQL.AllTagsForFilterAllTags, ILabel
public type: CriterionType;
public parameterName: string;
public modifier = CriterionModifier.Equals;
public modifierOptions = [];
public options: GQL.AllTagsForFilterAllTags[] = [];
public value: ILabeledId[] = [];

View File

@@ -157,6 +157,7 @@ export class ListFilterModel {
const encodedCriterion = JSON.parse(jsonString);
const criterion = makeCriteria(encodedCriterion.type);
criterion.value = encodedCriterion.value;
criterion.modifier = encodedCriterion.modifier;
this.criteria.push(criterion);
}
}
@@ -168,6 +169,7 @@ export class ListFilterModel {
const encodedCriterion: any = {};
encodedCriterion.type = criterion.type;
encodedCriterion.value = criterion.value;
encodedCriterion.modifier = criterion.modifier;
const jsonCriterion = JSON.stringify(encodedCriterion);
encodedCriteria.push(jsonCriterion);
});
@@ -200,7 +202,8 @@ export class ListFilterModel {
this.criteria.forEach((criterion) => {
switch (criterion.type) {
case "rating":
result.rating = (criterion as RatingCriterion).value;
const crit = criterion as RatingCriterion;
result.rating = { value: crit.value, modifier: crit.modifier };
break;
case "resolution": {
switch ((criterion as ResolutionCriterion).value) {

View File

@@ -16,3 +16,8 @@ export interface ILabeledId {
id: string;
label: string;
}
export interface ILabeledValue {
label: string;
value: string;
}