From fdb2dd9a8b213cc047f20683da8ebb07157251cd Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 4 Mar 2025 09:26:46 +1100 Subject: [PATCH] Use existing formats for saved filters (#5697) * Use existing formats for saved filters * Fix date criterion marshalling --- .../src/components/List/SavedFilterList.tsx | 4 +- .../models/list-filter/criteria/criterion.ts | 170 ++++++++++++++---- .../list-filter/criteria/custom-fields.ts | 21 ++- .../src/models/list-filter/criteria/rating.ts | 19 +- .../models/list-filter/criteria/stash-ids.ts | 32 +++- ui/v2.5/src/models/list-filter/filter.ts | 25 ++- ui/v2.5/src/models/list-filter/types.ts | 19 +- 7 files changed, 218 insertions(+), 72 deletions(-) diff --git a/ui/v2.5/src/components/List/SavedFilterList.tsx b/ui/v2.5/src/components/List/SavedFilterList.tsx index 5a6bc91cb..63654c028 100644 --- a/ui/v2.5/src/components/List/SavedFilterList.tsx +++ b/ui/v2.5/src/components/List/SavedFilterList.tsx @@ -67,7 +67,7 @@ export const SavedFilterList: React.FC = ({ mode: filter.mode, name, find_filter: filterCopy.makeFindFilter(), - object_filter: filterCopy.makeFilter(), + object_filter: filterCopy.makeSavedFilter(), ui_options: filterCopy.makeSavedUIOptions(), }, }, @@ -142,7 +142,7 @@ export const SavedFilterList: React.FC = ({ value: { mode: filter.mode, find_filter: filterCopy.makeFindFilter(), - object_filter: filterCopy.makeFilter(), + object_filter: filterCopy.makeSavedFilter(), ui_options: filterCopy.makeSavedUIOptions(), }, }, diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index 984dfba3d..4df0f520d 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -5,9 +5,9 @@ import { HierarchicalMultiCriterionInput, IntCriterionInput, MultiCriterionInput, - DateCriterionInput, TimestampCriterionInput, ConfigDataFragment, + DateCriterionInput, } from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; import { @@ -21,11 +21,13 @@ import { ITimestampValue, ILabeledValueListValue, IPhashDistanceValue, + IRangeValue, } from "../types"; export type Option = string | number | IOptionType; export type CriterionValue = | string + | boolean | string[] | ILabeledId[] | IHierarchicalLabelValue @@ -36,7 +38,7 @@ export type CriterionValue = | ITimestampValue | IPhashDistanceValue; -export interface ISavedCriterion { +export interface ISavedCriterion { modifier: CriterionModifier; value: T | undefined; } @@ -82,9 +84,15 @@ export abstract class Criterion { return `${this.criterionOption.type}`; } - public abstract toJSON(): string; + public abstract toQueryParams(): Record; + + // fromDecodedParams is used to set the criterion from the query string + // i is the decoded parameter object + public abstract fromDecodedParams(i: Record): void; public abstract applyToCriterionInput(input: Record): void; + + public abstract applyToSavedCriterion(input: Record): void; public abstract setFromSavedCriterion(criterion: unknown): void; } @@ -164,38 +172,61 @@ export abstract class ModifierCriterion< ); } - public toJSON() { - let encodedCriterion; + public toQueryParams(): Record { + let encodedCriterion: Record = { + type: this.criterionOption.type, + modifier: this.modifier, + }; + if ( - this.modifier === CriterionModifier.IsNull || - this.modifier === CriterionModifier.NotNull + this.modifier !== CriterionModifier.IsNull && + this.modifier !== CriterionModifier.NotNull ) { - encodedCriterion = { - type: this.criterionOption.type, - modifier: this.modifier, - }; - } else { - encodedCriterion = { - type: this.criterionOption.type, - value: this.value, - modifier: this.modifier, - }; + encodedCriterion.value = this.encodeValue(); } - return JSON.stringify(encodedCriterion); + + return encodedCriterion; } - public setFromSavedCriterion(criterion: ISavedCriterion) { - if (criterion.value !== undefined && criterion.value !== null) { - this.value = criterion.value; + protected encodeValue(): unknown { + return this.value; + } + + protected decodeValue(v: unknown) { + if (v !== undefined && v !== null) { + this.value = v as V; } - this.modifier = criterion.modifier; + } + + public fromDecodedParams(i: unknown): void { + // use same logic as from saved criterion by default + const c = i as ISavedCriterion; + this.modifier = c.modifier; + this.decodeValue(c.value); + } + + public setFromSavedCriterion(criterion: unknown) { + const c = criterion as ISavedCriterion; + if (c.value !== undefined && c.value !== null) { + this.value = c.value; + } + this.modifier = c.modifier; } public applyToCriterionInput(input: Record) { input[this.criterionOption.type] = this.toCriterionInput(); } - public toCriterionInput(): unknown { + // TODO - saved criterion _should_ be criterion input + // kicking this can down the road a little further + public applyToSavedCriterion(input: Record): void { + input[this.criterionOption.type] = { + value: this.value, + modifier: this.modifier, + }; + } + + protected toCriterionInput(): unknown { return { value: this.value, modifier: this.modifier, @@ -755,6 +786,33 @@ export function createMandatoryNumberCriterionOption( return new MandatoryNumberCriterionOption(messageID ?? value, value); } +export function encodeRangeValue( + modifier: CriterionModifier, + value: IRangeValue +): unknown { + // only encode value2 if modifier is between/not between + if ( + modifier === CriterionModifier.Between || + modifier === CriterionModifier.NotBetween + ) { + return { value: value.value, value2: value.value2 }; + } + + return { value: value.value }; +} + +export function decodeRangeValue(v: { + value: V | IRangeValue; + value2?: V; +}): IRangeValue { + // handle backwards compatible value + if (typeof v.value === "object") { + return v.value as IRangeValue; + } else { + return { value: v.value, value2: v.value2 }; + } +} + export class NumberCriterion extends ModifierCriterion { constructor(type: ModifierCriterionOption) { super(type, { value: undefined, value2: undefined }); @@ -787,6 +845,19 @@ export class NumberCriterion extends ModifierCriterion { }; } + public setFromSavedCriterion(c: { + modifier: CriterionModifier; + value: number | INumberValue; + value2?: number; + }) { + super.setFromSavedCriterion(c); + // this.value = decodeRangeValue(c); + } + + protected encodeValue(): unknown { + return encodeRangeValue(this.modifier, this.value); + } + protected getLabelValue(_intl: IntlShape) { const { value, value2 } = this.value; if ( @@ -867,6 +938,19 @@ export class DurationCriterion extends ModifierCriterion { }; } + public setFromSavedCriterion(c: { + modifier: CriterionModifier; + value: number | INumberValue; + value2?: number; + }) { + super.setFromSavedCriterion(c); + // this.value = decodeRangeValue(c); + } + + protected encodeValue(): unknown { + return encodeRangeValue(this.modifier, this.value); + } + protected getLabelValue(_intl: IntlShape) { const value = TextUtils.secondsToTimestamp(this.value.value ?? 0); const value2 = TextUtils.secondsToTimestamp(this.value.value2 ?? 0); @@ -940,17 +1024,23 @@ export class DateCriterion extends ModifierCriterion { this.value = { ...this.value }; } - public encodeValue() { - return { - value: this.value.value, - value2: this.value.value2, - }; + public setFromSavedCriterion(c: { + modifier: CriterionModifier; + value: string | IDateValue; + value2?: string; + }) { + super.setFromSavedCriterion(c); + // this.value = decodeRangeValue(c); + } + + protected encodeValue(): unknown { + return encodeRangeValue(this.modifier, this.value); } public toCriterionInput(): DateCriterionInput { return { modifier: this.modifier, - value: this.value?.value, + value: this.value?.value ?? "", value2: this.value?.value2, }; } @@ -1043,23 +1133,29 @@ export class TimestampCriterion extends ModifierCriterion { this.value = { ...this.value }; } - public encodeValue() { - return { - value: this.value?.value, - value2: this.value?.value2, - }; - } - public toCriterionInput(): TimestampCriterionInput { return { modifier: this.modifier, - value: this.transformValueToInput(this.value.value), + value: this.transformValueToInput(this.value.value ?? ""), value2: this.value.value2 ? this.transformValueToInput(this.value.value2) : null, }; } + public setFromSavedCriterion(c: { + modifier: CriterionModifier; + value: string | ITimestampValue; + value2?: string; + }) { + this.setFromSavedCriterion(c); + // this.value = decodeRangeValue(c); + } + + protected encodeValue(): unknown { + return encodeRangeValue(this.modifier, this.value); + } + protected getLabelValue() { const { value } = this.value; return this.modifier === CriterionModifier.Between || diff --git a/ui/v2.5/src/models/list-filter/criteria/custom-fields.ts b/ui/v2.5/src/models/list-filter/criteria/custom-fields.ts index 7a4503cbe..7f2e4f312 100644 --- a/ui/v2.5/src/models/list-filter/criteria/custom-fields.ts +++ b/ui/v2.5/src/models/list-filter/criteria/custom-fields.ts @@ -27,6 +27,10 @@ export class CustomFieldsCriterion extends Criterion { input.custom_fields = cloneDeep(this.value); } + public applyToSavedCriterion(input: Record): void { + input.custom_fields = cloneDeep(this.value); + } + public getLabel(intl: IntlShape): string { // show first criterion if (this.value.length === 0) { @@ -91,19 +95,20 @@ export class CustomFieldsCriterion extends Criterion { ); } - public toJSON(): string { + public toQueryParams(): Record { const encodedCriterion = { type: this.criterionOption.type, value: this.value, }; - return JSON.stringify(encodedCriterion); + return encodedCriterion; } - public setFromSavedCriterion(criterion: { - type: string; - value: CustomFieldCriterionInput[]; - }): void { - const { value } = criterion; - this.value = cloneDeep(value); + public fromDecodedParams(i: unknown): void { + const criterion = i as { value: CustomFieldCriterionInput[] }; + this.value = cloneDeep(criterion.value); + } + + public setFromSavedCriterion(input: CustomFieldCriterionInput[]): void { + this.value = cloneDeep(input); } } diff --git a/ui/v2.5/src/models/list-filter/criteria/rating.ts b/ui/v2.5/src/models/list-filter/criteria/rating.ts index cc5d7e091..36f646e21 100644 --- a/ui/v2.5/src/models/list-filter/criteria/rating.ts +++ b/ui/v2.5/src/models/list-filter/criteria/rating.ts @@ -10,7 +10,11 @@ import { IntCriterionInput, } from "src/core/generated-graphql"; import { INumberValue } from "../types"; -import { ModifierCriterion, ModifierCriterionOption } from "./criterion"; +import { + encodeRangeValue, + ModifierCriterion, + ModifierCriterionOption, +} from "./criterion"; const modifierOptions = [ CriterionModifier.Equals, @@ -72,6 +76,19 @@ export class RatingCriterion extends ModifierCriterion { }; } + public setFromSavedCriterion(c: { + modifier: CriterionModifier; + value: number | INumberValue; + value2?: number; + }) { + super.setFromSavedCriterion(c); + // this.value = decodeRangeValue(c); + } + + protected encodeValue(): unknown { + return encodeRangeValue(this.modifier, this.value); + } + protected getLabelValue() { const { value, value2 } = this.value; if ( diff --git a/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts index 2d5ecd313..1800930fb 100644 --- a/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts +++ b/ui/v2.5/src/models/list-filter/criteria/stash-ids.ts @@ -5,7 +5,11 @@ import { StashIdCriterionInput, } from "src/core/generated-graphql"; import { IStashIDValue } from "../types"; -import { ModifierCriterion, ModifierCriterionOption } from "./criterion"; +import { + ISavedCriterion, + ModifierCriterion, + ModifierCriterionOption, +} from "./criterion"; export const StashIDCriterionOption = new ModifierCriterionOption({ messageID: "stash_id", @@ -90,7 +94,29 @@ export class StashIDCriterion extends ModifierCriterion { return ret; } - public toJSON() { + public setFromSavedCriterion( + criterion: StashIdCriterionInput | ISavedCriterion + ) { + super.setFromSavedCriterion(criterion); + + // const asStashIDValue = criterion as StashIdCriterionInput; + // const asSavedCriterion = + // criterion as ISavedCriterion; + // if (asStashIDValue.endpoint || asStashIDValue.stash_id) { + // this.value = { + // endpoint: asStashIDValue.endpoint ?? "", + // stashID: asStashIDValue.stash_id ?? "", + // }; + // } else if (asSavedCriterion.value) { + // this.value = { + // endpoint: asSavedCriterion.value.endpoint ?? "", + // stashID: asSavedCriterion.value.stash_id ?? "", + // }; + // } + } + + public toQueryParams(): Record { + super.toQueryParams(); let encodedCriterion; if ( (this.modifier === CriterionModifier.IsNull || @@ -108,7 +134,7 @@ export class StashIDCriterion extends ModifierCriterion { modifier: this.modifier, }; } - return JSON.stringify(encodedCriterion); + return encodedCriterion; } public isValid(): boolean { diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index fda673149..e63910ca8 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -5,11 +5,7 @@ import { SavedFilterDataFragment, SortDirectionEnum, } from "src/core/generated-graphql"; -import { - Criterion, - CriterionValue, - ISavedCriterion, -} from "./criteria/criterion"; +import { Criterion } from "./criteria/criterion"; import { getFilterOptions } from "./factory"; import { CriterionType, DisplayMode, SavedUIOptions } from "./types"; import { ListFilterOptions } from "./filter-options"; @@ -159,7 +155,7 @@ export class ListFilterModel { JSON.parse(jsonString); const criterion = this.makeCriterion(criterionType); - criterion.setFromSavedCriterion(savedCriterion); + criterion.fromDecodedParams(savedCriterion); this.criteria.push(criterion); } catch (err) { @@ -304,7 +300,7 @@ export class ListFilterModel { if (objectFilter) { for (const [k, v] of Object.entries(objectFilter)) { const criterion = this.makeCriterion(k as CriterionType); - criterion.setFromSavedCriterion(v as ISavedCriterion); + criterion.setFromSavedCriterion(v); this.criteria.push(criterion); } } @@ -335,7 +331,11 @@ export class ListFilterModel { // Returns query parameters with necessary parts URL-encoded public getEncodedParams(): IEncodedParams { const encodedCriteria: string[] = this.criteria.map((criterion) => { - let str = ListFilterModel.translateJSON(criterion.toJSON(), false); + const queryParams = criterion.toQueryParams(); + let str = ListFilterModel.translateJSON( + JSON.stringify(queryParams), + false + ); // URL-encode other characters str = encodeURI(str); @@ -447,6 +447,15 @@ export class ListFilterModel { return output; } + // TODO - this needs to just use makeFilter, but it needs a migration + public makeSavedFilter() { + const output: Record = {}; + for (const c of this.criteria) { + c.applyToSavedCriterion(output); + } + return output; + } + public makeSavedUIOptions(): SavedUIOptions { return { display_mode: this.displayMode, diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index 737965d1e..83ebaa010 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -39,11 +39,14 @@ export interface IHierarchicalLabelValue { depth: number; } -export interface INumberValue { - value: number | undefined; - value2: number | undefined; +export interface IRangeValue { + value: V | undefined; + value2: V | undefined; } +export type INumberValue = IRangeValue; +export type IDateValue = IRangeValue; +export type ITimestampValue = IRangeValue; export interface IPHashDuplicationValue { duplicated: boolean; distance?: number; // currently not implemented @@ -54,16 +57,6 @@ export interface IStashIDValue { stashID: string; } -export interface IDateValue { - value: string; - value2: string | undefined; -} - -export interface ITimestampValue { - value: string; - value2: string | undefined; -} - export interface IPhashDistanceValue { value: string; distance?: number;