Lots of Filtering Fixes & New Fields (#2244)
* Added an id for komf userscript to help it inject into Kavita's UI without relying on strings, given localization. * Still working the filter fields, there is a bug with selecting an input and it setting undefined like crazy. Path is coded but not tested or validated. * Stashing changed. Really not sure what's happening. I'm seeing 2 constructor calls for one row. I'm seeing a field change trigger 400 events. Values aren't getting set correctly on default string. I've made a ton of changes, when resuming this work, look at the diff. All of this can be reset excluding the Path work. * Lots of comments but the double instantiation is due to the mobile drawer. Added an ngIf which seems to work. * Fixed dropdown options triggering a ton of looped calls. Default limitTo to 0 when user empties blank or negative. * Removed a ton of UserId db calls from a ton of apis. Added a new API to allow UI to query a specific role to lessen load on UI. * Optimized the code on new filtering to only load people by a given role. This should speed up heavily tagged libraries. Commented out a bunch of code that's no longer used. Will be cleaned up later. * Fixed support so that library filter can handle multiple selections. * Fixed a bug when hitting enter in an input, the statement would be removed. * Fixed multi-select not resuming from url correctly. * Restored the series/all api for Tachiyomi to continue using until I'm motivated enough to update the extension. * Fixed some resuming of state with dropdowns, not always setting values in correct order. * Added FilePath Filter which lets a user search on individual files (slow, may need index) * Added a full filepath for new filtering.
This commit is contained in:
parent
69b5530a93
commit
cd84913fb9
29 changed files with 576 additions and 238 deletions
8
UI/Web/package-lock.json
generated
8
UI/Web/package-lock.json
generated
|
@ -37,7 +37,7 @@
|
|||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-select2-component": "^13.0.2",
|
||||
"ng-select2-component": "^13.0.6",
|
||||
"ngx-color-picker": "^14.0.0",
|
||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
|
@ -10558,9 +10558,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ng-select2-component": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.2.tgz",
|
||||
"integrity": "sha512-8Tms5p0V/0J0vCWOf2Vrk6tJlwbaf3D3As3iigcjRncYlfXN130agniBcZ007C3zK2KyLXJJRkEWzlCls8/TVQ==",
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ng-select2-component/-/ng-select2-component-13.0.6.tgz",
|
||||
"integrity": "sha512-CiAelglSz2aeYy0BiXRi32zc49Mq27+J1eDzTrXmf2o50MvNo3asS3NRVQcnSldo/zLcJafWCMueVfjVaV1etw==",
|
||||
"dependencies": {
|
||||
"ngx-infinite-scroll": ">=16.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-select2-component": "^13.0.2",
|
||||
"ng-select2-component": "^13.0.6",
|
||||
"ngx-color-picker": "^14.0.0",
|
||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
|
|
|
@ -24,7 +24,9 @@ export enum FilterField
|
|||
ReadProgress = 20,
|
||||
Formats = 21,
|
||||
ReleaseYear = 22,
|
||||
ReadTime = 23
|
||||
ReadTime = 23,
|
||||
Path = 24,
|
||||
FilePath = 25
|
||||
}
|
||||
|
||||
export const allFields = Object.keys(FilterField)
|
||||
|
|
|
@ -8,7 +8,7 @@ import {AgeRating} from '../_models/metadata/age-rating';
|
|||
import {AgeRatingDto} from '../_models/metadata/age-rating-dto';
|
||||
import {Language} from '../_models/metadata/language';
|
||||
import {PublicationStatusDto} from '../_models/metadata/publication-status-dto';
|
||||
import {Person} from '../_models/metadata/person';
|
||||
import {Person, PersonRole} from '../_models/metadata/person';
|
||||
import {Tag} from '../_models/tag';
|
||||
import {TextResonse} from '../_types/text-response';
|
||||
import {FilterComparison} from '../_models/metadata/v2/filter-comparison';
|
||||
|
@ -33,44 +33,44 @@ export class MetadataService {
|
|||
|
||||
constructor(private httpClient: HttpClient, private router: Router) { }
|
||||
|
||||
applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
|
||||
const dto: SeriesFilterV2 = {
|
||||
statements: [this.createDefaultFilterStatement(filter, comparison, value + '')],
|
||||
combination: FilterCombination.Or,
|
||||
limitTo: 0
|
||||
};
|
||||
//
|
||||
// console.log('navigating to: ', this.filterUtilityService.urlFromFilterV2(page.join('/'), dto));
|
||||
// this.router.navigateByUrl(this.filterUtilityService.urlFromFilterV2(page.join('/'), dto));
|
||||
// applyFilter(page: Array<any>, filter: FilterField, comparison: FilterComparison, value: string) {
|
||||
// const dto: SeriesFilterV2 = {
|
||||
// statements: [this.createDefaultFilterStatement(filter, comparison, value + '')],
|
||||
// combination: FilterCombination.Or,
|
||||
// limitTo: 0
|
||||
// };
|
||||
// //
|
||||
// // console.log('navigating to: ', this.filterUtilityService.urlFromFilterV2(page.join('/'), dto));
|
||||
// // this.router.navigateByUrl(this.filterUtilityService.urlFromFilterV2(page.join('/'), dto));
|
||||
//
|
||||
// // Creates a temp name for the filter
|
||||
// this.httpClient.post<string>(this.baseUrl + 'filter/create-temp', dto, TextResonse).pipe(map(name => {
|
||||
// dto.name = name;
|
||||
// }), switchMap((_) => {
|
||||
// let params: any = {};
|
||||
// params['filterName'] = dto.name;
|
||||
// return this.router.navigate(page, {queryParams: params});
|
||||
// })).subscribe();
|
||||
//
|
||||
// }
|
||||
|
||||
// Creates a temp name for the filter
|
||||
this.httpClient.post<string>(this.baseUrl + 'filter/create-temp', dto, TextResonse).pipe(map(name => {
|
||||
dto.name = name;
|
||||
}), switchMap((_) => {
|
||||
let params: any = {};
|
||||
params['filterName'] = dto.name;
|
||||
return this.router.navigate(page, {queryParams: params});
|
||||
})).subscribe();
|
||||
// getFilter(filterName: string) {
|
||||
// return this.httpClient.get<SeriesFilterV2>(this.baseUrl + 'filter?name=' + filterName);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
getFilter(filterName: string) {
|
||||
return this.httpClient.get<SeriesFilterV2>(this.baseUrl + 'filter?name=' + filterName);
|
||||
}
|
||||
|
||||
getAgeRating(ageRating: AgeRating) {
|
||||
if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
|
||||
return of(this.ageRatingTypes[ageRating]);
|
||||
}
|
||||
return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, TextResonse).pipe(map(ratingString => {
|
||||
if (this.ageRatingTypes === undefined) {
|
||||
this.ageRatingTypes = {};
|
||||
}
|
||||
|
||||
this.ageRatingTypes[ageRating] = ratingString;
|
||||
return this.ageRatingTypes[ageRating];
|
||||
}));
|
||||
}
|
||||
// getAgeRating(ageRating: AgeRating) {
|
||||
// if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
|
||||
// return of(this.ageRatingTypes[ageRating]);
|
||||
// }
|
||||
// return this.httpClient.get<string>(this.baseUrl + 'series/age-rating?ageRating=' + ageRating, TextResonse).pipe(map(ratingString => {
|
||||
// if (this.ageRatingTypes === undefined) {
|
||||
// this.ageRatingTypes = {};
|
||||
// }
|
||||
//
|
||||
// this.ageRatingTypes[ageRating] = ratingString;
|
||||
// return this.ageRatingTypes[ageRating];
|
||||
// }));
|
||||
// }
|
||||
|
||||
getAllAgeRatings(libraries?: Array<number>) {
|
||||
let method = 'metadata/age-ratings'
|
||||
|
@ -132,10 +132,14 @@ export class MetadataService {
|
|||
return this.httpClient.get<Array<Person>>(this.baseUrl + method);
|
||||
}
|
||||
|
||||
getChapterSummary(chapterId: number) {
|
||||
return this.httpClient.get<string>(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse);
|
||||
getAllPeopleByRole(role: PersonRole) {
|
||||
return this.httpClient.get<Array<Person>>(this.baseUrl + 'metadata/people-by-role?role=' + role);
|
||||
}
|
||||
|
||||
// getChapterSummary(chapterId: number) {
|
||||
// return this.httpClient.get<string>(this.baseUrl + 'metadata/chapter-summary?chapterId=' + chapterId, TextResonse);
|
||||
// }
|
||||
|
||||
createDefaultFilterDto(): SeriesFilterV2 {
|
||||
return {
|
||||
statements: [] as FilterStatement[],
|
||||
|
@ -159,6 +163,6 @@ export class MetadataService {
|
|||
updateFilter(arr: Array<FilterStatement>, index: number, filterStmt: FilterStatement) {
|
||||
arr[index].comparison = filterStmt.comparison;
|
||||
arr[index].field = filterStmt.field;
|
||||
arr[index].value = filterStmt.value + '';
|
||||
arr[index].value = filterStmt.value ? filterStmt.value + '' : '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export class SeriesService {
|
|||
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||
const data = filter || {};
|
||||
|
||||
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all', data, {observe: 'response', params}).pipe(
|
||||
return this.httpClient.post<PaginatedResult<Series[]>>(this.baseUrl + 'series/all-v2', data, {observe: 'response', params}).pipe(
|
||||
map((response: any) => {
|
||||
return this.utilityService.createPaginatedResult(response, this.paginatedResults);
|
||||
})
|
||||
|
|
|
@ -115,12 +115,14 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('[card-detail-layout] ngOnInit')
|
||||
if (this.trackByIdentity === undefined) {
|
||||
this.trackByIdentity = (_: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`;
|
||||
}
|
||||
|
||||
if (this.filterSettings === undefined) {
|
||||
this.filterSettings = new FilterSettings();
|
||||
console.log('[card-detail-layout] creating blank FilterSettings');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
@ -178,6 +180,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
|
|||
this.applyFilter.emit(event);
|
||||
this.updateApplied++;
|
||||
this.filter = event.filterV2;
|
||||
console.log('[card-detail-layout] apply filter')
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
|
|
@ -135,6 +135,11 @@ export class LibraryDetailComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
get Debug() {
|
||||
console.log('rendered section ');
|
||||
return 0;
|
||||
}
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private seriesService: SeriesService,
|
||||
private libraryService: LibraryService, private titleService: Title, private actionFactoryService: ActionFactoryService,
|
||||
private actionService: ActionService, public bulkSelectionService: BulkSelectionService, private hubService: MessageHubService,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<ng-container *transloco="let t; read: 'metadata-builder'">
|
||||
<ng-container *ngIf="filter">
|
||||
|
||||
<ng-container *ngIf="utilityService.getActiveBreakpoint() === Breakpoint.Desktop; else mobileView">
|
||||
<div class="container-fluid">
|
||||
<form [formGroup]="formGroup">
|
||||
<form [formGroup]="formGroup">
|
||||
<ng-container *ngIf="utilityService.getActiveBreakpoint() === Breakpoint.Desktop; else mobileView">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" formControlName="comparison">
|
||||
|
@ -12,33 +11,30 @@
|
|||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-icon" (click)="addFilter()" [ngbTooltip]="t('add-rule')" [disabled]="statementLimit === -1 || (statementLimit > 0 && filter.statements.length >= statementLimit)">
|
||||
<button type="button" class="btn btn-icon" (click)="addFilter()" [ngbTooltip]="t('add-rule')" [disabled]="statementLimit === -1 || (statementLimit > 0 && filter.statements.length >= statementLimit)">
|
||||
<i class="fa fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" aria-hidden="true">{{t('add-rule')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row mb-2" *ngFor="let filterStmt of filter.statements; let i = index">
|
||||
<div class="col-md-10">
|
||||
<app-metadata-row-filter [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)">
|
||||
<div class="col-md-1 ms-2">
|
||||
<button class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule', {num: i})" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
|
||||
<i class="fa-solid fa-minus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove-rule', {num: i})}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</app-metadata-row-filter>
|
||||
<div class="row mb-2" *ngFor="let filterStmt of filter.statements; let i = index">
|
||||
<div class="col-md-10">
|
||||
<app-metadata-row-filter [index]="i + 100" [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)">
|
||||
<div class="col-md-1 ms-2">
|
||||
<button type="button" class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule', {num: i})" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
|
||||
<i class="fa-solid fa-minus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove-rule', {num: i})}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</app-metadata-row-filter>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #mobileView>
|
||||
<!-- TODO: Robbie please help me style this drawer only view -->
|
||||
<div class="container-fluid">
|
||||
<form [formGroup]="formGroup">
|
||||
<ng-template #mobileView>
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-10">
|
||||
<select class="form-select" formControlName="comparison">
|
||||
|
@ -53,22 +49,21 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="row mb-3" *ngFor="let filterStmt of filter.statements; let i = index">
|
||||
<div class="col-md-12">
|
||||
<app-metadata-row-filter [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)">
|
||||
<div class="col-md-1 ms-2 col-1">
|
||||
<button class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule')" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
|
||||
<i class="fa-solid fa-minus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove-rule')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</app-metadata-row-filter>
|
||||
<div class="row mb-3" *ngFor="let filterStmt of filter.statements; let i = index">
|
||||
<div class="col-md-12">
|
||||
<app-metadata-row-filter [index]="i" [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)">
|
||||
<div class="col-md-1 ms-2 col-1">
|
||||
<button type="button" class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule')" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
|
||||
<i class="fa-solid fa-minus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove-rule')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</app-metadata-row-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</ng-template>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -19,10 +19,9 @@ 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 {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
|
||||
import {allFields, FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
import {allFields} from "../../../_models/metadata/v2/filter-field";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {tap} from "rxjs/operators";
|
||||
import {distinctUntilChanged, tap} from "rxjs/operators";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
|
||||
@Component({
|
||||
|
@ -52,12 +51,14 @@ export class MetadataBuilderComponent implements OnInit {
|
|||
@Input() statementLimit = 0;
|
||||
@Input() availableFilterFields = allFields;
|
||||
@Output() update: EventEmitter<SeriesFilterV2> = new EventEmitter<SeriesFilterV2>();
|
||||
@Output() apply: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly metadataService = inject(MetadataService);
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
protected readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
formGroup: FormGroup = new FormGroup({});
|
||||
|
||||
|
@ -66,39 +67,30 @@ export class MetadataBuilderComponent implements OnInit {
|
|||
{value: FilterCombination.And, title: translate('metadata-builder.and')},
|
||||
];
|
||||
|
||||
get Breakpoint() { return Breakpoint; }
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
if (this.filter === undefined) {
|
||||
// I've left this in to see if it ever happens or not
|
||||
console.error('No filter, creating one in metadata-builder')
|
||||
// If there is no default preset, let's open with series name
|
||||
this.filter = this.filterUtilityService.createSeriesV2Filter();
|
||||
this.filter.statements.push({
|
||||
value: '',
|
||||
comparison: FilterComparison.Equal,
|
||||
field: FilterField.SeriesName
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[builder] ngOnInit');
|
||||
this.formGroup.addControl('comparison', new FormControl<FilterCombination>(this.filter?.combination || FilterCombination.Or, []));
|
||||
this.formGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef), tap(values => {
|
||||
this.filter.combination = parseInt(this.formGroup.get('comparison')?.value, 10);
|
||||
this.formGroup.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), tap(values => {
|
||||
this.filter.combination = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterCombination;
|
||||
console.log('[builder] emitting filter from comparison change');
|
||||
this.update.emit(this.filter);
|
||||
})).subscribe()
|
||||
})).subscribe();
|
||||
}
|
||||
|
||||
addFilter() {
|
||||
console.log('[builder] Adding Filter')
|
||||
this.filter.statements = [this.metadataService.createDefaultFilterStatement(), ...this.filter.statements];
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
removeFilter(index: number) {
|
||||
console.log('[builder] Removing filter')
|
||||
this.filter.statements = this.filter.statements.slice(0, index).concat(this.filter.statements.slice(index + 1))
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
updateFilter(index: number, filterStmt: FilterStatement) {
|
||||
console.log('[builder] updating filter: ', this.filter.statements);
|
||||
this.metadataService.updateFilter(this.filter.statements, index, filterStmt);
|
||||
this.update.emit(this.filter);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
<form [formGroup]="formGroup">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-3 me-2 col-10 mb-2">
|
||||
<ng-container *ngIf="formGroup.get('input') as control">
|
||||
<select class="form-select me-2" formControlName="input">
|
||||
<option *ngFor="let field of availableFields" [value]="field">{{field | filterField}}</option>
|
||||
</select>
|
||||
</ng-container>
|
||||
<select class="form-select me-2" formControlName="input">
|
||||
<option *ngFor="let field of availableFields" [value]="field">{{field | filterField}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 me-2 col-10 mb-2">
|
||||
|
@ -22,7 +20,7 @@
|
|||
<input type="text" class="form-control me-2" autocomplete="true" formControlName="filterValue">
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="PredicateType.Number">
|
||||
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue">
|
||||
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue" min="0">
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="PredicateType.Dropdown">
|
||||
<ng-container *ngIf="dropdownOptions$ | async as opts">
|
||||
|
|
|
@ -3,15 +3,23 @@ import {
|
|||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
EventEmitter, inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||
import {FilterStatement} from '../../../_models/metadata/v2/filter-statement';
|
||||
import {BehaviorSubject, distinctUntilChanged, map, 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 {mangaFormatFilters} from 'src/app/_models/metadata/series-filter';
|
||||
import {PersonRole} from 'src/app/_models/metadata/person';
|
||||
|
@ -32,7 +40,7 @@ enum PredicateType {
|
|||
Dropdown = 3,
|
||||
}
|
||||
|
||||
const StringFields = [FilterField.SeriesName, FilterField.Summary];
|
||||
const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath];
|
||||
const NumberFields = [FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, FilterField.UserRating];
|
||||
const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating,
|
||||
FilterField.Translators, FilterField.Characters, FilterField.Publisher,
|
||||
|
@ -81,6 +89,7 @@ const DropdownComparisons = [FilterComparison.Equal,
|
|||
})
|
||||
export class MetadataFilterRowComponent implements OnInit {
|
||||
|
||||
@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
|
||||
*/
|
||||
|
@ -100,9 +109,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
dropdownOptions$ = of<Select2Option[]>([]);
|
||||
|
||||
loaded: boolean = false;
|
||||
|
||||
|
||||
get PredicateType() { return PredicateType };
|
||||
protected readonly PredicateType = PredicateType;
|
||||
|
||||
get MultipleDropdownAllowed() {
|
||||
const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison;
|
||||
|
@ -113,38 +120,37 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
private readonly collectionTagService: CollectionTagService) {}
|
||||
|
||||
ngOnInit() {
|
||||
console.log('[ngOnInit] creating stmt (' + this.index + '): ', this.preset)
|
||||
this.formGroup.addControl('input', new FormControl<FilterField>(FilterField.SeriesName, []));
|
||||
|
||||
this.formGroup.get('input')?.valueChanges.subscribe((val: string) => this.handleFieldChange(val));
|
||||
this.formGroup.get('input')?.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe((val: string) => this.handleFieldChange(val));
|
||||
this.populateFromPreset();
|
||||
|
||||
this.formGroup.get('filterValue')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef), tap(v => console.log('filterValue: ', v))).subscribe();
|
||||
|
||||
// Dropdown dynamic option selection
|
||||
this.dropdownOptions$ = this.formGroup.get('input')!.valueChanges.pipe(
|
||||
startWith(this.preset.value),
|
||||
switchMap((_) => this.getDropdownObservable()),
|
||||
tap((opts) => {
|
||||
if (!this.formGroup.get('filterValue')?.value) {
|
||||
this. populateFromPreset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.MultipleDropdownAllowed) {
|
||||
this.formGroup.get('filterValue')?.setValue((opts[0].value + '').split(','));
|
||||
} else {
|
||||
this.formGroup.get('filterValue')?.setValue(opts[0].value);
|
||||
}
|
||||
distinctUntilChanged(),
|
||||
filter(() => {
|
||||
const inputVal = parseInt(this.formGroup.get('input')?.value, 10) as FilterField;
|
||||
return DropdownFields.includes(inputVal);
|
||||
}),
|
||||
switchMap((_) => this.getDropdownObservable()),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
|
||||
this.formGroup.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => {
|
||||
this.filterStatement.emit({
|
||||
this.formGroup!.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => {
|
||||
const stmt = {
|
||||
comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison,
|
||||
field: parseInt(this.formGroup.get('input')?.value, 10) as FilterField,
|
||||
value: this.formGroup.get('filterValue')?.value!
|
||||
});
|
||||
};
|
||||
|
||||
if (!stmt.value && stmt.field !== FilterField.SeriesName) return;
|
||||
console.log('updating parent with new statement: ', stmt.value)
|
||||
this.filterStatement.emit(stmt);
|
||||
});
|
||||
|
||||
this.loaded = true;
|
||||
|
@ -152,30 +158,37 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
}
|
||||
|
||||
|
||||
|
||||
populateFromPreset() {
|
||||
const val = this.preset.value === "undefined" || !this.preset.value ? '' : this.preset.value;
|
||||
console.log('populating preset: ', val);
|
||||
this.formGroup.get('comparison')?.patchValue(this.preset.comparison);
|
||||
this.formGroup.get('input')?.patchValue(this.preset.field);
|
||||
|
||||
if (StringFields.includes(this.preset.field)) {
|
||||
this.formGroup.get('filterValue')?.patchValue(this.preset.value);
|
||||
this.formGroup.get('filterValue')?.patchValue(val);
|
||||
} else if (DropdownFields.includes(this.preset.field)) {
|
||||
if (this.MultipleDropdownAllowed) {
|
||||
this.formGroup.get('filterValue')?.setValue(this.preset.value.split(','));
|
||||
if (this.MultipleDropdownAllowed || val.includes(',')) {
|
||||
console.log('setting multiple values: ', val.split(',').map(d => parseInt(d, 10)));
|
||||
this.formGroup.get('filterValue')?.patchValue(val.split(',').map(d => parseInt(d, 10)));
|
||||
} else {
|
||||
if (this.preset.field === FilterField.Languages) {
|
||||
this.formGroup.get('filterValue')?.setValue(this.preset.value);
|
||||
this.formGroup.get('filterValue')?.patchValue(val);
|
||||
} else {
|
||||
this.formGroup.get('filterValue')?.setValue(parseInt(this.preset.value, 10));
|
||||
this.formGroup.get('filterValue')?.patchValue(parseInt(val, 10));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.formGroup.get('filterValue')?.patchValue(parseInt(this.preset.value, 10));
|
||||
this.formGroup.get('filterValue')?.patchValue(parseInt(val, 10));
|
||||
}
|
||||
|
||||
this.formGroup.get('comparison')?.patchValue(this.preset.comparison);
|
||||
this.formGroup.get('input')?.setValue(this.preset.field);
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
getDropdownObservable(): Observable<Select2Option[]> {
|
||||
const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField;
|
||||
console.log('Getting dropdown observable: ', filterField);
|
||||
switch (filterField) {
|
||||
case FilterField.PublicationStatus:
|
||||
return this.metadataService.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => {
|
||||
|
@ -224,41 +237,46 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
}
|
||||
|
||||
getPersonOptions(role: PersonRole) {
|
||||
return this.metadataService.getAllPeople().pipe(map(people => people.filter(p2 => p2.role === role).map(person => {
|
||||
return this.metadataService.getAllPeopleByRole(role).pipe(map(people => people.map(person => {
|
||||
return {value: person.id, label: person.name}
|
||||
})))
|
||||
})));
|
||||
}
|
||||
|
||||
|
||||
handleFieldChange(val: string) {
|
||||
const inputVal = parseInt(val, 10) as FilterField;
|
||||
console.log('HandleFieldChange: ', val);
|
||||
|
||||
if (StringFields.includes(inputVal)) {
|
||||
this.validComparisons$.next(StringComparisons);
|
||||
|
||||
this.predicateType$.next(PredicateType.Text);
|
||||
if (this.loaded) this.formGroup.get('filterValue')?.setValue('');
|
||||
|
||||
if (this.loaded) {
|
||||
this.formGroup.get('filterValue')?.patchValue('',{emitEvent: false});
|
||||
console.log('setting filterValue to empty string', this.formGroup.get('filterValue')?.value)
|
||||
} // BUG: undefined is getting set and the input value isn't updating and emitting to the backend
|
||||
return;
|
||||
}
|
||||
|
||||
if (NumberFields.includes(inputVal)) {
|
||||
let comps = [...NumberComparisons];
|
||||
const comps = [...NumberComparisons];
|
||||
if (inputVal === FilterField.ReleaseYear) {
|
||||
comps.push(...DateComparisons);
|
||||
}
|
||||
this.validComparisons$.next(comps);
|
||||
this.predicateType$.next(PredicateType.Number);
|
||||
if (this.loaded) this.formGroup.get('filterValue')?.setValue('');
|
||||
if (this.loaded) this.formGroup.get('filterValue')?.patchValue(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (DropdownFields.includes(inputVal)) {
|
||||
let comps = [...DropdownComparisons];
|
||||
const comps = [...DropdownComparisons];
|
||||
if (inputVal === FilterField.AgeRating) {
|
||||
comps.push(...NumberComparisons);
|
||||
}
|
||||
this.validComparisons$.next(comps);
|
||||
this.predicateType$.next(PredicateType.Dropdown);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,10 @@ export class FilterFieldPipe implements PipeTransform {
|
|||
return translate('filter-field-pipe.user-rating');
|
||||
case FilterField.Writers:
|
||||
return translate('filter-field-pipe.writers');
|
||||
case FilterField.Path:
|
||||
return translate('filter-field-pipe.path');
|
||||
case FilterField.FilePath:
|
||||
return translate('filter-field-pipe.file-path');
|
||||
default:
|
||||
throw new Error(`Invalid FilterField value: ${value}`);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
<ng-container *transloco="let t; read: 'metadata-filter'">
|
||||
<ng-container *ngIf="toggleService.toggleState$ | async as isOpen">
|
||||
<div class="phone-hidden">
|
||||
<div class="phone-hidden" *ngIf="utilityService.getActiveBreakpoint() > Breakpoint.Tablet">
|
||||
<div #collapse="ngbCollapse" [ngbCollapse]="!isOpen" (ngbCollapseChange)="setToggle($event)">
|
||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="not-phone-hidden">
|
||||
<div class="not-phone-hidden" *ngIf="utilityService.getActiveBreakpoint() < Breakpoint.Desktop">
|
||||
<app-drawer #commentDrawer="drawer" [isOpen]="isOpen" [options]="{topOffset: 56}" (drawerClosed)="toggleService.set(false)">
|
||||
<h5 header>
|
||||
{{t('filter-title')}}
|
||||
</h5>
|
||||
<div body class="drawer-body">
|
||||
<!-- TODO: BUG: Filter section is instantiated twice if this isn't ngIf'd -->
|
||||
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
|
||||
</div>
|
||||
</app-drawer>
|
||||
|
@ -19,9 +20,13 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-template #filterSection>
|
||||
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded">
|
||||
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded && filterV2">
|
||||
<div class="row justify-content-center g-0">
|
||||
<app-metadata-builder [filter]="filterV2!" [availableFilterFields]="allFilterFields" (update)="handleFilters($event)" [statementLimit]="filterSettings.statementLimit"></app-metadata-builder>
|
||||
<app-metadata-builder [filter]="filterV2"
|
||||
[availableFilterFields]="allFilterFields"
|
||||
[statementLimit]="filterSettings.statementLimit"
|
||||
(update)="handleFilters($event)">
|
||||
</app-metadata-builder>
|
||||
</div>
|
||||
<form [formGroup]="sortGroup" class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
|
|
|
@ -76,6 +76,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
allFilterFields = allFields;
|
||||
|
||||
handleFilters(filter: SeriesFilterV2) {
|
||||
console.log('[metadata-filter] updating filter');
|
||||
this.filterV2 = filter;
|
||||
}
|
||||
|
||||
|
@ -86,6 +87,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
constructor(public toggleService: ToggleService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('[metadata-filter] ngOnInit')
|
||||
if (this.filterSettings === undefined) {
|
||||
this.filterSettings = new FilterSettings();
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -137,6 +139,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
loadFromPresetsAndSetup() {
|
||||
this.fullyLoaded = false;
|
||||
|
||||
console.log('[metadata-filter] loading from preset and setting up');
|
||||
this.filterV2 = this.deepClone(this.filterSettings.presetsV2);
|
||||
|
||||
this.sortGroup = new FormGroup({
|
||||
|
@ -145,6 +148,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
});
|
||||
|
||||
this.sortGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
console.log('[metadata-filter] sortGroup value change');
|
||||
if (this.filterV2?.sortOptions === null) {
|
||||
this.filterV2.sortOptions = {
|
||||
isAscending: this.isAscendingSort,
|
||||
|
@ -152,12 +156,11 @@ export class MetadataFilterComponent implements OnInit {
|
|||
};
|
||||
}
|
||||
this.filterV2!.sortOptions!.sortField = parseInt(this.sortGroup.get('sortField')?.value, 10);
|
||||
this.filterV2!.limitTo = parseInt(this.sortGroup.get('limitTo')?.value, 10);
|
||||
this.filterV2!.limitTo = Math.max(parseInt(this.sortGroup.get('limitTo')?.value || '0', 10), 0);
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.fullyLoaded = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.apply();
|
||||
}
|
||||
|
||||
|
@ -173,6 +176,7 @@ export class MetadataFilterComponent implements OnInit {
|
|||
}
|
||||
|
||||
this.filterV2!.sortOptions!.isAscending = this.isAscendingSort;
|
||||
console.log('[metadata-filter] updated filter sort order')
|
||||
}
|
||||
|
||||
clear() {
|
||||
|
@ -181,7 +185,6 @@ export class MetadataFilterComponent implements OnInit {
|
|||
}
|
||||
|
||||
apply() {
|
||||
|
||||
this.applyFilter.emit({isFirst: this.updateApplied === 0, filterV2: this.filterV2!});
|
||||
|
||||
if (this.utilityService.getActiveBreakpoint() === Breakpoint.Mobile && this.updateApplied !== 0) {
|
||||
|
|
|
@ -1731,7 +1731,9 @@
|
|||
"tags": "Tags",
|
||||
"translators": "Translators",
|
||||
"user-rating": "User Rating",
|
||||
"writers": "Writers"
|
||||
"writers": "Writers",
|
||||
"path": "Path",
|
||||
"file-path": "File Path"
|
||||
},
|
||||
|
||||
"filter-comparison-pipe": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue