@@ -18,7 +18,7 @@
- @if (IsEmptySelected) {
+ @if (isEmptySelected()) {
@if (predicateType$ | async; as predicateType) {
@switch (predicateType) {
@case (PredicateType.Text) {
@@ -50,7 +50,7 @@
@@ -62,10 +62,11 @@
- @if (UiLabel !== null) {
- {{t(UiLabel.unit)}}
- @if (UiLabel.tooltip) {
-
+ @let label = uiLabel();
+ @if (label !== null) {
+ {{t(label.unit)}}
+ @if (label.tooltip) {
+
}
}
diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts
index d17df39fb..83c87c2cc 100644
--- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts
+++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts
@@ -2,28 +2,34 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
+ computed,
DestroyRef,
EventEmitter,
inject,
+ Injector,
+ input,
Input,
OnInit,
Output,
+ Signal,
} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {FilterStatement} from '../../../_models/metadata/v2/filter-statement';
-import {BehaviorSubject, distinctUntilChanged, filter, Observable, of, startWith, switchMap, tap} from 'rxjs';
+import {BehaviorSubject, distinctUntilChanged, filter, map, Observable, of, startWith, switchMap, tap} from 'rxjs';
import {MetadataService} from 'src/app/_services/metadata.service';
import {FilterComparison} from 'src/app/_models/metadata/v2/filter-comparison';
-import {allSeriesFilterFields, FilterField} from 'src/app/_models/metadata/v2/filter-field';
+import {FilterField} from 'src/app/_models/metadata/v2/filter-field';
import {AsyncPipe} from "@angular/common";
import {FilterFieldPipe} from "../../../_pipes/filter-field.pipe";
import {FilterComparisonPipe} from "../../../_pipes/filter-comparison.pipe";
-import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
+import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
import {Select2, Select2Option} from "ng-select2-component";
import {NgbDate, NgbDateParserFormatter, NgbInputDatepicker, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe";
import {AgeRatingPipe} from "../../../_pipes/age-rating.pipe";
+import {ValidFilterEntity} from "../../filter-settings";
+import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
enum PredicateType {
Text = 1,
@@ -131,31 +137,25 @@ const BooleanComparisons = [
],
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class MetadataFilterRowComponent implements OnInit {
-
- protected readonly FilterComparison = FilterComparison;
- protected readonly PredicateType = PredicateType;
+export class MetadataFilterRowComponent
implements OnInit {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
private readonly dateParser = inject(NgbDateParserFormatter);
private readonly metadataService = inject(MetadataService);
private readonly translocoService = inject(TranslocoService);
+ private readonly filterUtilitiesService = inject(FilterUtilitiesService);
+ private readonly injector = inject(Injector);
-
- @Input() index: number = 0; // This is only for debugging
/**
* Slightly misleading as this is the initial state and will be updated on the filterStatement event emitter
*/
- @Input() preset!: FilterStatement;
- @Input() availableFields: Array = allSeriesFilterFields;
- @Output() filterStatement = new EventEmitter>();
+ @Input() preset!: FilterStatement;
+ entityType = input.required();
+ @Output() filterStatement = new EventEmitter>();
- formGroup: FormGroup = new FormGroup({
- 'comparison': new FormControl(FilterComparison.Equal, []),
- 'filterValue': new FormControl('', []),
- });
+ formGroup!: FormGroup;
validComparisons$: BehaviorSubject = new BehaviorSubject([FilterComparison.Equal] as FilterComparison[]);
predicateType$: BehaviorSubject = new BehaviorSubject(PredicateType.Text as PredicateType);
dropdownOptions$ = of([]);
@@ -164,25 +164,57 @@ export class MetadataFilterRowComponent implements OnInit {
private readonly mangaFormatPipe = new MangaFormatPipe(this.translocoService);
private readonly ageRatingPipe = new AgeRatingPipe();
- get IsEmptySelected() {
- return parseInt(this.formGroup.get('comparison')?.value + '', 10) !== FilterComparison.IsEmpty;
- }
+ private comparisonSignal!: Signal;
+ private inputSignal!: Signal;
- get UiLabel(): FilterRowUi | null {
- const field = parseInt(this.formGroup.get('input')!.value, 10) as FilterField;
- if (!unitLabels.has(field)) return null;
- return unitLabels.get(field) as FilterRowUi;
- }
-
- get MultipleDropdownAllowed() {
- const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison;
- return comp === FilterComparison.Contains || comp === FilterComparison.NotContains || comp === FilterComparison.MustContains;
- }
+ isEmptySelected: Signal = computed(() => false);
+ uiLabel: Signal = computed(() => null);
+ isMultiSelectDropdownAllowed: Signal = computed(() => false);
+ sortFieldOptions = computed(() => []);
+ filterFieldOptions = computed(() => []);
ngOnInit() {
- this.formGroup.addControl('input', new FormControl(FilterField.SeriesName, []));
+
+ this.formGroup = new FormGroup({
+ 'comparison': new FormControl(FilterComparison.Equal, []),
+ 'filterValue': new FormControl('', []),
+ 'input': new FormControl(this.filterUtilitiesService.getDefaultFilterField(this.entityType()), [])
+ });
+
+ this.comparisonSignal = toSignal(
+ this.formGroup.get('comparison')!.valueChanges.pipe(
+ startWith(this.formGroup.get('comparison')!.value),
+ map(d => parseInt(d + '', 10) as FilterComparison)
+ )
+ , {requireSync: true, injector: this.injector});
+ this.inputSignal = toSignal(
+ this.formGroup.get('input')!.valueChanges.pipe(
+ startWith(this.formGroup.get('input')!.value),
+ map(d => parseInt(d + '', 10) as TFilter)
+ )
+ , {requireSync: true, injector: this.injector});
+
+ this.isEmptySelected = computed(() => this.comparisonSignal() !== FilterComparison.IsEmpty);
+ this.uiLabel = computed(() => {
+ if (!unitLabels.has(this.inputSignal())) return null;
+ return unitLabels.get(this.inputSignal()) as FilterRowUi;
+ });
+
+ this.isMultiSelectDropdownAllowed = computed(() => {
+ return this.comparisonSignal() === FilterComparison.Contains || this.comparisonSignal() === FilterComparison.NotContains || this.comparisonSignal() === FilterComparison.MustContains;
+ });
+
+ this.sortFieldOptions = computed(() => {
+ return this.filterUtilitiesService.getSortFields(this.entityType());
+ });
+ this.filterFieldOptions = computed(() => {
+ return this.filterUtilitiesService.getFilterFields(this.entityType());
+ });
+
+
+ //this.formGroup.addControl('input', new FormControl(FilterField.SeriesName, []));
this.formGroup.get('input')?.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe((val: string) => this.handleFieldChange(val));
this.populateFromPreset();
@@ -215,7 +247,7 @@ export class MetadataFilterRowComponent implements OnInit {
propagateFilterUpdate() {
const stmt = {
comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison,
- field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField,
+ field: parseInt(this.formGroup.get('input')?.value, 10) as TFilter,
value: this.formGroup.get('filterValue')?.value!
};
@@ -252,7 +284,7 @@ export class MetadataFilterRowComponent implements OnInit {
this.formGroup.get('filterValue')?.patchValue(this.dateParser.parse(val));
}
else if (DropdownFields.includes(this.preset.field)) {
- if (this.MultipleDropdownAllowed || val.includes(',')) {
+ if (this.isMultiSelectDropdownAllowed() || val.includes(',')) {
this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10)));
} else {
if (this.preset.field === FilterField.Languages) {
@@ -383,4 +415,7 @@ export class MetadataFilterRowComponent implements OnInit {
updateIfDateFilled() {
this.propagateFilterUpdate();
}
+
+ protected readonly FilterComparison = FilterComparison;
+ protected readonly PredicateType = PredicateType;
}
diff --git a/UI/Web/src/app/metadata-filter/filter-settings.ts b/UI/Web/src/app/metadata-filter/filter-settings.ts
index d2ab26614..c9a253d26 100644
--- a/UI/Web/src/app/metadata-filter/filter-settings.ts
+++ b/UI/Web/src/app/metadata-filter/filter-settings.ts
@@ -4,6 +4,11 @@ import {PersonSortField} from "../_models/metadata/v2/person-sort-field";
import {PersonFilterField} from "../_models/metadata/v2/person-filter-field";
import {FilterField} from "../_models/metadata/v2/filter-field";
+/**
+ * The set of entities that are supported for rich filtering. Each entity must have its own distinct SortField and FilterField enums.
+ */
+export type ValidFilterEntity = 'series' | 'person';
+
export class FilterSettingsBase {
presetsV2: FilterV2 | undefined;
sortDisabled = false;
@@ -12,20 +17,21 @@ export class FilterSettingsBase {
- type = 'sortField';
+ type: ValidFilterEntity = 'series';
}
/**
* Filter Settings for People entity
*/
export class PersonFilterSettings extends FilterSettingsBase {
- type = 'personSortField';
+ type: ValidFilterEntity = 'person';
}
diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html
index add2b4e94..6e5189728 100644
--- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html
+++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html
@@ -26,8 +26,8 @@
-
+
-