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,
|
||||
"""NOT MATCHES REGEX"""
|
||||
NOT_MATCHES_REGEX,
|
||||
""">= AND <="""
|
||||
BETWEEN,
|
||||
"""< OR >"""
|
||||
NOT_BETWEEN,
|
||||
}
|
||||
|
||||
input StringCriterionInput {
|
||||
@@ -336,6 +340,7 @@ input StringCriterionInput {
|
||||
|
||||
input IntCriterionInput {
|
||||
value: Int!
|
||||
value2: Int
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
|
||||
@@ -368,13 +368,8 @@ func stringCriterionHandler(c *models.StringCriterionInput, column string) crite
|
||||
func intCriterionHandler(c *models.IntCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if c != nil {
|
||||
clause, count := getIntCriterionWhereClause(column, *c)
|
||||
|
||||
if count == 1 {
|
||||
f.addWhere(clause, c.Value)
|
||||
} else {
|
||||
f.addWhere(clause)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause(column, *c)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,13 +490,9 @@ type countCriterionHandlerBuilder struct {
|
||||
func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
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, criterion.Value)
|
||||
} else {
|
||||
f.addWhere(clause)
|
||||
}
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package sqlite
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -356,25 +355,8 @@ func performerIsMissingCriterionHandler(qb *performerQueryBuilder, isMissing *st
|
||||
func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if year != nil && year.Modifier.IsValid() {
|
||||
yearStr := strconv.Itoa(year.Value)
|
||||
startOfYear := yearStr + "-01-01"
|
||||
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)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause("cast(strftime('%Y', "+col+") as int)", *year)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,22 +364,11 @@ func yearFilterCriterionHandler(year *models.IntCriterionInput, col string) crit
|
||||
func performerAgeFilterCriterionHandler(age *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if age != nil && age.Modifier.IsValid() {
|
||||
var op string
|
||||
|
||||
switch age.Modifier {
|
||||
case models.CriterionModifierEquals:
|
||||
op = "=="
|
||||
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)
|
||||
}
|
||||
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)",
|
||||
*age,
|
||||
)
|
||||
f.addWhere(clause, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,11 +135,9 @@ func (qb *queryBuilder) addFilter(f *filterBuilder) {
|
||||
|
||||
func (qb *queryBuilder) handleIntCriterionInput(c *models.IntCriterionInput, column string) {
|
||||
if c != nil {
|
||||
clause, count := getIntCriterionWhereClause(column, *c)
|
||||
clause, args := getIntCriterionWhereClause(column, *c)
|
||||
qb.addWhere(clause)
|
||||
if count == 1 {
|
||||
qb.addArg(c.Value)
|
||||
}
|
||||
qb.addArg(args...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,12 +190,9 @@ func (qb *queryBuilder) handleStringCriterionInput(c *models.StringCriterionInpu
|
||||
|
||||
func (qb *queryBuilder) handleCountCriterion(countFilter *models.IntCriterionInput, primaryTable, joinTable, primaryFK string) {
|
||||
if countFilter != nil {
|
||||
clause, count := getCountCriterionClause(primaryTable, joinTable, primaryFK, *countFilter)
|
||||
|
||||
if count == 1 {
|
||||
qb.addArg(countFilter.Value)
|
||||
}
|
||||
clause, args := getCountCriterionClause(primaryTable, joinTable, primaryFK, *countFilter)
|
||||
|
||||
qb.addWhere(clause)
|
||||
qb.addArg(args...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,40 +461,12 @@ func phashCriterionHandler(phashFilter *models.StringCriterionInput) criterionHa
|
||||
func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if durationFilter != nil {
|
||||
clause, thisArgs := getDurationWhereClause(*durationFilter, column)
|
||||
f.addWhere(clause, thisArgs...)
|
||||
clause, args := getIntCriterionWhereClause("cast("+column+" as int)", *durationFilter)
|
||||
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 {
|
||||
return func(f *filterBuilder) {
|
||||
if resolution != nil && resolution.Value.IsValid() {
|
||||
|
||||
@@ -160,7 +160,7 @@ func getCriterionModifierBinding(criterionModifier models.CriterionModifier, val
|
||||
}
|
||||
if modifier := criterionModifier.String(); criterionModifier.IsValid() {
|
||||
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, "?")
|
||||
case "INCLUDES":
|
||||
return "IN " + getInBinding(length), length // TODO?
|
||||
@@ -189,6 +189,10 @@ func getSimpleCriterionClause(criterionModifier models.CriterionModifier, rhs st
|
||||
return "IS NULL", 0
|
||||
case "NOT_NULL":
|
||||
return "IS NOT NULL", 0
|
||||
case "BETWEEN":
|
||||
return "BETWEEN (" + rhs + ") AND (" + rhs + ")", 2
|
||||
case "NOT_BETWEEN":
|
||||
return "NOT BETWEEN (" + rhs + ") AND (" + rhs + ")", 2
|
||||
default:
|
||||
logger.Errorf("todo")
|
||||
return "= ?", 1 // TODO
|
||||
@@ -198,9 +202,30 @@ func getSimpleCriterionClause(criterionModifier models.CriterionModifier, rhs st
|
||||
return "= ?", 1 // TODO
|
||||
}
|
||||
|
||||
func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, int) {
|
||||
binding, count := getCriterionModifierBinding(input.Modifier, input.Value)
|
||||
return column + " " + binding, count
|
||||
func getIntCriterionWhereClause(column string, input models.IntCriterionInput) (string, []interface{}) {
|
||||
binding, _ := getSimpleCriterionClause(input.Modifier, "?")
|
||||
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
|
||||
@@ -226,7 +251,7 @@ func getMultiCriterionClause(primaryTable, foreignTable, joinTable, primaryFK, f
|
||||
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)
|
||||
return getIntCriterionWhereClause(lhs, criterion)
|
||||
}
|
||||
|
||||
@@ -359,12 +359,7 @@ func tagSceneCountCriterionHandler(qb *tagQueryBuilder, sceneCount *models.IntCr
|
||||
return func(f *filterBuilder) {
|
||||
if sceneCount != nil {
|
||||
f.addJoin("scenes_tags", "", "scenes_tags.tag_id = tags.id")
|
||||
clause, count := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
|
||||
|
||||
args := []interface{}{}
|
||||
if count == 1 {
|
||||
args = append(args, sceneCount.Value)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scenes_tags.scene_id)", *sceneCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
@@ -375,12 +370,7 @@ func tagImageCountCriterionHandler(qb *tagQueryBuilder, imageCount *models.IntCr
|
||||
return func(f *filterBuilder) {
|
||||
if imageCount != nil {
|
||||
f.addJoin("images_tags", "", "images_tags.tag_id = tags.id")
|
||||
clause, count := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
|
||||
|
||||
args := []interface{}{}
|
||||
if count == 1 {
|
||||
args = append(args, imageCount.Value)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
@@ -391,12 +381,7 @@ func tagGalleryCountCriterionHandler(qb *tagQueryBuilder, galleryCount *models.I
|
||||
return func(f *filterBuilder) {
|
||||
if galleryCount != nil {
|
||||
f.addJoin("galleries_tags", "", "galleries_tags.tag_id = tags.id")
|
||||
clause, count := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
|
||||
|
||||
args := []interface{}{}
|
||||
if count == 1 {
|
||||
args = append(args, galleryCount.Value)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
@@ -407,12 +392,7 @@ func tagPerformerCountCriterionHandler(qb *tagQueryBuilder, performerCount *mode
|
||||
return func(f *filterBuilder) {
|
||||
if performerCount != nil {
|
||||
f.addJoin("performers_tags", "", "performers_tags.tag_id = tags.id")
|
||||
clause, count := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
|
||||
|
||||
args := []interface{}{}
|
||||
if count == 1 {
|
||||
args = append(args, performerCount.Value)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performerCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
@@ -424,12 +404,7 @@ func tagMarkerCountCriterionHandler(qb *tagQueryBuilder, markerCount *models.Int
|
||||
if markerCount != nil {
|
||||
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")
|
||||
clause, count := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
||||
|
||||
args := []interface{}{}
|
||||
if count == 1 {
|
||||
args = append(args, markerCount.Value)
|
||||
}
|
||||
clause, args := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
||||
|
||||
f.addHaving(clause, args...)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
### ✨ 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 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))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Form, Modal } from "react-bootstrap";
|
||||
import { FilterSelect, DurationInput } from "src/components/Shared";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import {
|
||||
DurationCriterion,
|
||||
CriterionValue,
|
||||
Criterion,
|
||||
IHierarchicalLabeledIdCriterion,
|
||||
NumberCriterion,
|
||||
ILabeledIdCriterion,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import {
|
||||
NoneCriterion,
|
||||
@@ -15,11 +16,18 @@ import {
|
||||
} from "src/models/list-filter/criteria/none";
|
||||
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
criterionIsHierarchicalLabelValue,
|
||||
criterionIsNumberValue,
|
||||
CriterionType,
|
||||
} 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 {
|
||||
onAddCriterion: (
|
||||
@@ -48,13 +56,6 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const messages = defineMessages({
|
||||
studio_depth: {
|
||||
id: "studio_depth",
|
||||
defaultMessage: "Levels (empty for all)",
|
||||
},
|
||||
});
|
||||
|
||||
// Configure if we are editing an existing criterion
|
||||
useEffect(() => {
|
||||
if (!editingCriterion) {
|
||||
@@ -64,6 +65,10 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
}
|
||||
}, [editingCriterion]);
|
||||
|
||||
useEffect(() => {
|
||||
valueStage.current = criterion.value;
|
||||
}, [criterion]);
|
||||
|
||||
function onChangedCriteriaType(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const newCriterionType = event.target.value as CriterionType;
|
||||
const newCriterion = makeCriteria(newCriterionType);
|
||||
@@ -78,41 +83,13 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
setCriterion(newCriterion);
|
||||
}
|
||||
|
||||
function onChangedSingleSelect(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
function onValueChanged(value: CriterionValue) {
|
||||
const newCriterion = _.cloneDeep(criterion);
|
||||
newCriterion.value = event.target.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;
|
||||
newCriterion.value = value;
|
||||
setCriterion(newCriterion);
|
||||
}
|
||||
|
||||
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;
|
||||
onAddCriterion(criterion, oldId);
|
||||
}
|
||||
@@ -151,136 +128,57 @@ export const AddFilterDialog: React.FC<IAddFilterProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(criterion.value)) {
|
||||
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;
|
||||
|
||||
if (criterion instanceof ILabeledIdCriterion) {
|
||||
return (
|
||||
<FilterSelect
|
||||
type={criterion.criterionOption.type}
|
||||
isMulti
|
||||
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)}
|
||||
<LabeledIdFilter
|
||||
criterion={criterion}
|
||||
onValueChanged={onValueChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||
if (criterion.criterionOption.type !== "studios") return;
|
||||
|
||||
return (
|
||||
<FilterSelect
|
||||
type={criterion.criterionOption.type}
|
||||
isMulti
|
||||
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)}
|
||||
<HierarchicalLabelValueFilter
|
||||
criterion={criterion}
|
||||
onValueChanged={onValueChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (options && !criterionIsHierarchicalLabelValue(criterion.value)) {
|
||||
if (
|
||||
options &&
|
||||
!criterionIsHierarchicalLabelValue(criterion.value) &&
|
||||
!criterionIsNumberValue(criterion.value) &&
|
||||
!Array.isArray(criterion.value)
|
||||
) {
|
||||
defaultValue.current = criterion.value;
|
||||
return (
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={onChangedSingleSelect}
|
||||
value={criterion.value.toString()}
|
||||
className="btn-secondary"
|
||||
>
|
||||
{options.map((c) => (
|
||||
<option key={c.toString()} value={c.toString()}>
|
||||
{c}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
<OptionsFilter
|
||||
criterion={criterion}
|
||||
onValueChanged={onValueChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (criterion instanceof DurationCriterion) {
|
||||
// render duration control
|
||||
return (
|
||||
<DurationInput
|
||||
numericValue={criterion.value ? criterion.value : 0}
|
||||
onValueChange={onChangedDuration}
|
||||
<DurationFilter
|
||||
criterion={criterion}
|
||||
onValueChanged={onValueChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (criterion instanceof NumberCriterion) {
|
||||
return (
|
||||
<Form.Control
|
||||
className="btn-secondary"
|
||||
type={criterion.criterionOption.inputType}
|
||||
onChange={onChangedInput}
|
||||
onBlur={onBlurInput}
|
||||
defaultValue={criterion.value ? criterion.value.toString() : ""}
|
||||
/>
|
||||
<NumberFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||
);
|
||||
}
|
||||
function renderAdditional() {
|
||||
if (criterion instanceof IHierarchicalLabeledIdCriterion) {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
<InputFilter criterion={criterion} onValueChanged={onValueChanged} />
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Form.Group>{renderModifier()}</Form.Group>
|
||||
<Form.Group>{renderSelect()}</Form.Group>
|
||||
{renderAdditional()}
|
||||
{renderSelect()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
onReset?(): void;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
@@ -108,7 +109,13 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||
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>
|
||||
{maybeRenderReset()}
|
||||
|
||||
@@ -380,6 +380,11 @@
|
||||
"country": "Country",
|
||||
"cover_image": "Cover Image",
|
||||
"created_at": "Created At",
|
||||
"criterion": {
|
||||
"greater_than": "Greater than",
|
||||
"less_than": "Less than",
|
||||
"value": "Value"
|
||||
},
|
||||
"criterion_modifier": {
|
||||
"equals": "is",
|
||||
"excludes": "excludes",
|
||||
@@ -392,7 +397,9 @@
|
||||
"matches_regex": "matches regex",
|
||||
"not_equals": "is not",
|
||||
"not_matches_regex": "not matches regex",
|
||||
"not_null": "is not null"
|
||||
"not_null": "is not null",
|
||||
"between": "between",
|
||||
"not_between": "not between"
|
||||
},
|
||||
"date": "Date",
|
||||
"death_date": "Death Date",
|
||||
|
||||
@@ -4,24 +4,26 @@ import { IntlShape } from "react-intl";
|
||||
import {
|
||||
CriterionModifier,
|
||||
HierarchicalMultiCriterionInput,
|
||||
IntCriterionInput,
|
||||
MultiCriterionInput,
|
||||
} from "src/core/generated-graphql";
|
||||
import DurationUtils from "src/utils/duration";
|
||||
import {
|
||||
CriterionType,
|
||||
encodeILabeledId,
|
||||
IHierarchicalLabelValue,
|
||||
ILabeledId,
|
||||
ILabeledValue,
|
||||
INumberValue,
|
||||
IOptionType,
|
||||
IHierarchicalLabelValue,
|
||||
} from "../types";
|
||||
|
||||
export type Option = string | number | IOptionType;
|
||||
export type CriterionValue =
|
||||
| string
|
||||
| number
|
||||
| ILabeledId[]
|
||||
| IHierarchicalLabelValue;
|
||||
| IHierarchicalLabelValue
|
||||
| INumberValue;
|
||||
|
||||
const modifierMessageIDs = {
|
||||
[CriterionModifier.Equals]: "criterion_modifier.equals",
|
||||
@@ -35,6 +37,8 @@ const modifierMessageIDs = {
|
||||
[CriterionModifier.Excludes]: "criterion_modifier.excludes",
|
||||
[CriterionModifier.MatchesRegex]: "criterion_modifier.matches_regex",
|
||||
[CriterionModifier.NotMatchesRegex]: "criterion_modifier.not_matches_regex",
|
||||
[CriterionModifier.Between]: "criterion_modifier.between",
|
||||
[CriterionModifier.NotBetween]: "criterion_modifier.not_between",
|
||||
};
|
||||
|
||||
// V = criterion value type
|
||||
@@ -293,6 +297,8 @@ export class NumberCriterionOption extends CriterionOption {
|
||||
CriterionModifier.LessThan,
|
||||
CriterionModifier.IsNull,
|
||||
CriterionModifier.NotNull,
|
||||
CriterionModifier.Between,
|
||||
CriterionModifier.NotBetween,
|
||||
],
|
||||
defaultModifier: CriterionModifier.Equals,
|
||||
options,
|
||||
@@ -305,13 +311,42 @@ export function createNumberCriterionOption(value: CriterionType) {
|
||||
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() {
|
||||
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) {
|
||||
super(type, 0);
|
||||
super(type, { value: 0, value2: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,6 +450,8 @@ export class MandatoryNumberCriterionOption extends CriterionOption {
|
||||
CriterionModifier.NotEquals,
|
||||
CriterionModifier.GreaterThan,
|
||||
CriterionModifier.LessThan,
|
||||
CriterionModifier.Between,
|
||||
CriterionModifier.NotBetween,
|
||||
],
|
||||
defaultModifier: CriterionModifier.Equals,
|
||||
inputType: "number",
|
||||
@@ -426,12 +463,37 @@ export function createMandatoryNumberCriterionOption(value: CriterionType) {
|
||||
return new MandatoryNumberCriterionOption(value, value, value);
|
||||
}
|
||||
|
||||
export class DurationCriterion extends Criterion<number> {
|
||||
export class DurationCriterion extends Criterion<INumberValue> {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
|
||||
export interface INumberValue {
|
||||
value: number;
|
||||
value2: number | undefined;
|
||||
}
|
||||
|
||||
export function criterionIsHierarchicalLabelValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any
|
||||
@@ -30,6 +35,13 @@ export function criterionIsHierarchicalLabelValue(
|
||||
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) {
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user