mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Filtering Done (#2222)
* Replaced normal dropdowns with select2 (which will eventually replace our custom typeaheads). Still needs styling. * More css * Styling. Fixed preloading typeahead with multiple options on load. * Styling to align with typeahead tag badges. * Done with filtering story. * Fixed a bug with switching between filters. * Fixed some extra } from localization
This commit is contained in:
parent
680f3d78d2
commit
c2375efe21
27
UI/Web/package-lock.json
generated
27
UI/Web/package-lock.json
generated
@ -37,6 +37,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-select2-component": "^13.0.2",
|
||||
"ngx-color-picker": "^14.0.0",
|
||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
@ -10556,6 +10557,20 @@
|
||||
"rxjs": ">=6.4.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"ngx-infinite-scroll": ">=16.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": ">=16.1.0",
|
||||
"@angular/common": ">=16.1.0",
|
||||
"@angular/core": ">=16.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-color-picker": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-color-picker/-/ngx-color-picker-14.0.0.tgz",
|
||||
@ -10598,6 +10613,18 @@
|
||||
"@angular/core": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-infinite-scroll": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-16.0.0.tgz",
|
||||
"integrity": "sha512-bzyNYd+wVlUUxcopRVr2DAa81eEc8vITtKVvb+c7R1uy8hWPTlxOEXf3L1qA4FMwTEzCQ9b37TXzlJji3qBy+A==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16.0.0 <17.0.0",
|
||||
"@angular/core": ">=16.0.0 <17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-slider-v2": {
|
||||
"version": "16.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ngx-slider-v2/-/ngx-slider-v2-16.0.2.tgz",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"lazysizes": "^5.3.2",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-select2-component": "^13.0.2",
|
||||
"ngx-color-picker": "^14.0.0",
|
||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
|
@ -1,7 +1,7 @@
|
||||
<ng-container *transloco="let t; read: 'personal-table-of-contents'">
|
||||
<div class="table-of-contents">
|
||||
<div *ngIf="Pages.length === 0">
|
||||
<em>{{t('no-data')}}}</em>
|
||||
<em>{{t('no-data')}}</em>
|
||||
</div>
|
||||
<ul>
|
||||
<li *ngFor="let page of Pages">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<ng-container *transloco="let t; read: 'table-of-contents'">
|
||||
<div class="table-of-contents">
|
||||
<div *ngIf="chapters.length === 0">
|
||||
<em>{{t('no-data')}}}</em>
|
||||
<em>{{t('no-data')}}</em>
|
||||
</div>
|
||||
<div *ngIf="chapters.length === 1; else nestedChildren">
|
||||
<ul>
|
||||
|
@ -49,7 +49,7 @@
|
||||
<div class="col-md-2 col-1">
|
||||
<button class="btn btn-icon" (click)="addFilter()" [ngbTooltip]="t('add-rule')">
|
||||
<i class="fa fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" aria-hidden="true">{{t('add-rule')}}}</span>
|
||||
<span class="visually-hidden" aria-hidden="true">{{t('add-rule')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,9 +25,17 @@
|
||||
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue">
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="PredicateType.Dropdown">
|
||||
<select class="col-auto form-select me-2" formControlName="filterValue">
|
||||
<option *ngFor="let option of dropdownOptions$ | async" [value]="option.value">{{option.title}}</option>
|
||||
</select>
|
||||
<ng-container *ngIf="dropdownOptions$ | async as opts">
|
||||
<ng-container *ngTemplateOutlet="dropdown; context: { options: opts, multipleAllowed: MultipleDropdownAllowed }"></ng-container>
|
||||
<ng-template #dropdown let-options="options" let-multipleAllowed="multipleAllowed">
|
||||
<select2 [data]="options"
|
||||
formControlName="filterValue"
|
||||
[multiple]="multipleAllowed"
|
||||
[infiniteScroll]="true"
|
||||
[resettable]="true">
|
||||
</select2>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
@ -0,0 +1,3 @@
|
||||
::ng-deep .select2-selection__rendered {
|
||||
padding-top: 4px !important;
|
||||
}
|
@ -19,10 +19,12 @@ import {LibraryService} from 'src/app/_services/library.service';
|
||||
import {CollectionTagService} from 'src/app/_services/collection-tag.service';
|
||||
import {FilterComparison} from 'src/app/_models/metadata/v2/filter-comparison';
|
||||
import {allFields, FilterField} from 'src/app/_models/metadata/v2/filter-field';
|
||||
import {AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase} from "@angular/common";
|
||||
import {AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet} 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 {Select2Module, Select2Option} from "ng-select2-component";
|
||||
import {TagBadgeComponent} from "../../../shared/tag-badge/tag-badge.component";
|
||||
|
||||
enum PredicateType {
|
||||
Text = 1,
|
||||
@ -70,12 +72,18 @@ const DropdownComparisons = [FilterComparison.Equal,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgForOf,
|
||||
NgIf
|
||||
NgIf,
|
||||
Select2Module,
|
||||
NgTemplateOutlet,
|
||||
TagBadgeComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
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;
|
||||
@Input() availableFields: Array<FilterField> = allFields;
|
||||
@Output() filterStatement = new EventEmitter<FilterStatement>();
|
||||
@ -89,14 +97,18 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
});
|
||||
validComparisons$: BehaviorSubject<FilterComparison[]> = new BehaviorSubject([FilterComparison.Equal] as FilterComparison[]);
|
||||
predicateType$: BehaviorSubject<PredicateType> = new BehaviorSubject(PredicateType.Text as PredicateType);
|
||||
dropdownOptions$ = of<{value: number, title: string}[]>([]);
|
||||
|
||||
dropdownOptions$ = of<Select2Option[]>([]);
|
||||
|
||||
loaded: boolean = false;
|
||||
|
||||
|
||||
get PredicateType() { return PredicateType };
|
||||
|
||||
get MultipleDropdownAllowed() {
|
||||
const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison;
|
||||
return comp === FilterComparison.Contains || comp === FilterComparison.NotContains;
|
||||
}
|
||||
|
||||
constructor(private readonly metadataService: MetadataService, private readonly libraryService: LibraryService,
|
||||
private readonly collectionTagService: CollectionTagService) {}
|
||||
|
||||
@ -106,26 +118,27 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
this.formGroup.get('input')?.valueChanges.subscribe((val: string) => this.handleFieldChange(val));
|
||||
this.populateFromPreset();
|
||||
|
||||
this.buildDisabledList();
|
||||
|
||||
|
||||
// Dropdown dynamic option selection
|
||||
this.dropdownOptions$ = this.formGroup.get('input')!.valueChanges.pipe(
|
||||
startWith(this.preset.value),
|
||||
switchMap((_) => this.getDropdownObservable()),
|
||||
tap((opts) => {
|
||||
const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField;
|
||||
const filterComparison = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison;
|
||||
if (this.preset.field === filterField && this.preset.comparison === filterComparison) {
|
||||
//console.log('using preset value for dropdown option')
|
||||
if (!this.formGroup.get('filterValue')?.value) {
|
||||
this. populateFromPreset();
|
||||
return;
|
||||
}
|
||||
|
||||
this.formGroup.get('filterValue')?.setValue(opts[0].value);
|
||||
if (this.MultipleDropdownAllowed) {
|
||||
this.formGroup.get('filterValue')?.setValue((opts[0].value + '').split(','));
|
||||
} else {
|
||||
this.formGroup.get('filterValue')?.setValue(opts[0].value);
|
||||
}
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
|
||||
this.formGroup.valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)).subscribe(_ => {
|
||||
this.filterStatement.emit({
|
||||
comparison: parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison,
|
||||
@ -138,13 +151,20 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
buildDisabledList() {
|
||||
|
||||
}
|
||||
|
||||
populateFromPreset() {
|
||||
if (StringFields.includes(this.preset.field)) {
|
||||
this.formGroup.get('filterValue')?.patchValue(this.preset.value);
|
||||
} else if (DropdownFields.includes(this.preset.field)) {
|
||||
if (this.MultipleDropdownAllowed) {
|
||||
this.formGroup.get('filterValue')?.setValue(this.preset.value.split(','));
|
||||
} else {
|
||||
if (this.preset.field === FilterField.Languages) {
|
||||
this.formGroup.get('filterValue')?.setValue(this.preset.value);
|
||||
} else {
|
||||
this.formGroup.get('filterValue')?.setValue(parseInt(this.preset.value, 10));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.formGroup.get('filterValue')?.patchValue(parseInt(this.preset.value, 10));
|
||||
}
|
||||
@ -154,40 +174,40 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
getDropdownObservable(): Observable<{value: any, title: string}[]> {
|
||||
getDropdownObservable(): Observable<Select2Option[]> {
|
||||
const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField;
|
||||
switch (filterField) {
|
||||
case FilterField.PublicationStatus:
|
||||
return this.metadataService.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => {
|
||||
return {value: pub.value, title: pub.title}
|
||||
return {value: pub.value, label: pub.title}
|
||||
})));
|
||||
case FilterField.AgeRating:
|
||||
return this.metadataService.getAllAgeRatings().pipe(map(ratings => ratings.map(rating => {
|
||||
return {value: rating.value, title: rating.title}
|
||||
return {value: rating.value, label: rating.title}
|
||||
})));
|
||||
case FilterField.Genres:
|
||||
return this.metadataService.getAllGenres().pipe(map(genres => genres.map(genre => {
|
||||
return {value: genre.id, title: genre.title}
|
||||
return {value: genre.id, label: genre.title}
|
||||
})));
|
||||
case FilterField.Languages:
|
||||
return this.metadataService.getAllLanguages().pipe(map(statuses => statuses.map(status => {
|
||||
return {value: status.isoCode, title: status.title + ` (${status.isoCode})`}
|
||||
return {value: status.isoCode, label: status.title + ` (${status.isoCode})`}
|
||||
})));
|
||||
case FilterField.Formats:
|
||||
return of(mangaFormatFilters).pipe(map(statuses => statuses.map(status => {
|
||||
return {value: status.value, title: status.title}
|
||||
return {value: status.value, label: status.title}
|
||||
})));
|
||||
case FilterField.Libraries:
|
||||
return this.libraryService.getLibraries().pipe(map(libs => libs.map(lib => {
|
||||
return {value: lib.id, title: lib.name}
|
||||
return {value: lib.id, label: lib.name}
|
||||
})));
|
||||
case FilterField.Tags:
|
||||
return this.metadataService.getAllTags().pipe(map(statuses => statuses.map(status => {
|
||||
return {value: status.id, title: status.title}
|
||||
return {value: status.id, label: status.title}
|
||||
})));
|
||||
case FilterField.CollectionTags:
|
||||
return this.collectionTagService.allTags().pipe(map(statuses => statuses.map(status => {
|
||||
return {value: status.id, title: status.title}
|
||||
return {value: status.id, label: status.title}
|
||||
})));
|
||||
case FilterField.Characters: return this.getPersonOptions(PersonRole.Character);
|
||||
case FilterField.Colorist: return this.getPersonOptions(PersonRole.Colorist);
|
||||
@ -205,7 +225,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
|
||||
getPersonOptions(role: PersonRole) {
|
||||
return this.metadataService.getAllPeople().pipe(map(people => people.filter(p2 => p2.role === role).map(person => {
|
||||
return {value: person.id, title: person.name}
|
||||
return {value: person.id, label: person.name}
|
||||
})))
|
||||
}
|
||||
|
||||
@ -218,7 +238,6 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
|
||||
this.predicateType$.next(PredicateType.Text);
|
||||
if (this.loaded) this.formGroup.get('filterValue')?.setValue('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@
|
||||
@import './theme/components/offcanvas';
|
||||
@import './theme/components/table';
|
||||
@import './theme/components/alerts';
|
||||
@import './theme/components/typeahead';
|
||||
|
||||
|
||||
@import './theme/utilities/utilities';
|
||||
|
72
UI/Web/src/theme/components/_typeahead.scss
Normal file
72
UI/Web/src/theme/components/_typeahead.scss
Normal file
@ -0,0 +1,72 @@
|
||||
:root {
|
||||
/* size */
|
||||
--select2-single-height: 36px;
|
||||
--select2-multiple-height: 36px;
|
||||
|
||||
/* label */
|
||||
--select2-label-text-color: #000;
|
||||
--select2-required-color: red;
|
||||
|
||||
/* selection */
|
||||
--select2-selection-border-radius: 4px;
|
||||
--select2-selection-background: var(--input-bg-readonly-color);
|
||||
--select2-selection-disabled-background: #eee;
|
||||
--select2-selection-border-color: var(--input-border-color);
|
||||
--select2-selection-focus-border-color: var(--input-focus-boxshadow-color);
|
||||
--select2-selection-text-color: var(--input-text-color);
|
||||
|
||||
/* selection: choice item (multiple) */
|
||||
--select2-selection-choice-background: var(--tagbadge-filled-bg-color);
|
||||
--select2-selection-choice-text-color: var(--tagbadge-filled-text-color);
|
||||
--select2-selection-choice-border-color: var(--tagbadge-border-color);
|
||||
--select2-selection-choice-close-color: var(--tagbadge-filled-text-color);
|
||||
--select2-selection-choice-hover-close-color: var(--tagbadge-filled-text-color);
|
||||
|
||||
/* placeholder */
|
||||
--select2-placeholder-color: #999;
|
||||
--select2-placeholder-overflow: ellipsis;
|
||||
|
||||
/* no result message */
|
||||
--select2-no-result-color: #888;
|
||||
--select2-no-result-font-style: italic;
|
||||
|
||||
/* no result message */
|
||||
--select2-too-much-result-color: #888;
|
||||
--select2-too-much-result-style: italic;
|
||||
|
||||
/* reset */
|
||||
--select2-reset-color: #999;
|
||||
|
||||
/* arrow */
|
||||
--select2-arrow-color: #ccc;
|
||||
|
||||
/* dropdown panel */
|
||||
--select2-dropdown-background: var(--input-bg-readonly-color);
|
||||
--select2-dropdown-border-color: var(--input-border-color);
|
||||
|
||||
/* overlay */
|
||||
--select2-overlay-backdrop: transparent;
|
||||
|
||||
/* search field */
|
||||
--select2-search-border-color: var(--input-border-color);
|
||||
--select2-search-background: var(--input-bg-readonly-color);
|
||||
--select2-search-border-radius: 0px;
|
||||
|
||||
/* dropdown option */
|
||||
--select2-option-text-color: var(--body-text-color);
|
||||
--select2-option-disabled-text-color: #999;
|
||||
--select2-option-disabled-background: transparent;
|
||||
--select2-option-selected-text-color: lightgrey;
|
||||
--select2-option-selected-background: var(--btn-disabled-bg-color);
|
||||
--select2-option-highlighted-text-color: #fff;
|
||||
--select2-option-highlighted-background: #5897fb; // TODO: This needs to be done correctly and applied throughout the app
|
||||
--select2-option-group-text-color: var(--body-text-color);
|
||||
--select2-option-group-background: transparent;
|
||||
|
||||
/* hint */
|
||||
--select2-hint-text-color: #888;
|
||||
}
|
||||
|
||||
//.select2-selection__rendered {
|
||||
// padding-top: 4px;
|
||||
//}
|
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.7.10"
|
||||
"version": "0.7.7.11"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user