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