Must Contains Filter (#2249)
* Removed docker-compose.yml as it's not used and may confuse users. * Added ability to delete single collections from card actions. Updated transloco library which fixes older iOS browsers not being able to load Kavita. * Added a Must Contains comparison which will make so all values must exist. * Fixed up multiselect dropdowns not reseting value when changing filter field
This commit is contained in:
parent
b5540e58e0
commit
072fadf1de
20 changed files with 210 additions and 87 deletions
|
|
@ -32,7 +32,7 @@ public class CollectionController : BaseApiController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[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<SeriesMetadata>();
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the collection tag from all Series it was attached to
|
||||
/// </summary>
|
||||
/// <param name="tagId"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> 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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,20 @@ public enum FilterComparison
|
|||
LessThan = 3,
|
||||
LessThanEqual = 4,
|
||||
/// <summary>
|
||||
///
|
||||
/// value is within any of the series. This is inheritently an OR, even if combinator is an AND
|
||||
/// </summary>
|
||||
/// <remarks>Only works with IList</remarks>
|
||||
Contains = 5,
|
||||
/// <summary>
|
||||
/// value is within All of the series. This is an AND, even if combinator ORs the different statements
|
||||
/// </summary>
|
||||
/// <remarks>Only works with IList</remarks>
|
||||
MustContains = 6,
|
||||
/// <summary>
|
||||
/// Performs a LIKE %value%
|
||||
/// </summary>
|
||||
Matches = 6,
|
||||
NotContains = 7,
|
||||
Matches = 7,
|
||||
NotContains = 8,
|
||||
/// <summary>
|
||||
/// Not Equal to
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IQueryable<Series>>()
|
||||
{
|
||||
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<IQueryable<Series>>()
|
||||
{
|
||||
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<IQueryable<Series>>()
|
||||
{
|
||||
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<IQueryable<Series>>()
|
||||
{
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ namespace API.Services;
|
|||
public interface ICollectionTagService
|
||||
{
|
||||
Task<bool> TagExistsByName(string name);
|
||||
Task<bool> DeleteTag(CollectionTag tag);
|
||||
Task<bool> UpdateTag(CollectionTagDto dto);
|
||||
Task<bool> AddTagToSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
||||
Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
||||
|
|
@ -49,6 +50,12 @@ public class CollectionTagService : ICollectionTagService
|
|||
return await _unitOfWork.CollectionTagRepository.TagExists(name);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteTag(CollectionTag tag)
|
||||
{
|
||||
_unitOfWork.CollectionTagRepository.Remove(tag);
|
||||
return await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateTag(CollectionTagDto dto)
|
||||
{
|
||||
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(dto.Id);
|
||||
|
|
@ -130,6 +137,7 @@ public class CollectionTagService : ICollectionTagService
|
|||
public async Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds)
|
||||
{
|
||||
if (tag == null) return false;
|
||||
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
|
||||
foreach (var seriesIdToRemove in seriesIds)
|
||||
{
|
||||
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue