Refactored to make the FilterSettings genetic and type safe. Now I need to refactor the metadata component to respect the generics.

This commit is contained in:
Joseph Milazzo 2025-06-09 18:27:06 -05:00
parent 2654ea2965
commit a4c31debd0
20 changed files with 117 additions and 65 deletions

View file

@ -23,7 +23,7 @@ export enum SortField {
Random = 9
}
export const allSortFields = Object.keys(SortField)
export const allSeriesSortFields = Object.keys(SortField)
.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0)
.map(key => parseInt(key, 10)) as SortField[];

View file

@ -48,7 +48,7 @@ const enumArray = Object.keys(FilterField)
enumArray.sort((a, b) => a.value.localeCompare(b.value));
export const allFields = enumArray
export const allSeriesFilterFields = enumArray
.map(key => parseInt(key.key, 10))as FilterField[];
export const allPeople = [

View file

@ -2,10 +2,10 @@ import {FilterStatement} from "./filter-statement";
import {FilterCombination} from "./filter-combination";
import {SortOptions} from "./sort-options";
export interface FilterV2<T> {
export interface FilterV2<TFilter, TSort extends number = number> {
name?: string;
statements: Array<FilterStatement<T>>;
statements: Array<FilterStatement<TFilter>>;
combination: FilterCombination;
sortOptions?: SortOptions;
sortOptions?: SortOptions<TSort>;
limitTo: number;
}

View file

@ -3,3 +3,9 @@ export enum PersonFilterField {
Role = 1,
Name = 2
}
export const allPersonFilterFields = Object.keys(PersonFilterField)
.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0)
.map(key => parseInt(key, 10)) as PersonFilterField[];

View file

@ -3,3 +3,7 @@ export enum PersonSortField {
SeriesCount = 2,
ChapterCount = 3
}
export const allPersonSortFields = Object.keys(PersonSortField)
.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0)
.map(key => parseInt(key, 10)) as PersonSortField[];

View file

@ -1,11 +1,10 @@
import {SortField} from "../series-filter";
import {PersonSortField} from "./person-sort-field";
/**
* Series-based Sort options
*/
export interface SortOptions {
sortField: SortField;
export interface SortOptions<TSort extends number = number> {
sortField: TSort;
isAscending: boolean;
}

View file

@ -25,5 +25,4 @@ export class FilterService {
renameSmartFilter(filter: SmartFilter) {
return this.httpClient.post(this.baseUrl + `filter/rename?filterId=${filter.id}&name=${filter.name.trim()}`, {});
}
}

View file

@ -227,5 +227,4 @@ export class SeriesService {
updateDontMatch(seriesId: number, dontMatch: boolean) {
return this.httpClient.post<string>(this.baseUrl + `series/dont-match?seriesId=${seriesId}&dontMatch=${dontMatch}`, {}, TextResonse);
}
}

View file

@ -11,13 +11,12 @@ import {Title} from '@angular/platform-browser';
import {ActivatedRoute, Router} from '@angular/router';
import {debounceTime, take} from 'rxjs/operators';
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
import {FilterSettings} from 'src/app/metadata-filter/filter-settings';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
import {UtilityService} from 'src/app/shared/_services/utility.service';
import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
import {Pagination} from 'src/app/_models/pagination';
import {Series} from 'src/app/_models/series';
import {FilterEvent} from 'src/app/_models/metadata/series-filter';
import {FilterEvent, SortField} from 'src/app/_models/metadata/series-filter';
import {Action, ActionItem} from 'src/app/_services/action-factory.service';
import {ActionService} from 'src/app/_services/action.service';
import {JumpbarService} from 'src/app/_services/jumpbar.service';
@ -38,6 +37,7 @@ import {BrowseTitlePipe} from "../../../_pipes/browse-title.pipe";
import {MetadataService} from "../../../_services/metadata.service";
import {Observable} from "rxjs";
import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {SeriesFilterSettings} from "../../../metadata-filter/filter-settings";
@Component({
@ -68,8 +68,8 @@ export class AllSeriesComponent implements OnInit {
series: Series[] = [];
loadingSeries = false;
pagination: Pagination = new Pagination();
filter: FilterV2<FilterField> | undefined = undefined;
filterSettings: FilterSettings<FilterField> = new FilterSettings();
filter: FilterV2<FilterField, SortField> | undefined = undefined;
filterSettings: SeriesFilterSettings = new SeriesFilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActiveCheck!: FilterV2<FilterField>;
filterActive: boolean = false;

View file

@ -3,7 +3,6 @@ import {ActivatedRoute, Router} from '@angular/router';
import {ToastrService} from 'ngx-toastr';
import {take} from 'rxjs';
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
import {FilterSettings} from 'src/app/metadata-filter/filter-settings';
import {ConfirmService} from 'src/app/shared/confirm.service';
import {DownloadService} from 'src/app/shared/_services/download.service';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
@ -28,6 +27,7 @@ import {FilterV2} from "../../../_models/metadata/v2/filter-v2";
import {Title} from "@angular/platform-browser";
import {WikiLink} from "../../../_models/wiki";
import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {SeriesFilterSettings} from "../../../metadata-filter/filter-settings";
@Component({
selector: 'app-bookmarks',
@ -65,7 +65,7 @@ export class BookmarksComponent implements OnInit {
pagination: Pagination = new Pagination();
filter: FilterV2<FilterField> | undefined = undefined;
filterSettings: FilterSettings<FilterField> = new FilterSettings();
filterSettings: SeriesFilterSettings = new SeriesFilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false;
filterActiveCheck!: FilterV2<FilterField>;

View file

@ -32,10 +32,10 @@ import {debounceTime, tap} from "rxjs/operators";
import {SortButtonComponent} from "../../_single-module/sort-button/sort-button.component";
import {PersonSortField} from "../../_models/metadata/v2/person-sort-field";
import {PersonSortOptions} from "../../_models/metadata/v2/sort-options";
import {FilterSettings} from "../../metadata-filter/filter-settings";
import {PersonFilterField} from "../../_models/metadata/v2/person-filter-field";
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
import {FilterV2} from "../../_models/metadata/v2/filter-v2";
import {PersonFilterSettings} from "../../metadata-filter/filter-settings";
@Component({
@ -83,7 +83,7 @@ export class BrowseAuthorsComponent implements OnInit {
query: new FormControl('', []),
});
isAscending: boolean = true;
filterSettings: FilterSettings<PersonFilterField> = new FilterSettings<PersonFilterField>();
filterSettings: PersonFilterSettings = new PersonFilterSettings();
filterActive: boolean = false;
filterOpen: EventEmitter<boolean> = new EventEmitter();
filter: FilterV2<PersonFilterField> | undefined = undefined;

View file

@ -22,7 +22,6 @@ import {
} from '@angular/core';
import {NavigationStart, Router} from '@angular/router';
import {VirtualScrollerComponent, VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {FilterSettings} from 'src/app/metadata-filter/filter-settings';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
import {JumpKey} from 'src/app/_models/jumpbar/jump-key';
@ -39,6 +38,7 @@ import {filter, map} from "rxjs/operators";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {tap} from "rxjs";
import {FilterV2} from "../../_models/metadata/v2/filter-v2";
import {FilterSettingsBase, SeriesFilterSettings} from "../../metadata-filter/filter-settings";
const ANIMATION_TIME_MS = 0;
@ -49,7 +49,7 @@ const ANIMATION_TIME_MS = 0;
* How to use:
* - For filtering:
* - pass a filterSettings which will bootstrap the filtering bar
* - pass a jumpbar method binding to calc the count for the entity
* - pass a jumpbar method binding to calc the count for the entity (not implemented yet)
* - For card layout
* - Pass an identity function for trackby
* - Pass a pagination object for the total count
@ -84,7 +84,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
*/
@Input() parentScroll!: Element | Window;
// Filter Code
// We need to pass filterOpen from the grandfather to the metadata filter due to the filter button being in a separate component
@Input() filterOpen!: EventEmitter<boolean>;
/**
* Should filtering be shown on the page
@ -98,7 +98,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
* A trackBy to help with rendering. This is required as without it there are issues when scrolling
*/
@Input({required: true}) trackByIdentity!: TrackByFunction<any>;
@Input() filterSettings!: FilterSettings<number>;
@Input() filterSettings!: FilterSettingsBase;
@Input() refresh!: EventEmitter<void>;
/**
* Pass the filter object optionally. If not passed, will create a SeriesFilter by default
@ -150,7 +150,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
// }
if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings();
this.filterSettings = new SeriesFilterSettings();
this.cdRef.markForCheck();
}

View file

@ -19,7 +19,6 @@ import {ToastrService} from 'ngx-toastr';
import {debounceTime, take} from 'rxjs/operators';
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
import {EditCollectionTagsComponent} from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
import {FilterSettings} from 'src/app/metadata-filter/filter-settings';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
import {UserCollection} from 'src/app/_models/collection-tag';
@ -63,6 +62,7 @@ import {DefaultModalOptions} from "../../../_models/default-modal-options";
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
import {PromotedIconComponent} from "../../../shared/_components/promoted-icon/promoted-icon.component";
import {FilterStatement} from "../../../_models/metadata/v2/filter-statement";
import {SeriesFilterSettings} from "../../../metadata-filter/filter-settings";
@Component({
selector: 'app-collection-detail',
@ -111,7 +111,7 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
pagination: Pagination = new Pagination();
collectionTagActions: ActionItem<UserCollection>[] = [];
filter: FilterV2<FilterField> | undefined = undefined;
filterSettings: FilterSettings<FilterField> = new FilterSettings();
filterSettings: SeriesFilterSettings = new SeriesFilterSettings();
summary: string = '';
user!: User;

View file

@ -25,7 +25,6 @@ import {EVENTS, MessageHubService} from '../_services/message-hub.service';
import {SeriesService} from '../_services/series.service';
import {NavService} from '../_services/nav.service';
import {FilterUtilitiesService} from '../shared/_services/filter-utilities.service';
import {FilterSettings} from '../metadata-filter/filter-settings';
import {JumpKey} from '../_models/jumpbar/jump-key';
import {SeriesRemovedEvent} from '../_models/events/series-removed-event';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@ -43,6 +42,7 @@ import {FilterField} from "../_models/metadata/v2/filter-field";
import {CardActionablesComponent} from "../_single-module/card-actionables/card-actionables.component";
import {LoadingComponent} from "../shared/loading/loading.component";
import {debounceTime, ReplaySubject, tap} from "rxjs";
import {SeriesFilterSettings} from "../metadata-filter/filter-settings";
@Component({
selector: 'app-library-detail',
@ -76,7 +76,7 @@ export class LibraryDetailComponent implements OnInit {
pagination: Pagination = {currentPage: 0, totalPages: 0, totalItems: 0, itemsPerPage: 0};
actions: ActionItem<Library>[] = [];
filter: FilterV2<FilterField> | undefined = undefined;
filterSettings: FilterSettings<FilterField> = new FilterSettings();
filterSettings: SeriesFilterSettings = new SeriesFilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false;
filterActiveCheck!: FilterV2<FilterField>;

View file

@ -18,7 +18,7 @@ import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {FilterCombination} from "../../../_models/metadata/v2/filter-combination";
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
import {allFields} from "../../../_models/metadata/v2/filter-field";
import {allSeriesFilterFields} from "../../../_models/metadata/v2/filter-field";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {distinctUntilChanged, tap} from "rxjs/operators";
import {translate, TranslocoDirective} from "@jsverse/transloco";
@ -43,7 +43,7 @@ export class MetadataBuilderComponent implements OnInit {
* The number of statements that can be. 0 means unlimited. -1 means none.
*/
@Input() statementLimit = 0;
@Input() availableFilterFields = allFields;
@Input() availableFilterFields = allSeriesFilterFields;
@Output() update: EventEmitter<FilterV2<number>> = new EventEmitter<FilterV2<number>>();
@Output() apply: EventEmitter<void> = new EventEmitter<void>();

View file

@ -14,7 +14,7 @@ import {FilterStatement} from '../../../_models/metadata/v2/filter-statement';
import {BehaviorSubject, distinctUntilChanged, filter, 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 {allFields, FilterField} from 'src/app/_models/metadata/v2/filter-field';
import {allSeriesFilterFields, 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";
@ -148,7 +148,7 @@ export class MetadataFilterRowComponent implements OnInit {
* Slightly misleading as this is the initial state and will be updated on the filterStatement event emitter
*/
@Input() preset!: FilterStatement<number>;
@Input() availableFields: Array<FilterField> = allFields;
@Input() availableFields: Array<FilterField> = allSeriesFilterFields;
@Output() filterStatement = new EventEmitter<FilterStatement<number>>();

View file

@ -1,11 +1,31 @@
import {FilterV2} from "../_models/metadata/v2/filter-v2";
import {SortField} from "../_models/metadata/series-filter";
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";
export class FilterSettings<T> {
presetsV2: FilterV2<T> | undefined;
export class FilterSettingsBase<TFilter extends number = number, TSort extends number = number> {
presetsV2: FilterV2<TFilter, TSort> | undefined;
sortDisabled = false;
/**
* The number of statements that can be on the filter. Set to 1 to disable adding more.
*/
statementLimit: number = 0;
saveDisabled: boolean = false;
}
}
/**
* Filter Settings for Series entity
*/
export class SeriesFilterSettings extends FilterSettingsBase<FilterField, SortField> {
type = 'sortField';
}
/**
* Filter Settings for People entity
*/
export class PersonFilterSettings extends FilterSettingsBase<PersonFilterField, PersonSortField> {
type = 'personSortField';
}

View file

@ -2,10 +2,12 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
ContentChild,
DestroyRef,
EventEmitter,
inject,
input,
Input,
OnInit,
Output
@ -14,9 +16,8 @@ import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular
import {NgbCollapse} from '@ng-bootstrap/ng-bootstrap';
import {Breakpoint, UtilityService} from '../shared/_services/utility.service';
import {Library} from '../_models/library/library';
import {allSortFields, FilterEvent, FilterItem, SortField} from '../_models/metadata/series-filter';
import {allSeriesSortFields, FilterEvent, FilterItem} from '../_models/metadata/series-filter';
import {ToggleService} from '../_services/toggle.service';
import {FilterSettings} from './filter-settings';
import {FilterV2} from '../_models/metadata/v2/filter-v2';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {DrawerComponent} from '../shared/drawer/drawer.component';
@ -24,11 +25,15 @@ import {AsyncPipe, NgClass, NgTemplateOutlet} from '@angular/common';
import {translate, TranslocoModule, TranslocoService} from "@jsverse/transloco";
import {SortFieldPipe} from "../_pipes/sort-field.pipe";
import {MetadataBuilderComponent} from "./_components/metadata-builder/metadata-builder.component";
import {allFields, FilterField} from "../_models/metadata/v2/filter-field";
import {allSeriesFilterFields, FilterField} from "../_models/metadata/v2/filter-field";
import {FilterService} from "../_services/filter.service";
import {ToastrService} from "ngx-toastr";
import {animate, style, transition, trigger} from "@angular/animations";
import {SortButtonComponent} from "../_single-module/sort-button/sort-button.component";
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
import {FilterSettingsBase} from "./filter-settings";
import {allPersonSortFields} from "../_models/metadata/v2/person-sort-field";
import {allPersonFilterFields} from "../_models/metadata/v2/person-filter-field";
@Component({
selector: 'app-metadata-filter',
@ -51,10 +56,11 @@ import {SortButtonComponent} from "../_single-module/sort-button/sort-button.com
ReactiveFormsModule, FormsModule, AsyncPipe, TranslocoModule,
MetadataBuilderComponent, NgClass, SortButtonComponent]
})
export class MetadataFilterComponent implements OnInit {
protected readonly allFilterFields = allFields;
export class MetadataFilterComponent<TFilter extends number = number, TSort extends number = number> implements OnInit {
protected readonly allFilterFields = allSeriesFilterFields;
private readonly destroyRef = inject(DestroyRef);
private readonly filterUtilityService = inject(FilterUtilitiesService);
public readonly utilityService = inject(UtilityService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly toastr = inject(ToastrService);
@ -63,7 +69,7 @@ export class MetadataFilterComponent implements OnInit {
protected readonly translocoService = inject(TranslocoService);
private readonly sortFieldPipe = new SortFieldPipe(this.translocoService);
protected readonly allSortFields = allSortFields.map(f => {
protected readonly allSortFields = allSeriesSortFields.map(f => {
return {title: this.sortFieldPipe.transform(f), value: f};
}).sort((a, b) => a.title.localeCompare(b.title));
@ -76,7 +82,23 @@ export class MetadataFilterComponent implements OnInit {
* Should filtering be shown on the page
*/
@Input() filteringDisabled: boolean = false;
@Input({required: true}) filterSettings!: FilterSettings<FilterField>;
//@Input({required: true}) filterSettings!: FilterSettings<T>;
@Input({required: true}) filterSettings!: FilterSettingsBase<TFilter, TSort>;
/**
* Entity type derives the Sort and Filter fields
*/
entityType = input<'series' | 'person'>('series');
sortFieldOptions = computed(() => {
if (this.entityType() === 'series') return allSeriesSortFields;
return allPersonSortFields;
});
filterFieldOptions = computed(() => {
if (this.entityType() === 'series') return allSeriesFilterFields;
return allPersonFilterFields;
});
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
@ContentChild('[ngbCollapse]') collapse!: NgbCollapse;
@ -100,7 +122,7 @@ export class MetadataFilterComponent implements OnInit {
ngOnInit(): void {
if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings();
this.filterSettings = new FilterSettingsBase<TFilter, TSort>();
this.cdRef.markForCheck();
}
@ -159,8 +181,10 @@ export class MetadataFilterComponent implements OnInit {
this.filterV2 = this.deepClone(this.filterSettings.presetsV2);
const defaultSortField = this.sortFieldOptions()[0];
this.sortGroup = new FormGroup({
sortField: new FormControl({value: this.filterV2?.sortOptions?.sortField || SortField.SortName, disabled: this.filterSettings.sortDisabled}, []),
sortField: new FormControl({value: this.filterV2?.sortOptions?.sortField || defaultSortField, disabled: this.filterSettings.sortDisabled}, []),
limitTo: new FormControl(this.filterV2?.limitTo || 0, []),
name: new FormControl(this.filterV2?.name || '', [])
});
@ -192,9 +216,11 @@ export class MetadataFilterComponent implements OnInit {
this.isAscendingSort = isAscending;
if (this.filterV2?.sortOptions === null) {
const defaultSortField = this.sortFieldOptions()[0];
this.filterV2.sortOptions = {
isAscending: this.isAscendingSort,
sortField: SortField.SortName
sortField: defaultSortField
}
}
@ -236,5 +262,6 @@ export class MetadataFilterComponent implements OnInit {
this.toggleService.set(!this.filteringCollapsed);
}
protected readonly Breakpoint = Breakpoint;
}

View file

@ -12,7 +12,6 @@ import {TextResonse} from "../../_types/text-response";
import {environment} from "../../../environments/environment";
import {map, tap} from "rxjs/operators";
import {Observable, of, switchMap} from "rxjs";
import {Location} from "@angular/common";
import {PersonFilterField} from "../../_models/metadata/v2/person-filter-field";
@ -21,12 +20,11 @@ import {PersonFilterField} from "../../_models/metadata/v2/person-filter-field";
})
export class FilterUtilitiesService {
private readonly location = inject(Location);
private readonly router = inject(Router);
private readonly metadataService = inject(MetadataService);
private readonly http = inject(HttpClient);
private apiUrl = environment.apiUrl;
private readonly apiUrl = environment.apiUrl;
encodeFilter(filter: FilterV2<number> | undefined) {
return this.http.post<string>(this.apiUrl + 'filter/encode', filter, TextResonse);

View file

@ -16,7 +16,6 @@ import {Title} from '@angular/platform-browser';
import {ActivatedRoute, Router} from '@angular/router';
import {debounceTime, take} from 'rxjs';
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
import {FilterSettings} from 'src/app/metadata-filter/filter-settings';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
import {UtilityService} from 'src/app/shared/_services/utility.service';
import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event';
@ -41,6 +40,7 @@ import {
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {FilterV2} from "../../../_models/metadata/v2/filter-v2";
import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {SeriesFilterSettings} from "../../../metadata-filter/filter-settings";
@Component({
@ -60,7 +60,7 @@ export class WantToReadComponent implements OnInit, AfterContentChecked {
series: Array<Series> = [];
pagination: Pagination = new Pagination();
filter: FilterV2<FilterField> | undefined = undefined;
filterSettings: FilterSettings<FilterField> = new FilterSettings();
filterSettings: SeriesFilterSettings = new SeriesFilterSettings();
refresh: EventEmitter<void> = new EventEmitter();
filterActiveCheck!: FilterV2<FilterField>;