From c2375efe21ce34f6b6b028a23c49ac56f6006d26 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Thu, 17 Aug 2023 19:09:04 -0500 Subject: [PATCH] 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 --- UI/Web/package-lock.json | 27 +++++++ UI/Web/package.json | 1 + .../personal-table-of-contents.component.html | 2 +- .../table-of-contents.component.html | 2 +- .../metadata-builder.component.html | 2 +- .../metadata-filter-row.component.html | 14 +++- .../metadata-filter-row.component.scss | 3 + .../metadata-filter-row.component.ts | 69 +++++++++++------- UI/Web/src/styles.scss | 1 + UI/Web/src/theme/components/_typeahead.scss | 72 +++++++++++++++++++ openapi.json | 2 +- 11 files changed, 163 insertions(+), 32 deletions(-) create mode 100644 UI/Web/src/theme/components/_typeahead.scss diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 5d4594bb7..dc34bcada 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -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", diff --git a/UI/Web/package.json b/UI/Web/package.json index 1e273421f..d259d45c7 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -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", diff --git a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html index db3761ba0..33b3a1a59 100644 --- a/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html +++ b/UI/Web/src/app/book-reader/_components/personal-table-of-contents/personal-table-of-contents.component.html @@ -1,7 +1,7 @@
- {{t('no-data')}}} + {{t('no-data')}}
  • diff --git a/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html b/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html index 9345150ba..1b6982af9 100644 --- a/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html +++ b/UI/Web/src/app/book-reader/_components/table-of-contents/table-of-contents.component.html @@ -1,7 +1,7 @@
    - {{t('no-data')}}} + {{t('no-data')}}
      diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html index 3b1520f16..f93851d37 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html @@ -49,7 +49,7 @@
    diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html index c47c61338..2e3e6fcb6 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.html @@ -25,9 +25,17 @@ - + + + + + + + diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss index e69de29bb..ee0c6d278 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.scss @@ -0,0 +1,3 @@ +::ng-deep .select2-selection__rendered { + padding-top: 4px !important; +} 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 8af81a8af..7f044daee 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 @@ -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 = allFields; @Output() filterStatement = new EventEmitter(); @@ -89,14 +97,18 @@ export class MetadataFilterRowComponent implements OnInit { }); validComparisons$: BehaviorSubject = new BehaviorSubject([FilterComparison.Equal] as FilterComparison[]); predicateType$: BehaviorSubject = new BehaviorSubject(PredicateType.Text as PredicateType); - dropdownOptions$ = of<{value: number, title: string}[]>([]); - + dropdownOptions$ = of([]); 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 { 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; } diff --git a/UI/Web/src/styles.scss b/UI/Web/src/styles.scss index 313f42126..7bb064643 100644 --- a/UI/Web/src/styles.scss +++ b/UI/Web/src/styles.scss @@ -40,6 +40,7 @@ @import './theme/components/offcanvas'; @import './theme/components/table'; @import './theme/components/alerts'; +@import './theme/components/typeahead'; @import './theme/utilities/utilities'; diff --git a/UI/Web/src/theme/components/_typeahead.scss b/UI/Web/src/theme/components/_typeahead.scss new file mode 100644 index 000000000..7f2bdf40f --- /dev/null +++ b/UI/Web/src/theme/components/_typeahead.scss @@ -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; +//} diff --git a/openapi.json b/openapi.json index 154ecfe81..51fd54e78 100644 --- a/openapi.json +++ b/openapi.json @@ -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": [ {