diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index 8d3abb3ad..4f5f955be 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -32,7 +32,7 @@ public class CollectionController : BaseApiController } /// - /// Return a list of all collection tags on the server + /// Return a list of all collection tags on the server for the logged in user. /// /// [HttpGet] @@ -130,7 +130,6 @@ public class CollectionController : BaseApiController { var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updateSeriesForTagDto.Tag.Id, CollectionTagIncludes.SeriesMetadata); if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist")); - tag.SeriesMetadatas ??= new List(); if (await _collectionService.RemoveTagFromSeries(tag, updateSeriesForTagDto.SeriesIdsToRemove)) return Ok(await _localizationService.Translate(User.GetUserId(), "collection-updated")); @@ -142,4 +141,29 @@ public class CollectionController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); } + + /// + /// Removes the collection tag from all Series it was attached to + /// + /// + /// + [Authorize(Policy = "RequireAdminRole")] + [HttpDelete] + public async Task DeleteTag(int tagId) + { + try + { + var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId, CollectionTagIncludes.SeriesMetadata); + if (tag == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "collection-doesnt-exist")); + + if (await _collectionService.DeleteTag(tag)) + return Ok(await _localizationService.Translate(User.GetUserId(), "collection-deleted")); + } + catch (Exception) + { + await _unitOfWork.RollbackAsync(); + } + + return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error")); + } } diff --git a/API/DTOs/Filtering/v2/FilterComparision.cs b/API/DTOs/Filtering/v2/FilterComparision.cs index 6e3dc6fd8..109667dad 100644 --- a/API/DTOs/Filtering/v2/FilterComparision.cs +++ b/API/DTOs/Filtering/v2/FilterComparision.cs @@ -11,15 +11,20 @@ public enum FilterComparison LessThan = 3, LessThanEqual = 4, /// - /// + /// value is within any of the series. This is inheritently an OR, even if combinator is an AND /// /// Only works with IList Contains = 5, /// + /// value is within All of the series. This is an AND, even if combinator ORs the different statements + /// + /// Only works with IList + MustContains = 6, + /// /// Performs a LIKE %value% /// - Matches = 6, - NotContains = 7, + Matches = 7, + NotContains = 8, /// /// Not Equal to /// diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index aab679315..3457469b4 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -976,14 +976,15 @@ public class SeriesRepository : ISeriesRepository { foreach (var stmt in filter.Statements.Where(stmt => stmt.Field == FilterField.Libraries)) { + var libIds = stmt.Value.Split(',').Select(int.Parse); if (stmt.Comparison is FilterComparison.Equal or FilterComparison.Contains) { - filterIncludeLibs.AddRange(stmt.Value.Split(',').Select(int.Parse)); + filterIncludeLibs.AddRange(libIds); } else { - filterExcludeLibs.AddRange(stmt.Value.Split(',').Select(int.Parse)); + filterExcludeLibs.AddRange(libIds); } } diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index fa5e7b633..b1dfeab1f 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -1,8 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using API.DTOs.Filtering.v2; using API.Entities; using API.Entities.Enums; @@ -28,6 +26,8 @@ public static class SeriesFilter return queryable.Where(s => s.Metadata.Language.Equals(languages.First())); case FilterComparison.Contains: return queryable.Where(s => languages.Contains(s.Metadata.Language)); + case FilterComparison.MustContains: + return queryable.Where(s => languages.All(s2 => s2.Equals(s.Metadata.Language))); case FilterComparison.NotContains: return queryable.Where(s => !languages.Contains(s.Metadata.Language)); case FilterComparison.NotEqual: @@ -78,6 +78,7 @@ public static class SeriesFilter case FilterComparison.NotEqual: case FilterComparison.BeginsWith: case FilterComparison.EndsWith: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.ReleaseYear"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -112,6 +113,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.Rating"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -149,6 +151,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.AgeRating"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -182,6 +185,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.AverageReadTime"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -204,6 +208,7 @@ public static class SeriesFilter return queryable.Where(s => !pubStatues.Contains(s.Metadata.PublicationStatus)); case FilterComparison.NotEqual: return queryable.Where(s => s.Metadata.PublicationStatus != firstStatus); + case FilterComparison.MustContains: case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -273,6 +278,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.ReadProgress"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null); @@ -295,6 +301,15 @@ public static class SeriesFilter case FilterComparison.NotEqual: case FilterComparison.NotContains: return queryable.Where(s => s.Metadata.Tags.Any(t => !tags.Contains(t.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(tags.Select(gId => queryable.Where(s => s.Metadata.Tags.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -325,6 +340,15 @@ public static class SeriesFilter case FilterComparison.NotEqual: case FilterComparison.NotContains: return queryable.Where(s => s.Metadata.People.Any(t => !people.Contains(t.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(people.Select(gId => queryable.Where(s => s.Metadata.People.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -355,6 +379,15 @@ public static class SeriesFilter case FilterComparison.NotEqual: case FilterComparison.NotContains: return queryable.Where(s => s.Metadata.Genres.All(p => !genres.Contains(p.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(genres.Select(gId => queryable.Where(s => s.Metadata.Genres.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -385,6 +418,7 @@ public static class SeriesFilter case FilterComparison.NotContains: case FilterComparison.NotEqual: return queryable.Where(s => !formats.Contains(s.Format)); + case FilterComparison.MustContains: case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -407,7 +441,6 @@ public static class SeriesFilter { if (!condition || collectionTags.Count == 0) return queryable; - //var first = collectionTags.First(); switch (comparison) { case FilterComparison.Equal: @@ -416,6 +449,15 @@ public static class SeriesFilter case FilterComparison.NotContains: case FilterComparison.NotEqual: return queryable.Where(s => !s.Metadata.CollectionTags.Any(t => collectionTags.Contains(t.Id))); + case FilterComparison.MustContains: + // Deconstruct and do a Union of a bunch of where statements since this doesn't translate + var queries = new List>() + { + queryable + }; + queries.AddRange(collectionTags.Select(gId => queryable.Where(s => s.Metadata.CollectionTags.Any(p => p.Id == gId)))); + + return queries.Aggregate((q1, q2) => q1.Intersect(q2)); case FilterComparison.GreaterThan: case FilterComparison.GreaterThanEqual: case FilterComparison.LessThan: @@ -475,6 +517,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.Name"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); @@ -508,6 +551,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.Metadata.Summary"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); @@ -543,6 +587,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); @@ -618,6 +663,7 @@ public static class SeriesFilter case FilterComparison.IsAfter: case FilterComparison.IsInLast: case FilterComparison.IsNotInLast: + case FilterComparison.MustContains: throw new KavitaException($"{comparison} not applicable for Series.FolderPath"); default: throw new ArgumentOutOfRangeException(nameof(comparison), comparison, "Filter Comparison is not supported"); diff --git a/API/I18N/en.json b/API/I18N/en.json index f4807c456..59851fac5 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -43,6 +43,7 @@ "file-missing": "File was not found in book", "collection-updated": "Collection updated successfully", + "collection-deleted": "Collection deleted", "generic-error": "Something went wrong, please try again", "collection-doesnt-exist": "Collection does not exist", diff --git a/API/Services/CollectionTagService.cs b/API/Services/CollectionTagService.cs index 7c5aeaa71..b024d687a 100644 --- a/API/Services/CollectionTagService.cs +++ b/API/Services/CollectionTagService.cs @@ -17,6 +17,7 @@ namespace API.Services; public interface ICollectionTagService { Task TagExistsByName(string name); + Task DeleteTag(CollectionTag tag); Task UpdateTag(CollectionTagDto dto); Task AddTagToSeries(CollectionTag? tag, IEnumerable seriesIds); Task RemoveTagFromSeries(CollectionTag? tag, IEnumerable seriesIds); @@ -49,6 +50,12 @@ public class CollectionTagService : ICollectionTagService return await _unitOfWork.CollectionTagRepository.TagExists(name); } + public async Task DeleteTag(CollectionTag tag) + { + _unitOfWork.CollectionTagRepository.Remove(tag); + return await _unitOfWork.CommitAsync(); + } + public async Task UpdateTag(CollectionTagDto dto) { var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id); @@ -130,6 +137,7 @@ public class CollectionTagService : ICollectionTagService public async Task RemoveTagFromSeries(CollectionTag? tag, IEnumerable seriesIds) { if (tag == null) return false; + tag.SeriesMetadatas ??= new List(); foreach (var seriesIdToRemove in seriesIds) { tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove)); diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 45c6d57ea..67a882de1 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -23,7 +23,7 @@ "@iplab/ngx-file-upload": "^16.0.1", "@microsoft/signalr": "^7.0.10", "@ng-bootstrap/ng-bootstrap": "^15.1.1", - "@ngneat/transloco": "^5.0.6", + "@ngneat/transloco": "^5.0.7", "@ngneat/transloco-locale": "^5.1.1", "@ngneat/transloco-persist-lang": "^5.0.0", "@ngneat/transloco-persist-translations": "^5.0.0", @@ -3170,9 +3170,9 @@ } }, "node_modules/@ngneat/transloco": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@ngneat/transloco/-/transloco-5.0.6.tgz", - "integrity": "sha512-pt0jiU0co0nT72bhodT9ervBvSgl1jVUrTbLsHwjtP3WoJZxfOmXN21j5MSA/GJFRkolceI8+yWqtG7jux+WDg==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@ngneat/transloco/-/transloco-5.0.7.tgz", + "integrity": "sha512-x1c2e+7cOYPPVFPgqGcN3R6d7f18a4sMHzxsCamcxS2w7vWXcEzWKZ8JcI1TdpxrM+RKuj2NRfEEcr1HjAI/4w==", "dependencies": { "@ngneat/transloco-utils": "^5.0.0", "flat": "5.0.2", diff --git a/UI/Web/package.json b/UI/Web/package.json index 6e1f1b48c..05da6248c 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -28,7 +28,7 @@ "@iplab/ngx-file-upload": "^16.0.1", "@microsoft/signalr": "^7.0.10", "@ng-bootstrap/ng-bootstrap": "^15.1.1", - "@ngneat/transloco": "^5.0.6", + "@ngneat/transloco": "^5.0.7", "@ngneat/transloco-locale": "^5.1.1", "@ngneat/transloco-persist-lang": "^5.0.0", "@ngneat/transloco-persist-translations": "^5.0.0", diff --git a/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts b/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts index 64e5571bd..fa30dc786 100644 --- a/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts +++ b/UI/Web/src/app/_models/metadata/v2/filter-comparison.ts @@ -9,11 +9,12 @@ export enum FilterComparison { /// /// Only works with IList Contains = 5, + MustContains = 6, /// /// Performs a LIKE %value% /// - Matches = 6, - NotContains = 7, + Matches = 7, + NotContains = 8, /// /// Not Equal to /// @@ -42,4 +43,4 @@ export enum FilterComparison { /// Is Date not between now and X seconds ago /// IsNotInLast = 15, -} \ No newline at end of file +} diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 775cc456b..393aade6c 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -247,6 +247,14 @@ export class ActionFactoryService { requiresAdmin: true, children: [], }, + { + action: Action.Delete, + title: 'delete', + callback: this.dummyCallback, + requiresAdmin: false, + class: 'danger', + children: [], + }, ]; this.seriesActions = [ diff --git a/UI/Web/src/app/_services/collection-tag.service.ts b/UI/Web/src/app/_services/collection-tag.service.ts index 2f19352ab..3e4b8b508 100644 --- a/UI/Web/src/app/_services/collection-tag.service.ts +++ b/UI/Web/src/app/_services/collection-tag.service.ts @@ -41,4 +41,8 @@ export class CollectionTagService { tagNameExists(name: string) { return this.httpClient.get(this.baseUrl + 'collection/name-exists?name=' + name); } + + deleteTag(tagId: number) { + return this.httpClient.delete(this.baseUrl + 'collection?tagId=' + tagId, TextResonse); + } } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index 0847daa3a..c5a0fc09a 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -166,8 +166,8 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges { } hasCustomSort() { - return this.filter.sortOptions?.sortField != SortField.SortName || !this.filter.sortOptions.isAscending - || this.filterSettings.presetsV2?.sortOptions?.sortField != SortField.SortName || !this.filterSettings.presetsV2?.sortOptions?.isAscending; + return this.filter?.sortOptions?.sortField != SortField.SortName || !this.filter?.sortOptions.isAscending + || this.filterSettings?.presetsV2?.sortOptions?.sortField != SortField.SortName || !this.filterSettings?.presetsV2?.sortOptions?.isAscending; } performAction(action: ActionItem) { diff --git a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html index 365120ac3..f784460e6 100644 --- a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html +++ b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html @@ -12,7 +12,7 @@ > diff --git a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts index 16a1b15e5..ee2da9cec 100644 --- a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts +++ b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.ts @@ -1,31 +1,35 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, DestroyRef, + Component, + DestroyRef, EventEmitter, inject, OnInit } from '@angular/core'; -import { Title } from '@angular/platform-browser'; -import { Router } from '@angular/router'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { map, of} from 'rxjs'; -import { Observable } from 'rxjs/internal/Observable'; -import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; -import { CollectionTag } from 'src/app/_models/collection-tag'; -import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; -import { Tag } from 'src/app/_models/tag'; -import { AccountService } from 'src/app/_services/account.service'; -import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service'; -import { CollectionTagService } from 'src/app/_services/collection-tag.service'; -import { ImageService } from 'src/app/_services/image.service'; -import { JumpbarService } from 'src/app/_services/jumpbar.service'; +import {Title} from '@angular/platform-browser'; +import {Router} from '@angular/router'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {map, of} from 'rxjs'; +import {Observable} from 'rxjs/internal/Observable'; +import {EditCollectionTagsComponent} from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; +import {CollectionTag} from 'src/app/_models/collection-tag'; +import {JumpKey} from 'src/app/_models/jumpbar/jump-key'; +import {Tag} from 'src/app/_models/tag'; +import {AccountService} from 'src/app/_services/account.service'; +import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service'; +import {CollectionTagService} from 'src/app/_services/collection-tag.service'; +import {ImageService} from 'src/app/_services/image.service'; +import {JumpbarService} from 'src/app/_services/jumpbar.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import { NgIf, AsyncPipe, DecimalPipe } from '@angular/common'; -import { CardItemComponent } from '../../../cards/card-item/card-item.component'; -import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component'; -import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; +import {AsyncPipe, DecimalPipe, NgIf} from '@angular/common'; +import {CardItemComponent} from '../../../cards/card-item/card-item.component'; +import {CardDetailLayoutComponent} from '../../../cards/card-detail-layout/card-detail-layout.component'; +import { + SideNavCompanionBarComponent +} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; +import {ToastrService} from "ngx-toastr"; @Component({ @@ -49,11 +53,12 @@ export class AllCollectionsComponent implements OnInit { filterOpen: EventEmitter = new EventEmitter(); private readonly destroyRef = inject(DestroyRef); private readonly translocoService = inject(TranslocoService); + private readonly toastr = inject(ToastrService); constructor(private collectionService: CollectionTagService, private router: Router, private actionFactoryService: ActionFactoryService, private modalService: NgbModal, private titleService: Title, private jumpbarService: JumpbarService, - private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService, + private readonly cdRef: ChangeDetectorRef, public imageService: ImageService, public accountService: AccountService) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.titleService.setTitle('Kavita - ' + this.translocoService.translate('all-collections.title')); @@ -87,6 +92,11 @@ export class AllCollectionsComponent implements OnInit { handleCollectionActionCallback(action: ActionItem, collectionTag: CollectionTag) { switch (action.action) { + case(Action.Delete): + this.collectionService.deleteTag(collectionTag.id).subscribe(res => { + this.toastr.success(res); + }); + break; case(Action.Edit): const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true }); modalRef.componentInstance.tag = collectionTag; 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 6cf3d9236..0b19cb339 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 @@ -3,23 +3,15 @@ import { ChangeDetectorRef, Component, DestroyRef, - EventEmitter, inject, + EventEmitter, + inject, Input, OnInit, Output, } from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {FilterStatement} from '../../../_models/metadata/v2/filter-statement'; -import { - BehaviorSubject, - distinctUntilChanged, filter, - map, - Observable, - of, - startWith, - switchMap, - tap -} from 'rxjs'; +import {BehaviorSubject, distinctUntilChanged, filter, map, Observable, of, startWith, switchMap, tap} from 'rxjs'; import {MetadataService} from 'src/app/_services/metadata.service'; import {mangaFormatFilters} from 'src/app/_models/metadata/series-filter'; import {PersonRole} from 'src/app/_models/metadata/person'; @@ -50,6 +42,16 @@ const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, Fi FilterField.Formats, FilterField.CollectionTags, FilterField.Tags ]; +const DropdownFieldsWithoutMustContains = [ + FilterField.Libraries, FilterField.Formats, FilterField.AgeRating, FilterField.PublicationStatus +]; +const DropdownFieldsThatIncludeNumberComparisons = [ + FilterField.AgeRating +]; +const NumberFieldsThatIncludeDateComparisons = [ + FilterField.ReleaseYear +]; + const StringComparisons = [FilterComparison.Equal, FilterComparison.NotEqual, FilterComparison.BeginsWith, @@ -65,7 +67,8 @@ const NumberComparisons = [FilterComparison.Equal, const DropdownComparisons = [FilterComparison.Equal, FilterComparison.NotEqual, FilterComparison.Contains, - FilterComparison.NotContains]; + FilterComparison.NotContains, + FilterComparison.MustContains]; @Component({ selector: 'app-metadata-row-filter', @@ -113,7 +116,7 @@ export class MetadataFilterRowComponent implements OnInit { get MultipleDropdownAllowed() { const comp = parseInt(this.formGroup.get('comparison')?.value, 10) as FilterComparison; - return comp === FilterComparison.Contains || comp === FilterComparison.NotContains; + return comp === FilterComparison.Contains || comp === FilterComparison.NotContains || comp === FilterComparison.MustContains; } constructor(private readonly metadataService: MetadataService, private readonly libraryService: LibraryService, @@ -148,6 +151,12 @@ export class MetadataFilterRowComponent implements OnInit { value: this.formGroup.get('filterValue')?.value! }; + // Some ids can get through and be numbers, convert them to strings for the backend + if (typeof stmt.value === 'number' && !Number.isNaN(stmt.value)) { + stmt.value = stmt.value + ''; + } + + console.log('Trying to update parent with new stmt: ', stmt.value); if (!stmt.value && stmt.field !== FilterField.SeriesName) return; console.log('updating parent with new statement: ', stmt.value) this.filterStatement.emit(stmt); @@ -188,7 +197,6 @@ export class MetadataFilterRowComponent implements OnInit { getDropdownObservable(): Observable { const filterField = parseInt(this.formGroup.get('input')?.value, 10) as FilterField; - console.log('Getting dropdown observable: ', filterField); switch (filterField) { case FilterField.PublicationStatus: return this.metadataService.getAllPublicationStatus().pipe(map(pubs => pubs.map(pub => { @@ -252,15 +260,15 @@ export class MetadataFilterRowComponent implements OnInit { this.predicateType$.next(PredicateType.Text); if (this.loaded) { - this.formGroup.get('filterValue')?.patchValue('',{emitEvent: false}); + this.formGroup.get('filterValue')?.patchValue(''); console.log('setting filterValue to empty string', this.formGroup.get('filterValue')?.value) - } // BUG: undefined is getting set and the input value isn't updating and emitting to the backend + } return; } if (NumberFields.includes(inputVal)) { const comps = [...NumberComparisons]; - if (inputVal === FilterField.ReleaseYear) { + if (NumberFieldsThatIncludeDateComparisons.includes(inputVal)) { comps.push(...DateComparisons); } this.validComparisons$.next(comps); @@ -270,12 +278,16 @@ export class MetadataFilterRowComponent implements OnInit { } if (DropdownFields.includes(inputVal)) { - const comps = [...DropdownComparisons]; - if (inputVal === FilterField.AgeRating) { + let comps = [...DropdownComparisons]; + if (DropdownFieldsThatIncludeNumberComparisons.includes(inputVal)) { comps.push(...NumberComparisons); } + if (DropdownFieldsWithoutMustContains.includes(inputVal)) { + comps = comps.filter(c => c !== FilterComparison.MustContains); + } this.validComparisons$.next(comps); this.predicateType$.next(PredicateType.Dropdown); + if (this.loaded) this.formGroup.get('filterValue')?.patchValue(0); return; } } diff --git a/UI/Web/src/app/metadata-filter/_pipes/filter-comparison.pipe.ts b/UI/Web/src/app/metadata-filter/_pipes/filter-comparison.pipe.ts index 72e2e13a1..33a7c960e 100644 --- a/UI/Web/src/app/metadata-filter/_pipes/filter-comparison.pipe.ts +++ b/UI/Web/src/app/metadata-filter/_pipes/filter-comparison.pipe.ts @@ -40,6 +40,8 @@ export class FilterComparisonPipe implements PipeTransform { return translate('filter-comparison-pipe.is-in-last'); case FilterComparison.IsNotInLast: return translate('filter-comparison-pipe.is-not-in-last'); + case FilterComparison.MustContains: + return translate('filter-comparison-pipe.must-contains'); default: throw new Error(`Invalid FilterComparison value: ${value}`); } diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index e3939739a..a709b7da7 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1751,7 +1751,8 @@ "is-before": "Is before", "is-after": "Is after", "is-in-last": "Is in last", - "is-not-in-last": "Is not in last" + "is-not-in-last": "Is not in last", + "must-contains": "Must Contains" }, "toasts": { diff --git a/UI/Web/src/theme/components/_typeahead.scss b/UI/Web/src/theme/components/_typeahead.scss index 7f2bdf40f..e81d2e798 100644 --- a/UI/Web/src/theme/components/_typeahead.scss +++ b/UI/Web/src/theme/components/_typeahead.scss @@ -66,7 +66,3 @@ /* hint */ --select2-hint-text-color: #888; } - -//.select2-selection__rendered { -// padding-top: 4px; -//} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index e75c9b0c0..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: '3' -services: - kavita: - image: kizaing/kavita:latest - container_name: kavita - volumes: - - ./manga:/manga - - ./config:/kavita/config - ports: - - "5000:5000" - restart: unless-stopped - - #Uncomment if you want to implement healthchecks - #healthcheck: - # test: curl --fail http://localhost:5000 || exit 1 - # interval: 300s - # retries: 3 - # start_period: 30s - # timeout: 15s diff --git a/openapi.json b/openapi.json index 8efb4a059..2586577e6 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.14" + "version": "0.7.7.17" }, "servers": [ { @@ -1268,7 +1268,7 @@ "tags": [ "Collection" ], - "summary": "Return a list of all collection tags on the server", + "summary": "Return a list of all collection tags on the server for the logged in user.", "responses": { "200": { "description": "Success", @@ -1300,6 +1300,28 @@ } } } + }, + "delete": { + "tags": [ + "Collection" + ], + "summary": "Removes the collection tag from all Series it was attached to", + "parameters": [ + { + "name": "tagId", + "in": "query", + "description": "", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } } }, "/api/Collection/search": { @@ -13726,6 +13748,7 @@ 5, 6, 7, + 8, 9, 10, 11, @@ -18400,4 +18423,4 @@ "description": "Responsible for all things Want To Read" } ] -} +} \ No newline at end of file