mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Add (not) between modifiers for number criterion (#1559)
* Add (not) between modifiers for number criterion * Extract list filters into dedicated components Extract the filters from the AddFiltersDialog into custom components. This allows for further refactorring where components will be bound to criterions. * Add placeholders to number and duration criterions * Add backwards compatibility for saved filters Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -327,6 +327,10 @@ enum CriterionModifier {
|
|||||||
MATCHES_REGEX,
|
MATCHES_REGEX,
|
||||||
"""NOT MATCHES REGEX"""
|
"""NOT MATCHES REGEX"""
|
||||||
NOT_MATCHES_REGEX,
|
NOT_MATCHES_REGEX,
|
||||||
|
""">= AND <="""
|
||||||
|
BETWEEN,
|
||||||
|
"""< OR >"""
|
||||||
|
NOT_BETWEEN,
|
||||||
}
|
}
|
||||||
|
|
||||||
input StringCriterionInput {
|
input StringCriterionInput {
|
||||||
@@ -336,6 +340,7 @@ input StringCriterionInput {
|
|||||||
|
|
||||||
input IntCriterionInput {
|
input IntCriterionInput {
|
||||||
value: Int!
|
value: Int!
|
||||||
|
value2: Int
|
||||||
modifier: CriterionModifier!
|
modifier: CriterionModifier!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -368,13 +368,8 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite
|
|||||||
func intCriterionHandler(c *models.IntCriterionInput, column string) criterionHandlerFunc {
|
func intCriterionHandler(c *models.IntCriterionInput, column string) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
clause, count := getIntCriterionWhereClause(column, *c)
|
clause, args := getIntCriterionWhereClause(column, *c)
|
||||||
|
f.addWhere(clause, args...)
|
||||||
if count == 1 {
|
|
||||||
f.addWhere(clause, c.Value)
|
|
||||||
} else {
|
|
||||||
f.addWhere(clause)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,13 +490,9 @@ type countCriterionHandlerBuilder struct {
|
|||||||
func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
|
func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if criterion != nil {
|
if criterion != nil {
|
||||||
clause, count := getCountCriterionClause(m.primaryTable, m.joinTable, m.primaryFK, *criterion)
|
clause, args := getCountCriterionClause(m.primaryTable, m.joinTable, m.primaryFK, *criterion)
|
||||||
|
|
||||||
if count == 1 {
|
f.addWhere(clause, args...)
|
||||||
f.addWhere(clause, criterion.Value)
|
|
||||||
} else {
|
|
||||||
f.addWhere(clause)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package sqlite
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
@@ -356,25 +355,8 @@ func performerIsMissingCriterionHandler(qb *performerQueryBuilder, isMissing *st
|
|||||||
func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) criterionHandlerFunc {
|
func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if year != nil && year.Modifier.IsValid() {
|
if year != nil && year.Modifier.IsValid() {
|
||||||
yearStr := strconv.Itoa(year.Value)
|
clause, args := getIntCriterionWhereClause("cast(strftime('%Y', "+col+") as int)", *year)
|
||||||
startOfYear := yearStr + "-01-01"
|
f.addWhere(clause, args...)
|
||||||
endOfYear := yearStr + "-12-31"
|
|
||||||
|
|
||||||
switch year.Modifier {
|
|
||||||
case models.CriterionModifierEquals:
|
|
||||||
// between yyyy-01-01 and yyyy-12-31
|
|
||||||
f.addWhere(col+" >= ?", startOfYear)
|
|
||||||
f.addWhere(col+" <= ?", endOfYear)
|
|
||||||
case models.CriterionModifierNotEquals:
|
|
||||||
// outside of yyyy-01-01 to yyyy-12-31
|
|
||||||
f.addWhere(col+" < ? OR "+col+" > ?", startOfYear, endOfYear)
|
|
||||||
case models.CriterionModifierGreaterThan:
|
|
||||||
// > yyyy-12-31
|
|
||||||
f.addWhere(col+" > ?", endOfYear)
|
|
||||||
case models.CriterionModifierLessThan:
|
|
||||||
// < yyyy-01-01
|
|
||||||
f.addWhere(col+" < ?", startOfYear)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,22 +364,11 @@ func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) crit
|
|||||||
func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
|
func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if age != nil && age.Modifier.IsValid() {
|
if age != nil && age.Modifier.IsValid() {
|
||||||
var op string
|
clause, args := getIntCriterionWhereClause(
|
||||||
|
"cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int)",
|
||||||
switch age.Modifier {
|
*age,
|
||||||
case models.CriterionModifierEquals:
|
)
|
||||||
op = "=="
|
f.addWhere(clause, args...)
|
||||||
case models.CriterionModifierNotEquals:
|
|
||||||
op = "!="
|
|
||||||
case models.CriterionModifierGreaterThan:
|
|
||||||
op = ">"
|
|
||||||
case models.CriterionModifierLessThan:
|
|
||||||
op = "<"
|
|
||||||
}
|
|
||||||
|
|
||||||
if op != "" {
|
|
||||||
f.addWhere("cast(IFNULL(strftime('%Y.%m%d', performers.death_date), strftime('%Y.%m%d', 'now')) - strftime('%Y.%m%d', performers.birthdate) as int) "+op+" ?", age.Value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,11 +135,9 @@ func (qb *queryBuilder) addFilter(f *filterBuilder) {
|
|||||||
|
|
||||||
func (qb *queryBuilder) handleIntCriterionInput(c *models.IntCriterionInput, column string) {
|
func (qb *queryBuilder) handleIntCriterionInput(c *models.IntCriterionInput, column string) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
clause, count := getIntCriterionWhereClause(column, *c)
|
clause, args := getIntCriterionWhereClause(column, *c)
|
||||||
qb.addWhere(clause)
|
qb.addWhere(clause)
|
||||||
if count == 1 {
|
qb.addArg(args...)
|
||||||
qb.addArg(c.Value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +190,9 @@ func (qb *queryBuilder) handleStringCriterionInput(c *models.StringCriterionInpu
|
|||||||
|
|
||||||
func (qb *queryBuilder) handleCountCriterion(countFilter *models.IntCriterionInput, primaryTable, joinTable, primaryFK string) {
|
func (qb *queryBuilder) handleCountCriterion(countFilter *models.IntCriterionInput, primaryTable, joinTable, primaryFK string) {
|
||||||
if countFilter != nil {
|
if countFilter != nil {
|
||||||
clause, count := getCountCriterionClause(primaryTable, joinTable, primaryFK, *countFilter)
|
clause, args := getCountCriterionClause(primaryTable, joinTable, primaryFK, *countFilter)
|
||||||
|
|
||||||
if count == 1 {
|
|
||||||
qb.addArg(countFilter.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
qb.addWhere(clause)
|
qb.addWhere(clause)
|
||||||
|
qb.addArg(args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -461,40 +461,12 @@ func phashCriterionHandler(phashFilter *models.StringCriterionInput) criterionHa
|
|||||||
func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc {
|
func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if durationFilter != nil {
|
if durationFilter != nil {
|
||||||
clause, thisArgs := getDurationWhereClause(*durationFilter, column)
|
clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter)
|
||||||
f.addWhere(clause, thisArgs...)
|
f.addWhere(clause, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDurationWhereClause(durationFilter models.IntCriterionInput, column string) (string, []interface{}) {
|
|
||||||
// special case for duration. We accept duration as seconds as int but the
|
|
||||||
// field is floating point. Change the equals filter to return a range
|
|
||||||
// between x and x + 1
|
|
||||||
// likewise, not equals needs to be duration < x OR duration >= x
|
|
||||||
var clause string
|
|
||||||
args := []interface{}{}
|
|
||||||
|
|
||||||
value := durationFilter.Value
|
|
||||||
if durationFilter.Modifier == models.CriterionModifierEquals {
|
|
||||||
clause = fmt.Sprintf("%[1]s >= ? AND %[1]s < ?", column)
|
|
||||||
args = append(args, value)
|
|
||||||
args = append(args, value+1)
|
|
||||||
} else if durationFilter.Modifier == models.CriterionModifierNotEquals {
|
|
||||||
clause = fmt.Sprintf("(%[1]s < ? OR %[1]s >= ?)", column)
|
|
||||||
args = append(args, value)
|
|
||||||
args = append(args, value+1)
|
|
||||||
} else {
|
|
||||||
var count int
|
|
||||||
clause, count = getIntCriterionWhereClause(column, durationFilter)
|
|
||||||
if count == 1 {
|
|
||||||
args = append(args, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clause, args
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string) criterionHandlerFunc {
|
func resolutionCriterionHandler(resolution *models.ResolutionCriterionInput, heightColumn string, widthColumn string) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if resolution != nil && resolution.Value.IsValid() {
|
if resolution != nil && resolution.Value.IsValid() {
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func getCriterionModifierBinding(criterionModifier models.CriterionModifier, val
|
|||||||
}
|
}
|
||||||
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
||||||
switch modifier {
|
switch modifier {
|
||||||
case "EQUALS", "NOT_EQUALS", "GREATER_THAN", "LESS_THAN", "IS_NULL", "NOT_NULL":
|
case "EQUALS", "NOT_EQUALS", "GREATER_THAN", "LESS_THAN", "IS_NULL", "NOT_NULL", "BETWEEN", "NOT_BETWEEN":
|
||||||
return getSimpleCriterionClause(criterionModifier, "?")
|
return getSimpleCriterionClause(criterionModifier, "?")
|
||||||
case "INCLUDES":
|
case "INCLUDES":
|
||||||
return "IN " + getInBinding(length), length // TODO?
|
return "IN " + getInBinding(length), length // TODO?
|
||||||
@@ -189,6 +189,10 @@ func getSimpleCriterionClause(criterionModifier models.CriterionModifier, rhs st
|
|||||||
return "IS NULL", 0
|
return "IS NULL", 0
|
||||||
case "NOT_NULL":
|
case "NOT_NULL":
|
||||||
return "IS NOT NULL", 0
|
return "IS NOT NULL", 0
|
||||||
|
case "BETWEEN":
|
||||||
|
return "BETWEEN (" + rhs + ") AND (" + rhs + ")", 2
|
||||||
|
case "NOT_BETWEEN":
|
||||||
|
return "NOT BETWEEN (" + rhs + ") AND (" + rhs + ")", 2
|
||||||
default:
|
default:
|
||||||
logger.Errorf("todo")
|
logger.Errorf("todo")
|
||||||
return "= ?", 1 // TODO
|
return "= ?", 1 // TODO
|
||||||
@@ -198,9 +202,30 @@ func getSimpleCriterionClause(criterionModifier models.CriterionModifier, rhs st
|
|||||||
return "= ?", 1 // TODO
|
return "= ?", 1 // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, int) {
|
func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, []interface{}) {
|
||||||
binding, count := getCriterionModifierBinding(input.Modifier, input.Value)
|
binding, _ := getSimpleCriterionClause(input.Modifier, "?")
|
||||||
return column + " " + binding, count
|
var args []interface{}
|
||||||
|
|
||||||
|
switch input.Modifier {
|
||||||
|
case "EQUALS", "NOT_EQUALS":
|
||||||
|
args = []interface{}{input.Value}
|
||||||
|
break
|
||||||
|
case "LESS_THAN":
|
||||||
|
args = []interface{}{input.Value}
|
||||||
|
break
|
||||||
|
case "GREATER_THAN":
|
||||||
|
args = []interface{}{input.Value}
|
||||||
|
break
|
||||||
|
case "BETWEEN", "NOT_BETWEEN":
|
||||||
|
upper := 0
|
||||||
|
if input.Value2 != nil {
|
||||||
|
upper = *input.Value2
|
||||||
|
}
|
||||||
|
args = []interface{}{input.Value, upper}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return column + " " + binding, args
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns where clause and having clause
|
// returns where clause and having clause
|
||||||
@@ -226,7 +251,7 @@ func getMultiCriterionClause(primaryTable, foreignTable, joinTable, primaryFK, f
|
|||||||
return whereClause, havingClause
|
return whereClause, havingClause
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterion models.IntCriterionInput) (string, int) {
|
func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterion models.IntCriterionInput) (string, []interface{}) {
|
||||||
lhs := fmt.Sprintf("(SELECT COUNT(*) FROM %s s WHERE s.%s = %s.id)", joinTable, primaryFK, primaryTable)
|
lhs := fmt.Sprintf("(SELECT COUNT(*) FROM %s s WHERE s.%s = %s.id)", joinTable, primaryFK, primaryTable)
|
||||||
return getIntCriterionWhereClause(lhs, criterion)
|
return getIntCriterionWhereClause(lhs, criterion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,12 +359,7 @@ func tagSceneCountCriterionHandler(qb *tagQueryBuilder, sceneCount *models.IntCr
|
|||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if sceneCount != nil {
|
if sceneCount != nil {
|
||||||
f.addJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id")
|
f.addJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id")
|
||||||
clause, count := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
|
clause, args := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
|
||||||
|
|
||||||
args := []interface{}{}
|
|
||||||
if count == 1 {
|
|
||||||
args = append(args, sceneCount.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.addHaving(clause, args...)
|
f.addHaving(clause, args...)
|
||||||
}
|
}
|
||||||
@@ -375,12 +370,7 @@ func tagImageCountCriterionHandler(qb *tagQueryBuilder, imageCount *models.IntCr
|
|||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if imageCount != nil {
|
if imageCount != nil {
|
||||||
f.addJoin("images_tags", "", "images_tags.tag_id = tags.id")
|
f.addJoin("images_tags", "", "images_tags.tag_id = tags.id")
|
||||||
clause, count := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
|
clause, args := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
|
||||||
|
|
||||||
args := []interface{}{}
|
|
||||||
if count == 1 {
|
|
||||||
args = append(args, imageCount.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.addHaving(clause, args...)
|
f.addHaving(clause, args...)
|
||||||
}
|
}
|
||||||
@@ -391,12 +381,7 @@ func tagGalleryCountCriterionHandler(qb *tagQueryBuilder, galleryCount *models.I
|
|||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if galleryCount != nil {
|
if galleryCount != nil {
|
||||||
f.addJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id")
|
f.addJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id")
|
||||||
clause, count := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
|
clause, args := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
|
||||||
|
|
||||||
args := []interface{}{}
|
|
||||||
if count == 1 {
|
|
||||||
args = append(args, galleryCount.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.addHaving(clause, args...)
|
f.addHaving(clause, args...)
|
||||||
}
|
}
|
||||||
@@ -407,12 +392,7 @@ func tagPerformerCountCriterionHandler(qb *tagQueryBuilder, performerCount *mode
|
|||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if performerCount != nil {
|
if performerCount != nil {
|
||||||
f.addJoin("performers_tags", "", "performers_tags.tag_id = tags.id")
|
f.addJoin("performers_tags", "", "performers_tags.tag_id = tags.id")
|
||||||
clause, count := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
|
clause, args := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
|
||||||
|
|
||||||
args := []interface{}{}
|
|
||||||
if count == 1 {
|
|
||||||
args = append(args, performerCount.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.addHaving(clause, args...)
|
f.addHaving(clause, args...)
|
||||||
}
|
}
|
||||||
@@ -424,12 +404,7 @@ func tagMarkerCountCriterionHandler(qb *tagQueryBuilder, markerCount *models.Int
|
|||||||
if markerCount != nil {
|
if markerCount != nil {
|
||||||
f.addJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id")
|
f.addJoin("scene_markers_tags", "", "scene_markers_tags.tag_id = tags.id")
|
||||||
f.addJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id")
|
f.addJoin("scene_markers", "", "scene_markers_tags.scene_marker_id = scene_markers.id OR scene_markers.primary_tag_id = tags.id")
|
||||||
clause, count := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
clause, args := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
||||||
|
|
||||||
args := []interface{}{}
|
|
||||||
if count == 1 {
|
|
||||||
args = append(args, markerCount.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.addHaving(clause, args...)
|
f.addHaving(clause, args...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added between/not between modifiers for number criteria. ([#1559](https://github.com/stashapp/stash/pull/1559))
|
||||||
* Support excluding tag patterns when scraping. ([#1617](https://github.com/stashapp/stash/pull/1617))
|
* Support excluding tag patterns when scraping. ([#1617](https://github.com/stashapp/stash/pull/1617))
|
||||||
* Support setting a custom directory for default performer images. ([#1489](https://github.com/stashapp/stash/pull/1489))
|
* Support setting a custom directory for default performer images. ([#1489](https://github.com/stashapp/stash/pull/1489))
|
||||||
* Added filtering and sorting on scene marker count for tags. ([#1603](https://github.com/stashapp/stash/pull/1603))
|
* Added filtering and sorting on scene marker count for tags. ([#1603](https://github.com/stashapp/stash/pull/1603))
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Button, Form, Modal } from "react-bootstrap";
|
import { Button, Form, Modal } from "react-bootstrap";
|
||||||
import { FilterSelect, DurationInput } from "src/components/Shared";
|
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
DurationCriterion,
|
DurationCriterion,
|
||||||
CriterionValue,
|
CriterionValue,
|
||||||
Criterion,
|
Criterion,
|
||||||
IHierarchicalLabeledIdCriterion,
|
IHierarchicalLabeledIdCriterion,
|
||||||
|
NumberCriterion,
|
||||||
|
ILabeledIdCriterion,
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
import {
|
import {
|
||||||
NoneCriterion,
|
NoneCriterion,
|
||||||
@@ -15,11 +16,18 @@ import {
|
|||||||
} from "src/models/list-filter/criteria/none";
|
} from "src/models/list-filter/criteria/none";
|
||||||
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
criterionIsHierarchicalLabelValue,
|
criterionIsHierarchicalLabelValue,
|
||||||
|
criterionIsNumberValue,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
} from "src/models/list-filter/types";
|
} from "src/models/list-filter/types";
|
||||||
|
import { DurationFilter } from "./Filters/DurationFilter";
|
||||||
|
import { NumberFilter } from "./Filters/NumberFilter";
|
||||||
|
import { LabeledIdFilter } from "./Filters/LabeledIdFilter";
|
||||||
|
import { HierarchicalLabelValueFilter } from "./Filters/HierarchicalLabelValueFilter";
|
||||||
|
import { OptionsFilter } from "./Filters/OptionsFilter";
|
||||||
|
import { InputFilter } from "./Filters/InputFilter";
|
||||||
|
|
||||||
interface IAddFilterProps {
|
interface IAddFilterProps {
|
||||||
onAddCriterion: (
|
onAddCriterion: (
|
||||||
@@ -48,13 +56,6 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
studio_depth: {
|
|
||||||
id: "studio_depth",
|
|
||||||
defaultMessage: "Levels (empty for all)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure if we are editing an existing criterion
|
// Configure if we are editing an existing criterion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editingCriterion) {
|
if (!editingCriterion) {
|
||||||
@@ -64,6 +65,10 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
}
|
}
|
||||||
}, [editingCriterion]);
|
}, [editingCriterion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
valueStage.current = criterion.value;
|
||||||
|
}, [criterion]);
|
||||||
|
|
||||||
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const newCriterionType = event.target.value as CriterionType;
|
const newCriterionType = event.target.value as CriterionType;
|
||||||
const newCriterion = makeCriteria(newCriterionType);
|
const newCriterion = makeCriteria(newCriterionType);
|
||||||
@@ -78,41 +83,13 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
setCriterion(newCriterion);
|
setCriterion(newCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangedSingleSelect(event: React.ChangeEvent<HTMLSelectElement>) {
|
function onValueChanged(value: CriterionValue) {
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
newCriterion.value = event.target.value;
|
newCriterion.value = value;
|
||||||
setCriterion(newCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangedInput(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
valueStage.current = event.target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangedDuration(valueAsNumber: number) {
|
|
||||||
valueStage.current = valueAsNumber;
|
|
||||||
onBlurInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBlurInput() {
|
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
|
||||||
newCriterion.value = valueStage.current;
|
|
||||||
setCriterion(newCriterion);
|
setCriterion(newCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAddFilter() {
|
function onAddFilter() {
|
||||||
if (!Array.isArray(criterion.value) && defaultValue.current !== undefined) {
|
|
||||||
const value = defaultValue.current;
|
|
||||||
if (
|
|
||||||
options &&
|
|
||||||
(value === undefined || value === "" || typeof value === "number")
|
|
||||||
) {
|
|
||||||
criterion.value = options[0].toString();
|
|
||||||
} else if (typeof value === "number" && value === undefined) {
|
|
||||||
criterion.value = 0;
|
|
||||||
} else if (value === undefined) {
|
|
||||||
criterion.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const oldId = editingCriterion ? editingCriterion.getId() : undefined;
|
const oldId = editingCriterion ? editingCriterion.getId() : undefined;
|
||||||
onAddCriterion(criterion, oldId);
|
onAddCriterion(criterion, oldId);
|
||||||
}
|
}
|
||||||
@@ -151,136 +128,57 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(criterion.value)) {
|
if (criterion instanceof ILabeledIdCriterion) {
|
||||||
if (
|
|
||||||
criterion.criterionOption.type !== "performers" &&
|
|
||||||
criterion.criterionOption.type !== "studios" &&
|
|
||||||
criterion.criterionOption.type !== "parent_studios" &&
|
|
||||||
criterion.criterionOption.type !== "tags" &&
|
|
||||||
criterion.criterionOption.type !== "sceneTags" &&
|
|
||||||
criterion.criterionOption.type !== "performerTags" &&
|
|
||||||
criterion.criterionOption.type !== "movies"
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterSelect
|
<LabeledIdFilter
|
||||||
type={criterion.criterionOption.type}
|
criterion={criterion}
|
||||||
isMulti
|
onValueChanged={onValueChanged}
|
||||||
onSelect={(items) => {
|
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
|
||||||
newCriterion.value = items.map((i) => ({
|
|
||||||
id: i.id,
|
|
||||||
label: i.name!,
|
|
||||||
}));
|
|
||||||
setCriterion(newCriterion);
|
|
||||||
}}
|
|
||||||
ids={criterion.value.map((labeled) => labeled.id)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||||
if (criterion.criterionOption.type !== "studios") return;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterSelect
|
<HierarchicalLabelValueFilter
|
||||||
type={criterion.criterionOption.type}
|
criterion={criterion}
|
||||||
isMulti
|
onValueChanged={onValueChanged}
|
||||||
onSelect={(items) => {
|
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
|
||||||
newCriterion.value.items = items.map((i) => ({
|
|
||||||
id: i.id,
|
|
||||||
label: i.name!,
|
|
||||||
}));
|
|
||||||
setCriterion(newCriterion);
|
|
||||||
}}
|
|
||||||
ids={criterion.value.items.map((labeled) => labeled.id)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options && !criterionIsHierarchicalLabelValue(criterion.value)) {
|
if (
|
||||||
|
options &&
|
||||||
|
!criterionIsHierarchicalLabelValue(criterion.value) &&
|
||||||
|
!criterionIsNumberValue(criterion.value) &&
|
||||||
|
!Array.isArray(criterion.value)
|
||||||
|
) {
|
||||||
defaultValue.current = criterion.value;
|
defaultValue.current = criterion.value;
|
||||||
return (
|
return (
|
||||||
<Form.Control
|
<OptionsFilter
|
||||||
as="select"
|
criterion={criterion}
|
||||||
onChange={onChangedSingleSelect}
|
onValueChanged={onValueChanged}
|
||||||
value={criterion.value.toString()}
|
/>
|
||||||
className="btn-secondary"
|
|
||||||
>
|
|
||||||
{options.map((c) => (
|
|
||||||
<option key={c.toString()} value={c.toString()}>
|
|
||||||
{c}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (criterion instanceof DurationCriterion) {
|
if (criterion instanceof DurationCriterion) {
|
||||||
// render duration control
|
|
||||||
return (
|
return (
|
||||||
<DurationInput
|
<DurationFilter
|
||||||
numericValue={criterion.value ? criterion.value : 0}
|
criterion={criterion}
|
||||||
onValueChange={onChangedDuration}
|
onValueChanged={onValueChanged}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
if (criterion instanceof NumberCriterion) {
|
||||||
<Form.Control
|
|
||||||
className="btn-secondary"
|
|
||||||
type={criterion.criterionOption.inputType}
|
|
||||||
onChange={onChangedInput}
|
|
||||||
onBlur={onBlurInput}
|
|
||||||
defaultValue={criterion.value ? criterion.value.toString() : ""}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function renderAdditional() {
|
|
||||||
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<NumberFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
<Form.Group>
|
|
||||||
<Form.Check
|
|
||||||
checked={criterion.value.depth !== 0}
|
|
||||||
label={intl.formatMessage({ id: "include_child_studios" })}
|
|
||||||
onChange={() => {
|
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
|
||||||
newCriterion.value.depth =
|
|
||||||
newCriterion.value.depth !== 0 ? 0 : -1;
|
|
||||||
setCriterion(newCriterion);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
{criterion.value.depth !== 0 && (
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Control
|
|
||||||
className="btn-secondary"
|
|
||||||
type="number"
|
|
||||||
placeholder={intl.formatMessage(messages.studio_depth)}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newCriterion = _.cloneDeep(criterion);
|
|
||||||
newCriterion.value.depth = e.target.value
|
|
||||||
? parseInt(e.target.value, 10)
|
|
||||||
: -1;
|
|
||||||
setCriterion(newCriterion);
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
criterion.value && criterion.value.depth !== -1
|
|
||||||
? criterion.value.depth
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<InputFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Group>{renderModifier()}</Form.Group>
|
<Form.Group>{renderModifier()}</Form.Group>
|
||||||
<Form.Group>{renderSelect()}</Form.Group>
|
{renderSelect()}
|
||||||
{renderAdditional()}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
94
ui/v2.5/src/components/List/Filters/DurationFilter.tsx
Normal file
94
ui/v2.5/src/components/List/Filters/DurationFilter.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||||
|
import { DurationInput } from "../../Shared";
|
||||||
|
import { INumberValue } from "../../../models/list-filter/types";
|
||||||
|
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
|
interface IDurationFilterProps {
|
||||||
|
criterion: Criterion<INumberValue>;
|
||||||
|
onValueChanged: (value: INumberValue) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DurationFilter: React.FC<IDurationFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
function onChanged(valueAsNumber: number, property: "value" | "value2") {
|
||||||
|
const { value } = criterion;
|
||||||
|
value[property] = valueAsNumber;
|
||||||
|
onValueChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let equalsControl: JSX.Element | null = null;
|
||||||
|
if (
|
||||||
|
criterion.modifier === CriterionModifier.Equals ||
|
||||||
|
criterion.modifier === CriterionModifier.NotEquals
|
||||||
|
) {
|
||||||
|
equalsControl = (
|
||||||
|
<Form.Group>
|
||||||
|
<DurationInput
|
||||||
|
numericValue={criterion.value?.value}
|
||||||
|
onValueChange={(v: number) => onChanged(v, "value")}
|
||||||
|
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowerControl: JSX.Element | null = null;
|
||||||
|
if (
|
||||||
|
criterion.modifier === CriterionModifier.GreaterThan ||
|
||||||
|
criterion.modifier === CriterionModifier.Between ||
|
||||||
|
criterion.modifier === CriterionModifier.NotBetween
|
||||||
|
) {
|
||||||
|
lowerControl = (
|
||||||
|
<Form.Group>
|
||||||
|
<DurationInput
|
||||||
|
numericValue={criterion.value?.value}
|
||||||
|
onValueChange={(v: number) => onChanged(v, "value")}
|
||||||
|
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let upperControl: JSX.Element | null = null;
|
||||||
|
if (
|
||||||
|
criterion.modifier === CriterionModifier.LessThan ||
|
||||||
|
criterion.modifier === CriterionModifier.Between ||
|
||||||
|
criterion.modifier === CriterionModifier.NotBetween
|
||||||
|
) {
|
||||||
|
upperControl = (
|
||||||
|
<Form.Group>
|
||||||
|
<DurationInput
|
||||||
|
numericValue={
|
||||||
|
criterion.modifier === CriterionModifier.LessThan
|
||||||
|
? criterion.value?.value
|
||||||
|
: criterion.value?.value2
|
||||||
|
}
|
||||||
|
onValueChange={(v: number) =>
|
||||||
|
onChanged(
|
||||||
|
v,
|
||||||
|
criterion.modifier === CriterionModifier.LessThan
|
||||||
|
? "value"
|
||||||
|
: "value2"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{equalsControl}
|
||||||
|
{lowerControl}
|
||||||
|
{upperControl}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { defineMessages, useIntl } from "react-intl";
|
||||||
|
import { FilterSelect, ValidTypes } from "../../Shared";
|
||||||
|
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||||
|
import { IHierarchicalLabelValue } from "../../../models/list-filter/types";
|
||||||
|
|
||||||
|
interface IHierarchicalLabelValueFilterProps {
|
||||||
|
criterion: Criterion<IHierarchicalLabelValue>;
|
||||||
|
onValueChanged: (value: IHierarchicalLabelValue) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HierarchicalLabelValueFilter: React.FC<IHierarchicalLabelValueFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (
|
||||||
|
criterion.criterionOption.type !== "performers" &&
|
||||||
|
criterion.criterionOption.type !== "studios" &&
|
||||||
|
criterion.criterionOption.type !== "parent_studios" &&
|
||||||
|
criterion.criterionOption.type !== "tags" &&
|
||||||
|
criterion.criterionOption.type !== "sceneTags" &&
|
||||||
|
criterion.criterionOption.type !== "performerTags" &&
|
||||||
|
criterion.criterionOption.type !== "movies"
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
studio_depth: {
|
||||||
|
id: "studio_depth",
|
||||||
|
defaultMessage: "Levels (empty for all)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSelectionChanged(items: ValidTypes[]) {
|
||||||
|
const { value } = criterion;
|
||||||
|
value.items = items.map((i) => ({
|
||||||
|
id: i.id,
|
||||||
|
label: i.name!,
|
||||||
|
}));
|
||||||
|
onValueChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDepthChanged(depth: number) {
|
||||||
|
const { value } = criterion;
|
||||||
|
value.depth = depth;
|
||||||
|
onValueChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Group>
|
||||||
|
<FilterSelect
|
||||||
|
type={criterion.criterionOption.type}
|
||||||
|
isMulti
|
||||||
|
onSelect={onSelectionChanged}
|
||||||
|
ids={criterion.value.items.map((labeled) => labeled.id)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
checked={criterion.value.depth !== 0}
|
||||||
|
label={intl.formatMessage({ id: "include_child_studios" })}
|
||||||
|
onChange={() => onDepthChanged(criterion.value.depth !== 0 ? 0 : -1)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{criterion.value.depth !== 0 && (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
type="number"
|
||||||
|
placeholder={intl.formatMessage(messages.studio_depth)}
|
||||||
|
onChange={(e) =>
|
||||||
|
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)
|
||||||
|
}
|
||||||
|
defaultValue={
|
||||||
|
criterion.value && criterion.value.depth !== -1
|
||||||
|
? criterion.value.depth
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
31
ui/v2.5/src/components/List/Filters/InputFilter.tsx
Normal file
31
ui/v2.5/src/components/List/Filters/InputFilter.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
Criterion,
|
||||||
|
CriterionValue,
|
||||||
|
} from "../../../models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
|
interface IInputFilterProps {
|
||||||
|
criterion: Criterion<CriterionValue>;
|
||||||
|
onValueChanged: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputFilter: React.FC<IInputFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
function onChanged(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
onValueChanged(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
type={criterion.criterionOption.inputType}
|
||||||
|
onBlur={onChanged}
|
||||||
|
defaultValue={criterion.value ? criterion.value.toString() : ""}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
46
ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx
Normal file
46
ui/v2.5/src/components/List/Filters/LabeledIdFilter.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { FilterSelect, ValidTypes } from "../../Shared";
|
||||||
|
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||||
|
import { ILabeledId } from "../../../models/list-filter/types";
|
||||||
|
|
||||||
|
interface ILabeledIdFilterProps {
|
||||||
|
criterion: Criterion<ILabeledId[]>;
|
||||||
|
onValueChanged: (value: ILabeledId[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LabeledIdFilter: React.FC<ILabeledIdFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
criterion.criterionOption.type !== "performers" &&
|
||||||
|
criterion.criterionOption.type !== "studios" &&
|
||||||
|
criterion.criterionOption.type !== "parent_studios" &&
|
||||||
|
criterion.criterionOption.type !== "tags" &&
|
||||||
|
criterion.criterionOption.type !== "sceneTags" &&
|
||||||
|
criterion.criterionOption.type !== "performerTags" &&
|
||||||
|
criterion.criterionOption.type !== "movies"
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
function onSelectionChanged(items: ValidTypes[]) {
|
||||||
|
onValueChanged(
|
||||||
|
items.map((i) => ({
|
||||||
|
id: i.id,
|
||||||
|
label: i.name!,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<FilterSelect
|
||||||
|
type={criterion.criterionOption.type}
|
||||||
|
isMulti
|
||||||
|
onSelect={onSelectionChanged}
|
||||||
|
ids={criterion.value.map((labeled) => labeled.id)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
114
ui/v2.5/src/components/List/Filters/NumberFilter.tsx
Normal file
114
ui/v2.5/src/components/List/Filters/NumberFilter.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||||
|
import { INumberValue } from "../../../models/list-filter/types";
|
||||||
|
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
|
interface IDurationFilterProps {
|
||||||
|
criterion: Criterion<INumberValue>;
|
||||||
|
onValueChanged: (value: INumberValue) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const valueStage = useRef<INumberValue>(criterion.value);
|
||||||
|
|
||||||
|
function onChanged(
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
property: "value" | "value2"
|
||||||
|
) {
|
||||||
|
const value = parseInt(event.target.value, 10);
|
||||||
|
valueStage.current[property] = !Number.isNaN(value) ? value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBlurInput() {
|
||||||
|
onValueChanged(valueStage.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
let equalsControl: JSX.Element | null = null;
|
||||||
|
if (
|
||||||
|
criterion.modifier === CriterionModifier.Equals ||
|
||||||
|
criterion.modifier === CriterionModifier.NotEquals
|
||||||
|
) {
|
||||||
|
equalsControl = (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
type="number"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChanged(e, "value")
|
||||||
|
}
|
||||||
|
onBlur={onBlurInput}
|
||||||
|
defaultValue={criterion.value?.value ?? ""}
|
||||||
|
placeholder={intl.formatMessage({ id: "criterion.value" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowerControl: JSX.Element | null = null;
|
||||||
|
if (
|
||||||
|
criterion.modifier === CriterionModifier.GreaterThan ||
|
||||||
|
criterion.modifier === CriterionModifier.Between ||
|
||||||
|
criterion.modifier === CriterionModifier.NotBetween
|
||||||
|
) {
|
||||||
|
lowerControl = (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
type="number"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChanged(e, "value")
|
||||||
|
}
|
||||||
|
onBlur={onBlurInput}
|
||||||
|
defaultValue={criterion.value?.value ?? ""}
|
||||||
|
placeholder={intl.formatMessage({ id: "criterion.greater_than" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let upperControl: JSX.Element | null = null;
|
||||||
|
if (
|
||||||
|
criterion.modifier === CriterionModifier.LessThan ||
|
||||||
|
criterion.modifier === CriterionModifier.Between ||
|
||||||
|
criterion.modifier === CriterionModifier.NotBetween
|
||||||
|
) {
|
||||||
|
upperControl = (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
className="btn-secondary"
|
||||||
|
type="number"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChanged(
|
||||||
|
e,
|
||||||
|
criterion.modifier === CriterionModifier.LessThan
|
||||||
|
? "value"
|
||||||
|
: "value2"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onBlur={onBlurInput}
|
||||||
|
defaultValue={
|
||||||
|
(criterion.modifier === CriterionModifier.LessThan
|
||||||
|
? criterion.value?.value
|
||||||
|
: criterion.value?.value2) ?? ""
|
||||||
|
}
|
||||||
|
placeholder={intl.formatMessage({ id: "criterion.less_than" })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{equalsControl}
|
||||||
|
{lowerControl}
|
||||||
|
{upperControl}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
48
ui/v2.5/src/components/List/Filters/OptionsFilter.tsx
Normal file
48
ui/v2.5/src/components/List/Filters/OptionsFilter.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
Criterion,
|
||||||
|
CriterionValue,
|
||||||
|
} from "../../../models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
|
interface IOptionsFilterProps {
|
||||||
|
criterion: Criterion<CriterionValue>;
|
||||||
|
onValueChanged: (value: CriterionValue) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OptionsFilter: React.FC<IOptionsFilterProps> = ({
|
||||||
|
criterion,
|
||||||
|
onValueChanged,
|
||||||
|
}) => {
|
||||||
|
function onChanged(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
|
onValueChanged(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = criterion.criterionOption.options ?? [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
options &&
|
||||||
|
(criterion.value === undefined ||
|
||||||
|
criterion.value === "" ||
|
||||||
|
typeof criterion.value === "number")
|
||||||
|
) {
|
||||||
|
onValueChanged(options[0].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
onChange={onChanged}
|
||||||
|
value={criterion.value.toString()}
|
||||||
|
className="btn-secondary"
|
||||||
|
>
|
||||||
|
{options.map((c) => (
|
||||||
|
<option key={c.toString()} value={c.toString()}>
|
||||||
|
{c}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ interface IProps {
|
|||||||
): void;
|
): void;
|
||||||
onReset?(): void;
|
onReset?(): void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||||
@@ -108,7 +109,13 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
props.onValueChange(undefined);
|
props.onValueChange(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder={!props.disabled ? "hh:mm:ss" : undefined}
|
placeholder={
|
||||||
|
!props.disabled
|
||||||
|
? props.placeholder
|
||||||
|
? `${props.placeholder} (hh:mm:ss)`
|
||||||
|
: "hh:mm:ss"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
{maybeRenderReset()}
|
{maybeRenderReset()}
|
||||||
|
|||||||
@@ -380,6 +380,11 @@
|
|||||||
"country": "Country",
|
"country": "Country",
|
||||||
"cover_image": "Cover Image",
|
"cover_image": "Cover Image",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
|
"criterion": {
|
||||||
|
"greater_than": "Greater than",
|
||||||
|
"less_than": "Less than",
|
||||||
|
"value": "Value"
|
||||||
|
},
|
||||||
"criterion_modifier": {
|
"criterion_modifier": {
|
||||||
"equals": "is",
|
"equals": "is",
|
||||||
"excludes": "excludes",
|
"excludes": "excludes",
|
||||||
@@ -392,7 +397,9 @@
|
|||||||
"matches_regex": "matches regex",
|
"matches_regex": "matches regex",
|
||||||
"not_equals": "is not",
|
"not_equals": "is not",
|
||||||
"not_matches_regex": "not matches regex",
|
"not_matches_regex": "not matches regex",
|
||||||
"not_null": "is not null"
|
"not_null": "is not null",
|
||||||
|
"between": "between",
|
||||||
|
"not_between": "not between"
|
||||||
},
|
},
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"death_date": "Death Date",
|
"death_date": "Death Date",
|
||||||
|
|||||||
@@ -4,24 +4,26 @@ import { IntlShape } from "react-intl";
|
|||||||
import {
|
import {
|
||||||
CriterionModifier,
|
CriterionModifier,
|
||||||
HierarchicalMultiCriterionInput,
|
HierarchicalMultiCriterionInput,
|
||||||
|
IntCriterionInput,
|
||||||
MultiCriterionInput,
|
MultiCriterionInput,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import DurationUtils from "src/utils/duration";
|
import DurationUtils from "src/utils/duration";
|
||||||
import {
|
import {
|
||||||
CriterionType,
|
CriterionType,
|
||||||
encodeILabeledId,
|
encodeILabeledId,
|
||||||
|
IHierarchicalLabelValue,
|
||||||
ILabeledId,
|
ILabeledId,
|
||||||
ILabeledValue,
|
ILabeledValue,
|
||||||
|
INumberValue,
|
||||||
IOptionType,
|
IOptionType,
|
||||||
IHierarchicalLabelValue,
|
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
export type Option = string | number | IOptionType;
|
export type Option = string | number | IOptionType;
|
||||||
export type CriterionValue =
|
export type CriterionValue =
|
||||||
| string
|
| string
|
||||||
| number
|
|
||||||
| ILabeledId[]
|
| ILabeledId[]
|
||||||
| IHierarchicalLabelValue;
|
| IHierarchicalLabelValue
|
||||||
|
| INumberValue;
|
||||||
|
|
||||||
const modifierMessageIDs = {
|
const modifierMessageIDs = {
|
||||||
[CriterionModifier.Equals]: "criterion_modifier.equals",
|
[CriterionModifier.Equals]: "criterion_modifier.equals",
|
||||||
@@ -35,6 +37,8 @@ const modifierMessageIDs = {
|
|||||||
[CriterionModifier.Excludes]: "criterion_modifier.excludes",
|
[CriterionModifier.Excludes]: "criterion_modifier.excludes",
|
||||||
[CriterionModifier.MatchesRegex]: "criterion_modifier.matches_regex",
|
[CriterionModifier.MatchesRegex]: "criterion_modifier.matches_regex",
|
||||||
[CriterionModifier.NotMatchesRegex]: "criterion_modifier.not_matches_regex",
|
[CriterionModifier.NotMatchesRegex]: "criterion_modifier.not_matches_regex",
|
||||||
|
[CriterionModifier.Between]: "criterion_modifier.between",
|
||||||
|
[CriterionModifier.NotBetween]: "criterion_modifier.not_between",
|
||||||
};
|
};
|
||||||
|
|
||||||
// V = criterion value type
|
// V = criterion value type
|
||||||
@@ -293,6 +297,8 @@ export class NumberCriterionOption extends CriterionOption {
|
|||||||
CriterionModifier.LessThan,
|
CriterionModifier.LessThan,
|
||||||
CriterionModifier.IsNull,
|
CriterionModifier.IsNull,
|
||||||
CriterionModifier.NotNull,
|
CriterionModifier.NotNull,
|
||||||
|
CriterionModifier.Between,
|
||||||
|
CriterionModifier.NotBetween,
|
||||||
],
|
],
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
options,
|
options,
|
||||||
@@ -305,13 +311,42 @@ export function createNumberCriterionOption(value: CriterionType) {
|
|||||||
return new NumberCriterionOption(value, value, value);
|
return new NumberCriterionOption(value, value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NumberCriterion extends Criterion<number> {
|
export class NumberCriterion extends Criterion<INumberValue> {
|
||||||
|
private getValue() {
|
||||||
|
// backwards compatibility - if this.value is a number, use that
|
||||||
|
if (typeof this.value !== "object") {
|
||||||
|
return this.value as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public encodeValue() {
|
||||||
|
return {
|
||||||
|
value: this.getValue(),
|
||||||
|
value2: this.value.value2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
|
// backwards compatibility - if this.value is a number, use that
|
||||||
|
return {
|
||||||
|
modifier: this.modifier,
|
||||||
|
value: this.getValue(),
|
||||||
|
value2: this.value.value2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getLabelValue() {
|
public getLabelValue() {
|
||||||
return this.value.toString();
|
const value = this.getValue();
|
||||||
|
return this.modifier === CriterionModifier.Between ||
|
||||||
|
this.modifier === CriterionModifier.NotBetween
|
||||||
|
? `${value}, ${this.value.value2 ?? 0}`
|
||||||
|
: `${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, 0);
|
super(type, { value: 0, value2: undefined });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,6 +450,8 @@ export class MandatoryNumberCriterionOption extends CriterionOption {
|
|||||||
CriterionModifier.NotEquals,
|
CriterionModifier.NotEquals,
|
||||||
CriterionModifier.GreaterThan,
|
CriterionModifier.GreaterThan,
|
||||||
CriterionModifier.LessThan,
|
CriterionModifier.LessThan,
|
||||||
|
CriterionModifier.Between,
|
||||||
|
CriterionModifier.NotBetween,
|
||||||
],
|
],
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
@@ -426,12 +463,37 @@ export function createMandatoryNumberCriterionOption(value: CriterionType) {
|
|||||||
return new MandatoryNumberCriterionOption(value, value, value);
|
return new MandatoryNumberCriterionOption(value, value, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DurationCriterion extends Criterion<number> {
|
export class DurationCriterion extends Criterion<INumberValue> {
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, 0);
|
super(type, { value: 0, value2: undefined });
|
||||||
|
}
|
||||||
|
|
||||||
|
public encodeValue() {
|
||||||
|
return {
|
||||||
|
value: this.value.value,
|
||||||
|
value2: this.value.value2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toCriterionInput(): IntCriterionInput {
|
||||||
|
return {
|
||||||
|
modifier: this.modifier,
|
||||||
|
value: this.value.value,
|
||||||
|
value2: this.value.value2,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLabelValue() {
|
public getLabelValue() {
|
||||||
return DurationUtils.secondsToString(this.value);
|
return this.modifier === CriterionModifier.Between ||
|
||||||
|
this.modifier === CriterionModifier.NotBetween
|
||||||
|
? `${DurationUtils.secondsToString(
|
||||||
|
this.value.value
|
||||||
|
)} ${DurationUtils.secondsToString(this.value.value2 ?? 0)}`
|
||||||
|
: this.modifier === CriterionModifier.GreaterThan ||
|
||||||
|
this.modifier === CriterionModifier.LessThan ||
|
||||||
|
this.modifier === CriterionModifier.Equals ||
|
||||||
|
this.modifier === CriterionModifier.NotEquals
|
||||||
|
? DurationUtils.secondsToString(this.value.value)
|
||||||
|
: "?";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export interface IHierarchicalLabelValue {
|
|||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INumberValue {
|
||||||
|
value: number;
|
||||||
|
value2: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function criterionIsHierarchicalLabelValue(
|
export function criterionIsHierarchicalLabelValue(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
value: any
|
value: any
|
||||||
@@ -30,6 +35,13 @@ export function criterionIsHierarchicalLabelValue(
|
|||||||
return typeof value === "object" && "items" in value && "depth" in value;
|
return typeof value === "object" && "items" in value && "depth" in value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function criterionIsNumberValue(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
value: any
|
||||||
|
): value is INumberValue {
|
||||||
|
return typeof value === "object" && "value" in value && "value2" in value;
|
||||||
|
}
|
||||||
|
|
||||||
export function encodeILabeledId(o: ILabeledId) {
|
export function encodeILabeledId(o: ILabeledId) {
|
||||||
// escape " and \ and by encoding to JSON so that it encodes to JSON correctly down the line
|
// escape " and \ and by encoding to JSON so that it encodes to JSON correctly down the line
|
||||||
const adjustedLabel = JSON.stringify(o.label).slice(1, -1);
|
const adjustedLabel = JSON.stringify(o.label).slice(1, -1);
|
||||||
|
|||||||
Reference in New Issue
Block a user