Filtering Bugfixes (#1220)

* Cleaned up random strings and unified them in one place.

* Implemented the ability to disable typeaheads

* Refactored disable state to disable controls on filter

* Fixed an overflow regression on title

* Updated ComicInfo DTO which had some bad properties on it

* Cleaned up some code around disabled typeaheads/filters

* Fixed typeaheads causing resets to state and mucking up filter presets

* Fixed state not refreshing between page loads

* Fixed a bad parsing for My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras

* Cleanup within the metadata filter to reuse logic and minimize extra loops.

* Fixed a timing issue with typeahead and first load for people

* Fixed a bug in Publication Status for a given library, which would fail due to not performing some of the query in memory. Removed a custom index on Series table that wasn't used and potentially caused constraint issues.

* Added a wiki link for stats collections

* Security bump

* Fixed the regex
This commit is contained in:
Joseph Milazzo 2022-04-16 18:29:11 -05:00 committed by GitHub
parent e3aa9abf55
commit e630e0b2c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1856 additions and 291 deletions

View File

@ -169,6 +169,7 @@ namespace API.Tests.Parser
[InlineData("Love Hina - Volume 01 [Scans].pdf", "Love Hina")]
[InlineData("It's Witching Time! 001 (Digital) (Anonymous1234)", "It's Witching Time!")]
[InlineData("Zettai Karen Children v02 c003 - The Invisible Guardian (2) [JS Scans]", "Zettai Karen Children")]
[InlineData("My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras", "My Charms Are Wasted on Kuroiwa Medaka")]
public void ParseSeriesTest(string filename, string expected)
{
Assert.Equal(expected, API.Parser.Parser.ParseSeries(filename));

View File

@ -99,12 +99,12 @@ public class MetadataController : BaseApiController
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
/// <returns></returns>
[HttpGet("publication-status")]
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllPublicationStatus(string? libraryIds)
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
{
var ids = libraryIds?.Split(",").Select(int.Parse).ToList();
if (ids != null && ids.Count > 0)
if (ids is {Count: > 0})
{
return Ok(await _unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
return Ok(_unitOfWork.SeriesRepository.GetAllPublicationStatusesDtosForLibrariesAsync(ids));
}
return Ok(Enum.GetValues<PublicationStatus>().Select(t => new PublicationStatusDto()

View File

@ -47,11 +47,11 @@ namespace API.Data.Metadata
/// </summary>
public float UserRating { get; set; }
public string AlternateSeries { get; set; } = string.Empty;
public string StoryArc { get; set; } = string.Empty;
public string SeriesGroup { get; set; } = string.Empty;
public string AlternativeSeries { get; set; } = string.Empty;
public string AlternativeNumber { get; set; } = string.Empty;
public string AlternateNumber { get; set; } = string.Empty;
public int AlternateCount { get; set; } = 0;
public string AlternateSeries { get; set; } = string.Empty;
/// <summary>
/// This is Epub only: calibre:title_sort

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class RemoveCustomIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId_Format",
table: "Series");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_Series_Name_NormalizedName_LocalizedName_LibraryId_Format",
table: "Series",
columns: new[] { "Name", "NormalizedName", "LocalizedName", "LibraryId", "Format" },
unique: true);
}
}
}

View File

@ -744,9 +744,6 @@ namespace API.Data.Migrations
b.HasIndex("LibraryId");
b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format")
.IsUnique();
b.ToTable("Series");
});

View File

@ -94,7 +94,7 @@ public interface ISeriesRepository
Task<IList<SeriesMetadata>> GetSeriesMetadataForIdsAsync(IEnumerable<int> seriesIds);
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
}
@ -884,19 +884,19 @@ public class SeriesRepository : ISeriesRepository
.ToList();
}
public async Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
public IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
{
return await _context.Series
return _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Select(s => s.Metadata.PublicationStatus)
.Distinct()
.AsEnumerable()
.Select(s => new PublicationStatusDto()
{
Value = s,
Title = s.ToDescription()
})
.OrderBy(s => s.Title)
.ToListAsync();
.OrderBy(s => s.Title);
}

View File

@ -3,13 +3,11 @@ using System.Collections.Generic;
using API.Entities.Enums;
using API.Entities.Interfaces;
using API.Entities.Metadata;
using Microsoft.EntityFrameworkCore;
namespace API.Entities
namespace API.Entities;
public class Series : IEntityDate
{
[Index(nameof(Name), nameof(NormalizedName), nameof(LocalizedName), nameof(LibraryId), nameof(Format), IsUnique = true)]
public class Series : IEntityDate
{
public int Id { get; set; }
/// <summary>
/// The UI visible Name of the Series. This may or may not be the same as the OriginalName
@ -76,5 +74,4 @@ namespace API.Entities
public Library Library { get; set; }
public int LibraryId { get; set; }
}
}

View File

@ -132,9 +132,9 @@ namespace API.Parser
new Regex(
@"(?<Series>.*)(?:, Chapter )(?<Chapter>\d+)",
MatchOptions, RegexTimeout),
// Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz
// Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz, My Charms Are Wasted on Kuroiwa Medaka - Ch. 37.5 - Volume Extras
new Regex(
@"(?<Series>.*)(\s|_|-)(?!Vol)(\s|_|-)(?:Chapter)(\s|_|-)(?<Chapter>\d+)",
@"(?<Series>.+?)(\s|_|-)(?!Vol)(\s|_|-)((?:Chapter)|(?:Ch\.))(\s|_|-)(?<Chapter>\d+)",
MatchOptions, RegexTimeout),
// [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz
new Regex(

View File

@ -3730,9 +3730,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"

View File

@ -195,6 +195,7 @@ export class SeriesService {
}
createSeriesFilter(filter?: SeriesFilter) {
if (filter !== undefined) return filter;
const data: SeriesFilter = {
formats: [],
libraries: [],
@ -225,8 +226,6 @@ export class SeriesService {
seriesNameQuery: '',
};
if (filter === undefined) return data;
return filter;
return data;
}
}

View File

@ -40,7 +40,7 @@
<div class="mb-3">
<label for="stat-collection" class="form-label" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
<p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect.</p>
<p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect. See <a href="https://wiki.kavitareader.com/en/faq" target="_blank" referrerpolicy="no-refer">wiki</a> for what is collected.</p>
<div class="form-check form-switch">
<input id="stat-collection" type="checkbox" aria-label="Stat Collection" class="form-check-input" formControlName="allowStatCollection">
<label for="stat-collection" class="form-check-label">Send Data</label>

View File

@ -31,6 +31,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
onDestroy: Subject<void> = new Subject<void>();
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActiveCheck!: SeriesFilter;
filterActive: boolean = false;
bulkActionCallback = (action: Action, data: any) => {
@ -79,8 +80,9 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.titleService.setTitle('Kavita - All Series');
this.pagination = this.filterUtilityService.pagination();
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
this.filterActiveCheck = this.seriesService.createSeriesFilter();
}
ngOnInit(): void {
@ -117,12 +119,7 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
}
loadPage() {
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
if (this.filter == undefined) {
this.filter = this.seriesService.createSeriesFilter();
}
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getAllSeries(this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.pagination = series.pagination;

View File

@ -15,7 +15,6 @@ import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-adde
import { Pagination } from 'src/app/_models/pagination';
import { Series } from 'src/app/_models/series';
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
import { AccountService } from 'src/app/_services/account.service';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ActionService } from 'src/app/_services/action.service';
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
@ -41,6 +40,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
summary: string = '';
actionInProgress: boolean = false;
filterActiveCheck!: SeriesFilter;
filterActive: boolean = false;
filterOpen: EventEmitter<boolean> = new EventEmitter();
@ -98,9 +98,11 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
}
const tagId = parseInt(routeId, 10);
this.seriesPagination = this.filterUtilityService.pagination();
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
this.seriesPagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
this.filterSettings.presets.collectionTags = [tagId];
this.filterActiveCheck = this.seriesService.createSeriesFilter();
this.filterActiveCheck.collectionTags = [tagId];
this.updateTag(tagId);
}
@ -161,7 +163,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
}
loadPage() {
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getAllSeries(this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.seriesPagination = series.pagination;

View File

@ -37,6 +37,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
filterSettings: FilterSettings = new FilterSettings();
filterOpen: EventEmitter<boolean> = new EventEmitter();
filterActive: boolean = false;
filterActiveCheck!: SeriesFilter;
tabs: Array<{title: string, fragment: string}> = [
{title: 'Library', fragment: ''},
@ -101,9 +102,14 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
});
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
this.pagination = this.filterUtilityService.pagination();
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl();
this.filterSettings.presets.libraries = [this.libraryId];
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
// Setup filterActiveCheck to check filter against
this.filterActiveCheck = this.seriesService.createSeriesFilter();
this.filterActiveCheck.libraries = [this.libraryId];
this.filterSettings.libraryDisabled = true;
}
ngOnInit(): void {
@ -153,6 +159,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
updateFilter(data: FilterEvent) {
this.filter = data.filter;
if (!data.isFirst) this.filterUtilityService.updateUrlFromFilter(this.pagination, this.filter);
this.loadPage();
}
@ -165,7 +172,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
}
this.loadingSeries = true;
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterSettings.presets);
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result;
this.pagination = series.pagination;

View File

@ -3,6 +3,7 @@ import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { ReplaySubject, Subject } from 'rxjs';
import { debounceTime, filter, take, takeUntil } from 'rxjs/operators';
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
import { SeriesAddedEvent } from '../_models/events/series-added-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import { Library } from '../_models/library';
@ -153,19 +154,19 @@ export class LibraryComponent implements OnInit, OnDestroy {
handleSectionClick(sectionTitle: string) {
if (sectionTitle.toLowerCase() === 'recently updated series') {
const params: any = {};
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params['page'] = 1;
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
} else if (sectionTitle.toLowerCase() === 'on deck') {
const params: any = {};
params['readStatus'] = 'true,false,false';
params['sortBy'] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params['page'] = 1;
params[FilterQueryParam.ReadStatus] = 'true,false,false';
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
}else if (sectionTitle.toLowerCase() === 'newly added series') {
const params: any = {};
params['sortBy'] = SortField.Created + ',false'; // sort by created, desc
params['page'] = 1;
params[FilterQueryParam.SortBy] = SortField.Created + ',false'; // sort by created, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
}
}

View File

@ -13,9 +13,11 @@ export class FilterSettings {
tagsDisabled = false;
languageDisabled = false;
publicationStatusDisabled = false;
searchNameDisabled = false;
presets: SeriesFilter | undefined;
/**
* Should the filter section be open by default
* @deprecated This is deprecated UX pattern. New style is to show highlight on filter button.
*/
openByDefault = false;
}

View File

@ -19,13 +19,13 @@
<ng-template #filterSection>
<ng-template #globalFilterTooltip>This is library agnostic</ng-template>
<div class="filter-section mx-auto pb-3">
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded">
<div class="row justify-content-center g-0">
<div class="col-md-2 me-3" *ngIf="!filterSettings.formatDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="format" class="form-label">Format</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
<span class="visually-hidden" id="filter-global-format-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.formatDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -36,10 +36,10 @@
</div>
</div>
<div class="col-md-2 me-3"*ngIf="!filterSettings.libraryDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="libraries" class="form-label">Libraries</label>
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads" [disabled]="filterSettings.libraryDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -50,11 +50,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.collectionDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="collections" class="form-label">Collections</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
<span class="visually-hidden" id="filter-global-collections-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.collectionDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -65,10 +65,10 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.genresDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="genres" class="form-label">Genres</label>
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.genresDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -79,10 +79,10 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.tagsDisabled">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="tags" class="form-label">Tags</label>
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.tagsDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -95,10 +95,11 @@
</div>
<div class="row justify-content-center g-0">
<!-- The People row -->
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="cover-artist" class="form-label">Cover Artists</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -109,10 +110,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="writers" class="form-label">Writers</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Writer)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -123,10 +125,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="publisher" class="form-label">Publisher</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Publisher)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -137,10 +140,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="penciller" class="form-label">Penciller</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Penciller)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -151,10 +155,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="letterer" class="form-label">Letterer</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Letterer)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -165,10 +170,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="inker" class="form-label">Inker</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Inker)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -179,10 +185,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="editor" class="form-label">Editor</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Editor)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -193,10 +200,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="colorist" class="form-label">Colorist</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Colorist)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -207,10 +215,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="character" class="form-label">Character</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Character)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -221,10 +230,11 @@
</div>
</div>
<div class="col-md-2 me-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Translator)">
<div class="col-md-2 me-3">
<div class="mb-3">
<label for="translators" class="form-label">Translators</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)"
[reset]="resetTypeaheads" [disabled]="!peopleSettings.hasOwnProperty(PersonRole.Translator)">
<ng-template #badgeItem let-item let-position="idx">
{{item.name}}
</ng-template>
@ -236,7 +246,7 @@
</div>
</div>
<div class="row justify-content-center g-0">
<div class="col-md-2 me-3" *ngIf="!filterSettings.readProgressDisabled">
<div class="col-md-2 me-3">
<label class="form-label">Read Progress</label>
<form [formGroup]="readProgressGroup">
<div class="form-check form-check-inline">
@ -254,7 +264,7 @@
</form>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.ratingDisabled">
<div class="col-md-2 me-3">
<label for="ratings" class="form-label">Rating</label>
<form class="form-inline">
<ngb-rating class="rating-star" [(rate)]="filter.rating" (rateChange)="updateRating($event)" [resettable]="true">
@ -265,9 +275,9 @@
</form>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.ageRatingDisabled">
<div class="col-md-2 me-3">
<label for="age-rating" class="form-label">Age Rating</label>
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads" [disabled]="filterSettings.ageRatingDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -277,9 +287,10 @@
</app-typeahead>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.languageDisabled">
<div class="col-md-2 me-3">
<label for="languages" class="form-label">Language</label>
<app-typeahead (selectedData)="updateLanguageRating($event)" [settings]="languageSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updateLanguages($event)" [settings]="languageSettings"
[reset]="resetTypeaheads" [disabled]="filterSettings.languageDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -289,9 +300,10 @@
</app-typeahead>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.publicationStatusDisabled">
<div class="col-md-2 me-3">
<label for="publication-status" class="form-label">Publication Status</label>
<app-typeahead (selectedData)="updatePublicationStatus($event)" [settings]="publicationStatusSettings" [reset]="resetTypeaheads">
<app-typeahead (selectedData)="updatePublicationStatus($event)" [settings]="publicationStatusSettings"
[reset]="resetTypeaheads" [disabled]="filterSettings.publicationStatusDisabled">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
@ -313,11 +325,11 @@
</div>
</form>
</div>
<div class="col-md-2 me-3" *ngIf="!filterSettings.sortDisabled">
<div class="col-md-2 me-3">
<form [formGroup]="sortGroup">
<div class="mb-3">
<label for="sort-options" class="form-label">Sort By</label>
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;" [disabled]="filterSettings.sortDisabled">
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
<ng-template #descSort>
<i class="fa fa-arrow-down" title="Descending"></i>
@ -332,7 +344,6 @@
</div>
</form>
</div>
<div class="col-md-2 me-3" *ngIf="filterSettings.sortDisabled"></div>
<div class="col-md-2 me-3"></div>
<div class="col-md-2 me-3 mt-4">
<button class="btn btn-secondary col-12" (click)="clear()">Clear</button>

View File

@ -53,7 +53,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
tagsSettings: TypeaheadSettings<Tag> = new TypeaheadSettings();
languageSettings: TypeaheadSettings<Language> = new TypeaheadSettings();
peopleSettings: {[PersonRole: string]: TypeaheadSettings<Person>} = {};
resetTypeaheads: Subject<boolean> = new ReplaySubject(1);
resetTypeaheads: ReplaySubject<boolean> = new ReplaySubject(1);
/**
* Controls the visiblity of extended controls that sit below the main header.
@ -71,6 +71,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
updateApplied: number = 0;
fullyLoaded: boolean = false;
private onDestory: Subject<void> = new Subject();
@ -84,20 +86,32 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
constructor(private libraryService: LibraryService, private metadataService: MetadataService, private seriesService: SeriesService,
private utilityService: UtilityService, private collectionTagService: CollectionTagService) {
}
ngOnInit(): void {
if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings();
}
if (this.filterOpen) {
this.filterOpen.pipe(takeUntil(this.onDestory)).subscribe(openState => {
this.filteringCollapsed = !openState;
});
}
this.filter = this.seriesService.createSeriesFilter();
this.readProgressGroup = new FormGroup({
read: new FormControl(this.filter.readStatus.read, []),
notRead: new FormControl(this.filter.readStatus.notRead, []),
inProgress: new FormControl(this.filter.readStatus.inProgress, []),
read: new FormControl({value: this.filter.readStatus.read, disabled: this.filterSettings.readProgressDisabled}, []),
notRead: new FormControl({value: this.filter.readStatus.notRead, disabled: this.filterSettings.readProgressDisabled}, []),
inProgress: new FormControl({value: this.filter.readStatus.inProgress, disabled: this.filterSettings.readProgressDisabled}, []),
});
this.sortGroup = new FormGroup({
sortField: new FormControl(this.filter.sortOptions?.sortField || SortField.SortName, []),
sortField: new FormControl({value: this.filter.sortOptions?.sortField || SortField.SortName, disabled: this.filterSettings.sortDisabled}, []),
});
this.seriesNameGroup = new FormGroup({
seriesNameQuery: new FormControl(this.filter.seriesNameQuery || '', [])
seriesNameQuery: new FormControl({value: this.filter.seriesNameQuery || '', disabled: this.filterSettings.searchNameDisabled}, [])
});
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestory)).subscribe(changes => {
@ -138,19 +152,21 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
.subscribe(changes => {
this.filter.seriesNameQuery = changes;
});
this.loadFromPresetsAndSetup();
}
ngOnInit(): void {
if (this.filterSettings === undefined) {
this.filterSettings = new FilterSettings();
ngOnDestroy() {
this.onDestory.next();
this.onDestory.complete();
}
if (this.filterOpen) {
this.filterOpen.pipe(takeUntil(this.onDestory)).subscribe(openState => {
this.filteringCollapsed = !openState;
});
getPersonsSettings(role: PersonRole) {
return this.peopleSettings[role];
}
loadFromPresetsAndSetup() {
this.fullyLoaded = false;
if (this.filterSettings.presets) {
this.readProgressGroup.get('read')?.patchValue(this.filterSettings.presets.readStatus.read);
this.readProgressGroup.get('notRead')?.patchValue(this.filterSettings.presets.readStatus.notRead);
@ -174,21 +190,6 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
}
this.setupTypeaheads();
}
ngOnDestroy() {
this.onDestory.next();
this.onDestory.complete();
}
getPersonsSettings(role: PersonRole) {
return this.peopleSettings[role];
}
setupTypeaheads() {
this.setupFormatTypeahead();
forkJoin([
@ -201,7 +202,8 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.setupGenreTypeahead(),
this.setupPersonTypeahead(),
]).subscribe(results => {
this.resetTypeaheads.next(true);
this.fullyLoaded = true;
this.resetTypeaheads.next(false); // Pass false to ensure we reset to the preset and not to an empty typeahead
if (this.filterSettings.openByDefault) {
this.filteringCollapsed = false;
}
@ -226,8 +228,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.formats && this.filterSettings.presets?.formats.length > 0) {
this.formatSettings.savedData = mangaFormatFilters.filter(item => this.filterSettings.presets?.formats.includes(item.value));
this.filter.formats = this.formatSettings.savedData.map(item => item.value);
this.resetTypeaheads.next(true);
this.updateFormatFilters(this.formatSettings.savedData);
}
}
@ -251,7 +252,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.libraries && this.filterSettings.presets?.libraries.length > 0) {
return this.librarySettings.fetchFn('').pipe(map(libraries => {
this.librarySettings.savedData = libraries.filter(item => this.filterSettings.presets?.libraries.includes(item.id));
this.filter.libraries = this.librarySettings.savedData.map(item => item.id);
this.updateLibraryFilters(this.librarySettings.savedData);
return of(true);
}));
}
@ -278,7 +279,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.genres && this.filterSettings.presets?.genres.length > 0) {
return this.genreSettings.fetchFn('').pipe(map(genres => {
this.genreSettings.savedData = genres.filter(item => this.filterSettings.presets?.genres.includes(item.id));
this.filter.genres = this.genreSettings.savedData.map(item => item.id);
this.updateGenreFilters(this.genreSettings.savedData);
return of(true);
}));
}
@ -305,7 +306,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.ageRating && this.filterSettings.presets?.ageRating.length > 0) {
return this.ageRatingSettings.fetchFn('').pipe(map(rating => {
this.ageRatingSettings.savedData = rating.filter(item => this.filterSettings.presets?.ageRating.includes(item.value));
this.filter.ageRating = this.ageRatingSettings.savedData.map(item => item.value);
this.updateAgeRating(this.ageRatingSettings.savedData);
return of(true);
}));
}
@ -332,7 +333,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.publicationStatus && this.filterSettings.presets?.publicationStatus.length > 0) {
return this.publicationStatusSettings.fetchFn('').pipe(map(statuses => {
this.publicationStatusSettings.savedData = statuses.filter(item => this.filterSettings.presets?.publicationStatus.includes(item.value));
this.filter.publicationStatus = this.publicationStatusSettings.savedData.map(item => item.value);
this.updatePublicationStatus(this.publicationStatusSettings.savedData);
return of(true);
}));
}
@ -358,7 +359,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.tags && this.filterSettings.presets?.tags.length > 0) {
return this.tagsSettings.fetchFn('').pipe(map(tags => {
this.tagsSettings.savedData = tags.filter(item => this.filterSettings.presets?.tags.includes(item.id));
this.filter.tags = this.tagsSettings.savedData.map(item => item.id);
this.updateTagFilters(this.tagsSettings.savedData);
return of(true);
}));
}
@ -384,7 +385,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.languages && this.filterSettings.presets?.languages.length > 0) {
return this.languageSettings.fetchFn('').pipe(map(languages => {
this.languageSettings.savedData = languages.filter(item => this.filterSettings.presets?.languages.includes(item.isoCode));
this.filter.languages = this.languageSettings.savedData.map(item => item.isoCode);
this.updateLanguages(this.languageSettings.savedData);
return of(true);
}));
}
@ -410,7 +411,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
if (this.filterSettings.presets?.collectionTags && this.filterSettings.presets?.collectionTags.length > 0) {
return this.collectionSettings.fetchFn('').pipe(map(tags => {
this.collectionSettings.savedData = tags.filter(item => this.filterSettings.presets?.collectionTags.includes(item.id));
this.filter.collectionTags = this.collectionSettings.savedData.map(item => item.id);
this.updateCollectionFilters(this.collectionSettings.savedData);
return of(true);
}));
}
@ -423,16 +424,15 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
const fetch = personSettings.fetchFn as ((filter: string) => Observable<Person[]>);
return fetch('').pipe(map(people => {
personSettings.savedData = people.filter(item => presetField.includes(item.id));
peopleFilterField = personSettings.savedData.map(item => item.id);
this.resetTypeaheads.next(true);
this.peopleSettings[role] = personSettings;
this.updatePersonFilters(personSettings.savedData as Person[], role);
this.updatePersonFilters(personSettings.savedData, role);
return true;
}));
} else {
}
this.peopleSettings[role] = personSettings;
return of(true);
}
}
setupPersonTypeahead() {
@ -449,8 +449,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.updateFromPreset('penciller', this.filter.penciller, this.filterSettings.presets?.penciller, PersonRole.Penciller),
this.updateFromPreset('publisher', this.filter.publisher, this.filterSettings.presets?.publisher, PersonRole.Publisher),
this.updateFromPreset('translators', this.filter.translators, this.filterSettings.presets?.translators, PersonRole.Translator)
]).pipe(map(results => {
this.resetTypeaheads.next(true);
]).pipe(map(_ => {
return of(true);
}));
}
@ -537,6 +536,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
updateRating(rating: any) {
if (this.filterSettings.ratingDisabled) return;
this.filter.rating = rating;
}
@ -548,7 +548,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.filter.publicationStatus = dtos.map(item => item.value) || [];
}
updateLanguageRating(languages: Language[]) {
updateLanguages(languages: Language[]) {
this.filter.languages = languages.map(item => item.isoCode) || [];
}
@ -563,6 +563,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
}
updateSortOrder() {
if (this.filterSettings.sortDisabled) return;
this.isAscendingSort = !this.isAscendingSort;
if (this.filter.sortOptions === null) {
this.filter.sortOptions = {
@ -582,7 +583,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
this.sortGroup.get('sortField')?.setValue(SortField.SortName);
this.isAscendingSort = true;
// Apply any presets which will trigger the apply
this.setupTypeaheads();
this.loadFromPresetsAndSetup();
}
apply() {

View File

@ -1,10 +1,10 @@
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNavChangeEvent, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
import { NgbModal, NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin, Subject } from 'rxjs';
import { finalize, map, take, takeUntil, takeWhile } from 'rxjs/operators';
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service';
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';

View File

@ -4,12 +4,12 @@
<!-- This first row will have random information about the series-->
<div class="row g-0 mb-2">
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo('ageRating', seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
<ng-container *ngIf="series">
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo('languages', seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status" a11y-click="13,32" class="col-auto" (click)="goTo('publicationStatus', seriesMetadata.publicationStatus)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo('format', series.format)" [selectionMode]="TagBadgeCursor.Clickable">
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Format, series.format)" [selectionMode]="TagBadgeCursor.Clickable">
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
</app-tag-badge>
<app-tag-badge title="Last Read" class="col-auto" *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'" [selectionMode]="TagBadgeCursor.Selectable">
@ -25,7 +25,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.genres">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo('genres', item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Genres, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
@ -70,7 +70,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.writers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('writers', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Writers, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -89,7 +89,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.coverArtists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('coverArtists', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.CoverArtists, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -102,7 +102,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('character', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Character, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -115,7 +115,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.colorists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('colorist', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Colorist, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -128,7 +128,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.editors">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('editor', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Editor, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -141,7 +141,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.inkers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('inker', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Inker, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -154,7 +154,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.letterers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('letterer', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Letterer, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -166,7 +166,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.tags">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo('tags', item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
@ -178,7 +178,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.translators">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('translators', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Translator, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -191,7 +191,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.pencillers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('penciller', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Penciller, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
@ -204,7 +204,7 @@
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.publishers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo('publisher', item.id)" [person]="item"></app-person-badge>
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Publisher, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>

View File

@ -1,6 +1,7 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
import { UtilityService } from '../shared/_services/utility.service';
import { MangaFormat } from '../_models/manga-format';
import { ReadingList } from '../_models/reading-list';
@ -38,6 +39,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
return TagBadgeCursor;
}
get FilterQueryParam() {
return FilterQueryParam;
}
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private router: Router) { }
ngOnChanges(changes: SimpleChanges): void {
@ -64,10 +69,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
this.isCollapsed = !this.isCollapsed;
}
goTo(queryParamName: string, filter: any) {
goTo(queryParamName: FilterQueryParam, filter: any) {
let params: any = {};
params[queryParamName] = filter;
params['page'] = 1;
params[FilterQueryParam.Page] = 1;
this.router.navigate(['library', this.series.libraryId], {queryParams: params});
}

View File

@ -1,10 +1,42 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { LibraryType } from 'src/app/_models/library';
import { Pagination } from 'src/app/_models/pagination';
import { SeriesFilter, SortField } from 'src/app/_models/series-filter';
import { SeriesService } from 'src/app/_services/series.service';
/**
* Used to pass state between the filter and the url
*/
export enum FilterQueryParam {
Format = 'format',
Genres = 'genres',
AgeRating = 'ageRating',
PublicationStatus = 'publicationStatus',
Tags = 'tags',
Languages = 'languages',
CollectionTags = 'collectionTags',
Libraries = 'libraries',
Writers = 'writers',
Artists = 'artists',
Character = 'character',
Colorist = 'colorist',
CoverArtists = 'coverArtists',
Editor = 'editor',
Inker = 'inker',
Letterer = 'letterer',
Penciller = 'penciller',
Publisher = 'publisher',
Translator = 'translators',
ReadStatus = 'readStatus',
SortBy = 'sortBy',
Rating = 'rating',
Name = 'name',
/**
* This is a pagination control
*/
Page = 'page'
}
@Injectable({
providedIn: 'root'
})
@ -38,10 +70,11 @@ export class FilterUtilitiesService {
/**
* Will fetch current page from route if present
* @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data
* @returns A default pagination object
*/
pagination(): Pagination {
return {currentPage: parseInt(this.route.snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1};
pagination(snapshot: ActivatedRouteSnapshot): Pagination {
return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1};
}
@ -55,45 +88,43 @@ export class FilterUtilitiesService {
if (filter === undefined) return currentUrl;
let params = '';
params += this.joinFilter(filter.formats, FilterQueryParam.Format);
params += this.joinFilter(filter.genres, FilterQueryParam.Genres);
params += this.joinFilter(filter.ageRating, FilterQueryParam.AgeRating);
params += this.joinFilter(filter.publicationStatus, FilterQueryParam.PublicationStatus);
params += this.joinFilter(filter.tags, FilterQueryParam.Tags);
params += this.joinFilter(filter.languages, FilterQueryParam.Languages);
params += this.joinFilter(filter.collectionTags, FilterQueryParam.CollectionTags);
params += this.joinFilter(filter.libraries, FilterQueryParam.Libraries);
params += this.joinFilter(filter.formats, 'format');
params += this.joinFilter(filter.genres, 'genres');
params += this.joinFilter(filter.ageRating, 'ageRating');
params += this.joinFilter(filter.publicationStatus, 'publicationStatus');
params += this.joinFilter(filter.tags, 'tags');
params += this.joinFilter(filter.languages, 'languages');
params += this.joinFilter(filter.collectionTags, 'collectionTags');
params += this.joinFilter(filter.libraries, 'libraries');
params += this.joinFilter(filter.writers, 'writers');
params += this.joinFilter(filter.artists, 'artists');
params += this.joinFilter(filter.character, 'character');
params += this.joinFilter(filter.colorist, 'colorist');
params += this.joinFilter(filter.coverArtist, 'coverArtists');
params += this.joinFilter(filter.editor, 'editor');
params += this.joinFilter(filter.inker, 'inker');
params += this.joinFilter(filter.letterer, 'letterer');
params += this.joinFilter(filter.penciller, 'penciller');
params += this.joinFilter(filter.publisher, 'publisher');
params += this.joinFilter(filter.translators, 'translators');
params += this.joinFilter(filter.writers, FilterQueryParam.Writers);
params += this.joinFilter(filter.artists, FilterQueryParam.Artists);
params += this.joinFilter(filter.character, FilterQueryParam.Character);
params += this.joinFilter(filter.colorist, FilterQueryParam.Colorist);
params += this.joinFilter(filter.coverArtist, FilterQueryParam.CoverArtists);
params += this.joinFilter(filter.editor, FilterQueryParam.Editor);
params += this.joinFilter(filter.inker, FilterQueryParam.Inker);
params += this.joinFilter(filter.letterer, FilterQueryParam.Letterer);
params += this.joinFilter(filter.penciller, FilterQueryParam.Penciller);
params += this.joinFilter(filter.publisher, FilterQueryParam.Publisher);
params += this.joinFilter(filter.translators, FilterQueryParam.Translator);
// readStatus (we need to do an additonal check as there is a default case)
if (filter.readStatus && filter.readStatus.inProgress !== true && filter.readStatus.notRead !== true && filter.readStatus.read !== true) {
params += '&readStatus=' + `${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`;
params += `&${FilterQueryParam.ReadStatus}=${filter.readStatus.inProgress},${filter.readStatus.notRead},${filter.readStatus.read}`;
}
// sortBy (additional check to not save to url if default case)
if (filter.sortOptions && !(filter.sortOptions.sortField === SortField.SortName && filter.sortOptions.isAscending === true)) {
params += '&sortBy=' + filter.sortOptions.sortField + ',' + filter.sortOptions.isAscending;
params += `&${FilterQueryParam.SortBy}=${filter.sortOptions.sortField},${filter.sortOptions.isAscending}`;
}
if (filter.rating > 0) {
params += '&rating=' + filter.rating;
params += `&${FilterQueryParam.Rating}=${filter.rating}`;
}
if (filter.seriesNameQuery !== '') {
params += '&name=' + encodeURIComponent(filter.seriesNameQuery);
params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`;
}
return currentUrl + params;
@ -102,143 +133,143 @@ export class FilterUtilitiesService {
private joinFilter(filterProp: Array<any>, key: string) {
let params = '';
if (filterProp.length > 0) {
params += `&${key}=` + filterProp.join(',');
params += `&${key}=${filterProp.join(',')}`;
}
return params;
}
/**
* Returns a new instance of a filterSettings that is populated with filter presets from URL
* @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data
* @returns The Preset filter and if something was set within
*/
filterPresetsFromUrl(): [SeriesFilter, boolean] {
const snapshot = this.route.snapshot;
filterPresetsFromUrl(snapshot: ActivatedRouteSnapshot): [SeriesFilter, boolean] {
const filter = this.seriesService.createSeriesFilter();
let anyChanged = false;
const format = snapshot.queryParamMap.get('format');
const format = snapshot.queryParamMap.get(FilterQueryParam.Format);
if (format !== undefined && format !== null) {
filter.formats = [...filter.formats, ...format.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const genres = snapshot.queryParamMap.get('genres');
const genres = snapshot.queryParamMap.get(FilterQueryParam.Genres);
if (genres !== undefined && genres !== null) {
filter.genres = [...filter.genres, ...genres.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const ageRating = snapshot.queryParamMap.get('ageRating');
const ageRating = snapshot.queryParamMap.get(FilterQueryParam.AgeRating);
if (ageRating !== undefined && ageRating !== null) {
filter.ageRating = [...filter.ageRating, ...ageRating.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const publicationStatus = snapshot.queryParamMap.get('publicationStatus');
const publicationStatus = snapshot.queryParamMap.get(FilterQueryParam.PublicationStatus);
if (publicationStatus !== undefined && publicationStatus !== null) {
filter.publicationStatus = [...filter.publicationStatus, ...publicationStatus.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const tags = snapshot.queryParamMap.get('tags');
const tags = snapshot.queryParamMap.get(FilterQueryParam.Tags);
if (tags !== undefined && tags !== null) {
filter.tags = [...filter.tags, ...tags.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const languages = snapshot.queryParamMap.get('languages');
const languages = snapshot.queryParamMap.get(FilterQueryParam.Languages);
if (languages !== undefined && languages !== null) {
filter.languages = [...filter.languages, ...languages.split(',')];
anyChanged = true;
}
const writers = snapshot.queryParamMap.get('writers');
const writers = snapshot.queryParamMap.get(FilterQueryParam.Writers);
if (writers !== undefined && writers !== null) {
filter.writers = [...filter.writers, ...writers.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const artists = snapshot.queryParamMap.get('artists');
const artists = snapshot.queryParamMap.get(FilterQueryParam.Artists);
if (artists !== undefined && artists !== null) {
filter.artists = [...filter.artists, ...artists.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const character = snapshot.queryParamMap.get('character');
const character = snapshot.queryParamMap.get(FilterQueryParam.Character);
if (character !== undefined && character !== null) {
filter.character = [...filter.character, ...character.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const colorist = snapshot.queryParamMap.get('colorist');
const colorist = snapshot.queryParamMap.get(FilterQueryParam.Colorist);
if (colorist !== undefined && colorist !== null) {
filter.colorist = [...filter.colorist, ...colorist.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const coverArtists = snapshot.queryParamMap.get('coverArtists');
const coverArtists = snapshot.queryParamMap.get(FilterQueryParam.CoverArtists);
if (coverArtists !== undefined && coverArtists !== null) {
filter.coverArtist = [...filter.coverArtist, ...coverArtists.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const editor = snapshot.queryParamMap.get('editor');
const editor = snapshot.queryParamMap.get(FilterQueryParam.Editor);
if (editor !== undefined && editor !== null) {
filter.editor = [...filter.editor, ...editor.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const inker = snapshot.queryParamMap.get('inker');
const inker = snapshot.queryParamMap.get(FilterQueryParam.Inker);
if (inker !== undefined && inker !== null) {
filter.inker = [...filter.inker, ...inker.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const letterer = snapshot.queryParamMap.get('letterer');
const letterer = snapshot.queryParamMap.get(FilterQueryParam.Letterer);
if (letterer !== undefined && letterer !== null) {
filter.letterer = [...filter.letterer, ...letterer.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const penciller = snapshot.queryParamMap.get('penciller');
const penciller = snapshot.queryParamMap.get(FilterQueryParam.Penciller);
if (penciller !== undefined && penciller !== null) {
filter.penciller = [...filter.penciller, ...penciller.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const publisher = snapshot.queryParamMap.get('publisher');
const publisher = snapshot.queryParamMap.get(FilterQueryParam.Publisher);
if (publisher !== undefined && publisher !== null) {
filter.publisher = [...filter.publisher, ...publisher.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const translators = snapshot.queryParamMap.get('translators');
const translators = snapshot.queryParamMap.get(FilterQueryParam.Translator);
if (translators !== undefined && translators !== null) {
filter.translators = [...filter.translators, ...translators.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const libraries = snapshot.queryParamMap.get('libraries');
const libraries = snapshot.queryParamMap.get(FilterQueryParam.Libraries);
if (libraries !== undefined && libraries !== null) {
filter.libraries = [...filter.libraries, ...libraries.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
const collectionTags = snapshot.queryParamMap.get('collectionTags');
const collectionTags = snapshot.queryParamMap.get(FilterQueryParam.CollectionTags);
if (collectionTags !== undefined && collectionTags !== null) {
filter.collectionTags = [...filter.collectionTags, ...collectionTags.split(',').map(item => parseInt(item, 10))];
anyChanged = true;
}
// Rating, seriesName,
const rating = snapshot.queryParamMap.get('rating');
const rating = snapshot.queryParamMap.get(FilterQueryParam.Rating);
if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) {
filter.rating = parseInt(rating, 10);
anyChanged = true;
}
/// Read status is encoded as true,true,true
const readStatus = snapshot.queryParamMap.get('readStatus');
const readStatus = snapshot.queryParamMap.get(FilterQueryParam.ReadStatus);
if (readStatus !== undefined && readStatus !== null) {
const values = readStatus.split(',').map(i => i === 'true');
if (values.length === 3) {
@ -249,7 +280,7 @@ export class FilterUtilitiesService {
}
}
const sortBy = snapshot.queryParamMap.get('sortBy');
const sortBy = snapshot.queryParamMap.get(FilterQueryParam.SortBy);
if (sortBy !== undefined && sortBy !== null) {
const values = sortBy.split(',');
if (values.length === 1) {
@ -264,7 +295,7 @@ export class FilterUtilitiesService {
}
}
const searchNameQuery = snapshot.queryParamMap.get('name');
const searchNameQuery = snapshot.queryParamMap.get(FilterQueryParam.Name);
if (searchNameQuery !== undefined && searchNameQuery !== null && searchNameQuery !== '') {
filter.seriesNameQuery = decodeURIComponent(searchNameQuery);
anyChanged = true;

View File

@ -4,7 +4,7 @@
<ng-content select="[title]"></ng-content>
<ng-content select="[subtitle]"></ng-content>
</div>
<div class="col mr-auto hide-if-empty d-none d-sm-flex">
<div class="col mr-auto d-none d-sm-flex hide-if-empty">
<ng-content select="[main]"></ng-content>
</div>
<div class="col" *ngIf="hasFilter">

View File

@ -1,5 +1,5 @@
.hide-if-empty:empty {
display: none;
display: none !important;
}
::ng-deep .companion-bar {

View File

@ -5,17 +5,17 @@
<span class="visually-hidden">Field is locked</span>
</span>
</ng-container>
<div class="typeahead-input" (click)="onInputFocus($event)">
<div class="typeahead-input" [ngClass]="{'disabled': disabled}" (click)="onInputFocus($event)">
<app-tag-badge *ngFor="let option of optionSelection.selected(); let i = index">
<ng-container [ngTemplateOutlet]="badgeTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: i }"></ng-container>
<i class="fa fa-times" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
<i class="fa fa-times" *ngIf="!disabled" (click)="toggleSelection(option)" tabindex="0" aria-label="close"></i>
</app-tag-badge>
<input #input [id]="settings.id" type="text" autocomplete="off" formControlName="typeahead">
<input #input [id]="settings.id" type="text" autocomplete="off" formControlName="typeahead" *ngIf="!disabled">
<div class="spinner-border spinner-border-sm {{settings.multiple ? 'close-offset' : ''}}" role="status" *ngIf="isLoadingOptions">
<span class="visually-hidden">Loading...</span>
</div>
<ng-container *ngIf="settings.multiple && (selectedData | async) as selected">
<ng-container *ngIf="!disabled && settings.multiple && (selectedData | async) as selected">
<button class="btn btn-close float-end mt-2" *ngIf="selected.length > 0" style="font-size: 0.8rem;" (click)="clearSelections()"></button>
</ng-container>
</div>

View File

@ -43,6 +43,10 @@ input {
border: 1px solid var(--input-border-color);
color: var(--body-text-color);
&.disabled {
cursor: not-allowed !important;
}
input {
outline: 0 !important;
border-radius: .28571429rem;

View File

@ -2,7 +2,7 @@ import { DOCUMENT } from '@angular/common';
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { KEY_CODES } from '../shared/_services/utility.service';
import { SelectionCompareFn, TypeaheadSettings } from './typeahead-settings';
@ -141,18 +141,23 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
*/
@Input() settings!: TypeaheadSettings<any>;
/**
* When true, component will re-init and set back to false.
* When true, will reset field to no selections. When false, will reset to saved data
*/
@Input() reset: Subject<boolean> = new ReplaySubject(1);
@Input() reset: ReplaySubject<boolean> = new ReplaySubject(1);
/**
* When a field is locked, we render custom css to indicate to the user. Does not affect functionality.
*/
@Input() locked: boolean = false;
/**
* If disabled, a user will not be able to interact with the typeahead
*/
@Input() disabled: boolean = false;
@Output() selectedData = new EventEmitter<any[] | any>();
@Output() newItemAdded = new EventEmitter<any[] | any>();
@Output() onUnlock = new EventEmitter<void>();
@Output() lockedChange = new EventEmitter<boolean>();
@ViewChild('input') inputElem!: ElementRef<HTMLInputElement>;
@ContentChild('optionItem') optionTemplate!: TemplateRef<any>;
@ContentChild('badgeItem') badgeTemplate!: TemplateRef<any>;
@ -178,8 +183,8 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
ngOnInit() {
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((reset: boolean) => {
this.clearSelections();
this.reset.pipe(takeUntil(this.onDestroy)).subscribe((resetToEmpty: boolean) => {
this.clearSelections(resetToEmpty);
this.init();
});
@ -274,6 +279,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
@HostListener('window:keydown', ['$event'])
handleKeyPress(event: KeyboardEvent) {
if (!this.hasFocus) { return; }
if (this.disabled) return;
switch(event.key) {
case KEY_CODES.DOWN_ARROW:
@ -347,15 +353,26 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
this.resetField();
}
clearSelections() {
clearSelections(untoggleAll: boolean = false) {
if (this.optionSelection) {
if (!untoggleAll && this.settings.savedData) {
const isArray = this.settings.savedData.hasOwnProperty('length');
if (isArray) {
this.optionSelection = new SelectionModel<any>(true, this.settings.savedData);
} else {
this.optionSelection = new SelectionModel<any>(true, [this.settings.savedData]);
}
} else {
this.optionSelection.selected().forEach(item => this.optionSelection.toggle(item, false));
}
this.selectedData.emit(this.optionSelection.selected());
this.resetField();
}
}
handleOptionClick(opt: any) {
if (this.disabled) return;
if (!this.settings.multiple && this.optionSelection.selected().length > 0) {
return;
}
@ -402,6 +419,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
event.stopPropagation();
event.preventDefault();
}
if (this.disabled) return;
if (!this.settings.multiple && this.optionSelection.selected().length > 0) {
return;
@ -452,6 +470,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
}
unlock(event: any) {
if (this.disabled) return;
this.locked = !this.locked;
this.onUnlock.emit();
this.lockedChange.emit(this.locked);