mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Misc UI Fixes (#1477)
* Tweaked a Migration to log correctly only if something is going to be done. * Refactored Reading List Controller code into a dedicated service and cleaned up some methods that aren't needed anymore. * Fixed a bug where adding a new item to a reading list wasn't adding it at the end. * Fixed an issue where collection page would re-render the same covers on multiple items. * Fixed a missing margin-top which made the page extras drawer not render correctly and hence unclosable on small screens. * Added some timeout on manage users screen to give data time to flush. Added a dedicated token log for account flows, in case url encoding plays a part (but from testing it doesn't). * Reverted back to building for ES6 instead of es2020 for old Safari 12.5.5 browsers (10MB difference in build size). * Cleaned up the logic in removing series not found during scan loop. * Tweaked the timings for Library Watcher to 1 min and reprocess queue every 30 seconds.
This commit is contained in:
parent
d4285c49f3
commit
8edd5e476a
109
API.Tests/Services/ReadingListServiceTests.cs
Normal file
109
API.Tests/Services/ReadingListServiceTests.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Services;
|
||||
|
||||
public class ReadingListServiceTests
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IReadingListService _readingListService;
|
||||
|
||||
private readonly DataContext _context;
|
||||
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
private const string DataDirectory = "C:/data/";
|
||||
|
||||
public ReadingListServiceTests()
|
||||
{
|
||||
var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options;
|
||||
|
||||
_context = new DataContext(contextOptions);
|
||||
Task.Run(SeedDb).GetAwaiter().GetResult();
|
||||
|
||||
var config = new MapperConfiguration(cfg => cfg.AddProfile<AutoMapperProfiles>());
|
||||
var mapper = config.CreateMapper();
|
||||
_unitOfWork = new UnitOfWork(_context, mapper, null);
|
||||
|
||||
_readingListService = new ReadingListService(_unitOfWork, Substitute.For<ILogger<ReadingListService>>());
|
||||
}
|
||||
|
||||
#region Setup
|
||||
|
||||
private static DbConnection CreateInMemoryDatabase()
|
||||
{
|
||||
var connection = new SqliteConnection("Filename=:memory:");
|
||||
|
||||
connection.Open();
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private async Task<bool> SeedDb()
|
||||
{
|
||||
await _context.Database.MigrateAsync();
|
||||
var filesystem = CreateFileSystem();
|
||||
|
||||
await Seed.SeedSettings(_context,
|
||||
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
|
||||
|
||||
var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
|
||||
setting.Value = CacheDirectory;
|
||||
|
||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
|
||||
setting.Value = BackupDirectory;
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||
});
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
private async Task ResetDb()
|
||||
{
|
||||
_context.Series.RemoveRange(_context.Series.ToList());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static MockFileSystem CreateFileSystem()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
||||
fileSystem.AddDirectory("C:/kavita/config/");
|
||||
fileSystem.AddDirectory(CacheDirectory);
|
||||
fileSystem.AddDirectory(CoverImageDirectory);
|
||||
fileSystem.AddDirectory(BackupDirectory);
|
||||
fileSystem.AddDirectory(DataDirectory);
|
||||
|
||||
return fileSystem;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region RemoveFullyReadItems
|
||||
|
||||
// TODO: Implement all methods here
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
@ -477,6 +477,7 @@ namespace API.Controllers
|
||||
|
||||
var emailLink = GenerateEmailLink(token, "confirm-email", dto.Email);
|
||||
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
_logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, token);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
var accessible = await _emailService.CheckIfAccessible(host);
|
||||
if (accessible)
|
||||
@ -600,8 +601,10 @@ namespace API.Controllers
|
||||
if (!roles.Any(r => r is PolicyConstants.AdminRole or PolicyConstants.ChangePasswordRole))
|
||||
return Unauthorized("You are not permitted to this operation.");
|
||||
|
||||
var emailLink = GenerateEmailLink(await _userManager.GeneratePasswordResetTokenAsync(user), "confirm-reset-password", user.Email);
|
||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var emailLink = GenerateEmailLink(token, "confirm-reset-password", user.Email);
|
||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||
_logger.LogCritical("[Forgot Password]: Token {UserName}: {Token}", user.UserName, token);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
if (await _emailService.CheckIfAccessible(host))
|
||||
{
|
||||
@ -654,8 +657,10 @@ namespace API.Controllers
|
||||
"This user needs to migrate. Have them log out and login to trigger a migration flow");
|
||||
if (user.EmailConfirmed) return BadRequest("User already confirmed");
|
||||
|
||||
var emailLink = GenerateEmailLink(await _userManager.GenerateEmailConfirmationTokenAsync(user), "confirm-email", user.Email);
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var emailLink = GenerateEmailLink(token, "confirm-email", user.Email);
|
||||
_logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink);
|
||||
_logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token);
|
||||
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
||||
{
|
||||
EmailAddress = user.Email,
|
||||
@ -732,6 +737,8 @@ namespace API.Controllers
|
||||
var result = await _userManager.ConfirmEmailAsync(user, token);
|
||||
if (result.Succeeded) return true;
|
||||
|
||||
|
||||
|
||||
_logger.LogCritical("[Account] Email validation failed");
|
||||
if (!result.Errors.Any()) return false;
|
||||
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Filtering;
|
||||
@ -305,7 +306,7 @@ public class OpdsController : BaseApiController
|
||||
var userId = await GetUser(apiKey);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
|
||||
var userWithLists = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(user.UserName);
|
||||
var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName, AppUserIncludes.ReadingListsWithItems);
|
||||
var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId);
|
||||
if (readingList == null)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Services;
|
||||
@ -24,12 +25,13 @@ namespace API.Controllers
|
||||
/// <summary>
|
||||
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
|
||||
/// </summary>
|
||||
/// <param name="apiKey"></param>
|
||||
/// <remarks>This API is not fully built out and may require more information in later releases</remarks>
|
||||
/// <param name="apiKey">API key which will be used to authenticate and return a valid user token back</param>
|
||||
/// <param name="pluginName">Name of the Plugin</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPost("authenticate")]
|
||||
public async Task<ActionResult<UserDto>> Authenticate(string apiKey, string pluginName)
|
||||
public async Task<ActionResult<UserDto>> Authenticate([Required] string apiKey, [Required] string pluginName)
|
||||
{
|
||||
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
|
||||
// Should log into access table so we can tell the user
|
||||
|
@ -8,6 +8,7 @@ using API.DTOs.ReadingLists;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -19,12 +20,14 @@ namespace API.Controllers
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub)
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_readingListService = readingListService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,6 +58,11 @@ namespace API.Controllers
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Reading Lists the user has access to that have a series within it.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("lists-for-series")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
|
||||
{
|
||||
@ -78,17 +86,6 @@ namespace API.Controllers
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
private async Task<AppUser?> UserHasReadingListAccess(int readingListId)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
|
||||
AppUserIncludes.ReadingLists);
|
||||
if (user.ReadingLists.SingleOrDefault(rl => rl.Id == readingListId) == null && !await _unitOfWork.UserRepository.IsUserAdminAsync(user))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an items position
|
||||
@ -99,25 +96,14 @@ namespace API.Controllers
|
||||
public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
|
||||
{
|
||||
// Make sure UI buffers events
|
||||
var user = await UserHasReadingListAccess(dto.ReadingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
||||
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
||||
items.Remove(item);
|
||||
items.Insert(dto.ToPosition, item);
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Order = i;
|
||||
}
|
||||
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated");
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Updated");
|
||||
}
|
||||
|
||||
return BadRequest("Couldn't update position");
|
||||
}
|
||||
@ -130,25 +116,13 @@ namespace API.Controllers
|
||||
[HttpPost("delete-item")]
|
||||
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(dto.ReadingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).ToList();
|
||||
|
||||
var index = 0;
|
||||
foreach (var readingListItem in readingList.Items)
|
||||
{
|
||||
readingListItem.Order = index;
|
||||
index++;
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
if (await _readingListService.DeleteReadingListItem(dto))
|
||||
{
|
||||
return Ok("Updated");
|
||||
}
|
||||
@ -164,34 +138,16 @@ namespace API.Controllers
|
||||
[HttpPost("remove-read")]
|
||||
public async Task<ActionResult> DeleteReadFromList([FromQuery] int readingListId)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(readingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id);
|
||||
items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(user.Id, items.ToList());
|
||||
|
||||
// Collect all Ids to remove
|
||||
var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id);
|
||||
|
||||
try
|
||||
if (await _readingListService.RemoveFullyReadItems(readingListId, user))
|
||||
{
|
||||
var listItems =
|
||||
(await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r =>
|
||||
itemIdsToRemove.Contains(r.Id));
|
||||
_unitOfWork.ReadingListRepository.BulkRemove(listItems);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok("Nothing to remove");
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok("Updated");
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return BadRequest("Could not remove read items");
|
||||
}
|
||||
@ -204,20 +160,13 @@ namespace API.Controllers
|
||||
[HttpDelete]
|
||||
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(readingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
|
||||
|
||||
user.ReadingLists.Remove(readingList);
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
return Ok("Deleted");
|
||||
}
|
||||
if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was deleted");
|
||||
|
||||
return BadRequest("There was an issue deleting reading list");
|
||||
}
|
||||
@ -230,7 +179,8 @@ namespace API.Controllers
|
||||
[HttpPost("create")]
|
||||
public async Task<ActionResult<ReadingListDto>> CreateList(CreateReadingListDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserWithReadingListsByUsernameAsync(User.GetUsername());
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.ReadingListsWithItems);
|
||||
|
||||
// When creating, we need to make sure Title is unique
|
||||
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
|
||||
@ -260,7 +210,7 @@ namespace API.Controllers
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
if (readingList == null) return BadRequest("List does not exist");
|
||||
|
||||
var user = await UserHasReadingListAccess(readingList.Id);
|
||||
var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
@ -308,7 +258,7 @@ namespace API.Controllers
|
||||
[HttpPost("update-by-series")]
|
||||
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(dto.ReadingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
@ -320,7 +270,7 @@ namespace API.Controllers
|
||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
|
||||
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
@ -350,7 +300,7 @@ namespace API.Controllers
|
||||
[HttpPost("update-by-multiple")]
|
||||
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(dto.ReadingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
@ -365,7 +315,7 @@ namespace API.Controllers
|
||||
}
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList))
|
||||
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
@ -394,7 +344,7 @@ namespace API.Controllers
|
||||
[HttpPost("update-by-multiple-series")]
|
||||
public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(dto.ReadingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
@ -407,7 +357,7 @@ namespace API.Controllers
|
||||
foreach (var seriesId in ids.Keys)
|
||||
{
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(seriesId, ids[seriesId], readingList))
|
||||
if (await _readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
@ -432,7 +382,7 @@ namespace API.Controllers
|
||||
[HttpPost("update-by-volume")]
|
||||
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(dto.ReadingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
@ -444,7 +394,7 @@ namespace API.Controllers
|
||||
(await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList))
|
||||
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
@ -468,7 +418,7 @@ namespace API.Controllers
|
||||
[HttpPost("update-by-chapter")]
|
||||
public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto dto)
|
||||
{
|
||||
var user = await UserHasReadingListAccess(dto.ReadingListId);
|
||||
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
@ -477,7 +427,7 @@ namespace API.Controllers
|
||||
if (readingList == null) return BadRequest("Reading List does not exist");
|
||||
|
||||
// If there are adds, tell tracking this has been modified
|
||||
if (await AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
|
||||
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
|
||||
{
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
}
|
||||
@ -498,39 +448,7 @@ namespace API.Controllers
|
||||
return Ok("Nothing to do");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a list of Chapters as reading list items to the passed reading list.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="chapterIds"></param>
|
||||
/// <param name="readingList"></param>
|
||||
/// <returns>True if new chapters were added</returns>
|
||||
private async Task<bool> AddChaptersToReadingList(int seriesId, IList<int> chapterIds,
|
||||
ReadingList readingList)
|
||||
{
|
||||
// TODO: Move to ReadingListService and Unit Test
|
||||
readingList.Items ??= new List<ReadingListItem>();
|
||||
var lastOrder = 0;
|
||||
if (readingList.Items.Any())
|
||||
{
|
||||
lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli.Order);
|
||||
}
|
||||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
||||
.OrderBy(c => Parser.Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||
|
||||
var index = lastOrder + 1;
|
||||
foreach (var chapter in chaptersForSeries)
|
||||
{
|
||||
if (existingChapterExists.Contains(chapter.Id)) continue;
|
||||
readingList.Items.Add(DbFactory.ReadingListItem(index, seriesId, chapter.VolumeId, chapter.Id));
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index > lastOrder + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next chapter within the reading list
|
||||
|
@ -1,10 +1,18 @@
|
||||
namespace API.DTOs.ReadingLists
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs.ReadingLists
|
||||
{
|
||||
/// <summary>
|
||||
/// DTO for moving a reading list item to another position within the same list
|
||||
/// </summary>
|
||||
public class UpdateReadingListPosition
|
||||
{
|
||||
[Required]
|
||||
public int ReadingListId { get; set; }
|
||||
[Required]
|
||||
public int ReadingListItemId { get; set; }
|
||||
public int FromPosition { get; set; }
|
||||
[Required]
|
||||
public int ToPosition { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -13,16 +13,15 @@ public static class MigrateRemoveExtraThemes
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, IThemeService themeService)
|
||||
{
|
||||
Console.WriteLine("Removing Dark and E-Ink themes");
|
||||
|
||||
var themes = (await unitOfWork.SiteThemeRepository.GetThemes()).ToList();
|
||||
|
||||
if (themes.FirstOrDefault(t => t.Name.Equals("Light")) == null)
|
||||
{
|
||||
Console.WriteLine("Done. Nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Removing Dark and E-Ink themes");
|
||||
|
||||
var darkTheme = themes.Single(t => t.Name.Equals("Dark"));
|
||||
var lightTheme = themes.Single(t => t.Name.Equals("Light"));
|
||||
var eInkTheme = themes.Single(t => t.Name.Equals("E-Ink"));
|
||||
|
@ -121,7 +121,7 @@ public interface ISeriesRepository
|
||||
Task<int> GetSeriesIdByFolder(string folder);
|
||||
Task<Series> GetSeriesByFolderPath(string folder);
|
||||
Task<Series> GetFullSeriesByName(string series, int libraryId);
|
||||
Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format);
|
||||
Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true);
|
||||
Task RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId);
|
||||
Task<IDictionary<string, IList<SeriesModified>>> GetFolderPathMap(int libraryId);
|
||||
}
|
||||
@ -1217,8 +1217,9 @@ public class SeriesRepository : ISeriesRepository
|
||||
/// <param name="seriesName"></param>
|
||||
/// <param name="localizedName"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="withFullIncludes">Defaults to true. This will query against all foreign keys (deep). If false, just the series will come back</param>
|
||||
/// <returns></returns>
|
||||
public Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format)
|
||||
public Task<Series> GetFullSeriesByAnyName(string seriesName, string localizedName, int libraryId, MangaFormat format, bool withFullIncludes = true)
|
||||
{
|
||||
var normalizedSeries = Parser.Parser.Normalize(seriesName);
|
||||
var normalizedLocalized = Parser.Parser.Normalize(localizedName);
|
||||
@ -1233,6 +1234,11 @@ public class SeriesRepository : ISeriesRepository
|
||||
s.NormalizedName.Equals(normalizedLocalized) || s.NormalizedLocalizedName.Equals(normalizedLocalized));
|
||||
}
|
||||
|
||||
if (!withFullIncludes)
|
||||
{
|
||||
return query.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
return query.Include(s => s.Metadata)
|
||||
.ThenInclude(m => m.People)
|
||||
.Include(s => s.Metadata)
|
||||
@ -1261,15 +1267,28 @@ public class SeriesRepository : ISeriesRepository
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes series that are not in the seenSeries list. Does not commit.
|
||||
/// </summary>
|
||||
/// <param name="seenSeries"></param>
|
||||
/// <param name="libraryId"></param>
|
||||
public async Task RemoveSeriesNotInList(IList<ParsedSeries> seenSeries, int libraryId)
|
||||
{
|
||||
if (seenSeries.Count == 0) return;
|
||||
var ids = new List<int>();
|
||||
foreach (var parsedSeries in seenSeries)
|
||||
{
|
||||
ids.Add(await _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName && s.LibraryId == libraryId)
|
||||
.Select(s => s.Id).SingleAsync());
|
||||
var series = await _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||
s.LibraryId == libraryId)
|
||||
.Select(s => s.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
if (series > 0)
|
||||
{
|
||||
ids.Add(series);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var seriesToRemove = await _context.Series
|
||||
|
@ -23,7 +23,9 @@ public enum AppUserIncludes
|
||||
ReadingLists = 8,
|
||||
Ratings = 16,
|
||||
UserPreferences = 32,
|
||||
WantToRead = 64
|
||||
WantToRead = 64,
|
||||
ReadingListsWithItems = 128,
|
||||
|
||||
}
|
||||
|
||||
public interface IUserRepository
|
||||
@ -36,7 +38,6 @@ public interface IUserRepository
|
||||
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync();
|
||||
Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync();
|
||||
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
|
||||
Task<IEnumerable<AppUser>> GetNonAdminUsersAsync();
|
||||
Task<bool> IsUserAdminAsync(AppUser user);
|
||||
Task<AppUserRating> GetUserRatingAsync(int seriesId, int userId);
|
||||
Task<AppUserPreferences> GetPreferencesAsync(string username);
|
||||
@ -51,11 +52,9 @@ public interface IUserRepository
|
||||
Task<AppUser> GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None);
|
||||
Task<AppUser> GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None);
|
||||
Task<int> GetUserIdByUsernameAsync(string username);
|
||||
Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username);
|
||||
Task<IList<AppUserBookmark>> GetAllBookmarksByIds(IList<int> bookmarkIds);
|
||||
Task<AppUser> GetUserByEmailAsync(string email);
|
||||
Task<IEnumerable<AppUser>> GetAllUsers();
|
||||
|
||||
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
||||
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
||||
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags);
|
||||
@ -167,6 +166,11 @@ public class UserRepository : IUserRepository
|
||||
query = query.Include(u => u.ReadingLists);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.ReadingListsWithItems))
|
||||
{
|
||||
query = query.Include(u => u.ReadingLists).ThenInclude(r => r.Items);
|
||||
}
|
||||
|
||||
if (includeFlags.HasFlag(AppUserIncludes.Ratings))
|
||||
{
|
||||
query = query.Include(u => u.Ratings);
|
||||
@ -201,19 +205,6 @@ public class UserRepository : IUserRepository
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an AppUser by username. Returns back Reading List and their Items.
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username)
|
||||
{
|
||||
return await _context.Users
|
||||
.Include(u => u.ReadingLists)
|
||||
.ThenInclude(l => l.Items)
|
||||
.AsSplitQuery()
|
||||
.SingleOrDefaultAsync(x => x.UserName == username);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Bookmarks for a given set of Ids
|
||||
@ -267,11 +258,6 @@ public class UserRepository : IUserRepository
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetNonAdminUsersAsync()
|
||||
{
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.PlebRole);
|
||||
}
|
||||
|
||||
public async Task<bool> IsUserAdminAsync(AppUser user)
|
||||
{
|
||||
return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole);
|
||||
@ -404,14 +390,4 @@ public class UserRepository : IUserRepository
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateUserExists(string username)
|
||||
{
|
||||
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == username.ToUpper()))
|
||||
{
|
||||
throw new ValidationException("Username is taken.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ namespace API.Extensions
|
||||
services.AddScoped<IThemeService, ThemeService>();
|
||||
services.AddScoped<ISeriesService, SeriesService>();
|
||||
services.AddScoped<IProcessSeries, ProcessSeries>();
|
||||
services.AddScoped<IReadingListService, ReadingListService>();
|
||||
|
||||
services.AddScoped<IScannerService, ScannerService>();
|
||||
services.AddScoped<IMetadataService, MetadataService>();
|
||||
|
182
API/Services/ReadingListService.cs
Normal file
182
API/Services/ReadingListService.cs
Normal file
@ -0,0 +1,182 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
public interface IReadingListService
|
||||
{
|
||||
Task<bool> RemoveFullyReadItems(int readingListId, AppUser user);
|
||||
Task<bool> UpdateReadingListItemPosition(UpdateReadingListPosition dto);
|
||||
Task<bool> DeleteReadingListItem(UpdateReadingListPosition dto);
|
||||
Task<AppUser?> UserHasReadingListAccess(int readingListId, string username);
|
||||
Task<bool> DeleteReadingList(int readingListId, AppUser user);
|
||||
|
||||
Task<bool> AddChaptersToReadingList(int seriesId, IList<int> chapterIds,
|
||||
ReadingList readingList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Methods responsible for management of Reading Lists
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess"/> to be called beforehand</remarks>
|
||||
public class ReadingListService : IReadingListService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ReadingListService> _logger;
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
|
||||
public ReadingListService(IUnitOfWork unitOfWork, ILogger<ReadingListService> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes all entries that are fully read from the reading list
|
||||
/// </summary>
|
||||
/// <remarks>If called from API layer, expected for <see cref="UserHasReadingListAccess"/> to be called beforehand</remarks>
|
||||
/// <param name="readingListId">Reading List Id</param>
|
||||
/// <param name="user">User</param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> RemoveFullyReadItems(int readingListId, AppUser user)
|
||||
{
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, user.Id);
|
||||
items = await _unitOfWork.ReadingListRepository.AddReadingProgressModifiers(user.Id, items.ToList());
|
||||
|
||||
// Collect all Ids to remove
|
||||
var itemIdsToRemove = items.Where(item => item.PagesRead == item.PagesTotal).Select(item => item.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var listItems =
|
||||
(await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).Where(r =>
|
||||
itemIdsToRemove.Contains(r.Id));
|
||||
_unitOfWork.ReadingListRepository.BulkRemove(listItems);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a reading list item from one position to another. This will cause items at that position to be pushed one index.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> UpdateReadingListItemPosition(UpdateReadingListPosition dto)
|
||||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
||||
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
||||
items.Remove(item);
|
||||
items.Insert(dto.ToPosition, item);
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
items[i].Order = i;
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
||||
return await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteReadingListItem(UpdateReadingListPosition dto)
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).ToList();
|
||||
|
||||
var index = 0;
|
||||
foreach (var readingListItem in readingList.Items)
|
||||
{
|
||||
readingListItem.Order = index;
|
||||
index++;
|
||||
}
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
||||
return await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the user has access to the reading list to perform actions on it
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <param name="username"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<AppUser?> UserHasReadingListAccess(int readingListId, string username)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username,
|
||||
AppUserIncludes.ReadingListsWithItems);
|
||||
if (user.ReadingLists.SingleOrDefault(rl => rl.Id == readingListId) == null && !await _unitOfWork.UserRepository.IsUserAdminAsync(user))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the Reading List from kavita
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <param name="user">User should have ReadingLists populated</param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> DeleteReadingList(int readingListId, AppUser user)
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
|
||||
user.ReadingLists.Remove(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
||||
return await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a list of Chapters as reading list items to the passed reading list.
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <param name="chapterIds"></param>
|
||||
/// <param name="readingList"></param>
|
||||
/// <returns>True if new chapters were added</returns>
|
||||
public async Task<bool> AddChaptersToReadingList(int seriesId, IList<int> chapterIds, ReadingList readingList)
|
||||
{
|
||||
readingList.Items ??= new List<ReadingListItem>();
|
||||
var lastOrder = 0;
|
||||
if (readingList.Items.Any())
|
||||
{
|
||||
lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli.Order);
|
||||
}
|
||||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
var chaptersForSeries = (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds))
|
||||
.OrderBy(c => Parser.Parser.MinNumberFromRange(c.Volume.Name))
|
||||
.ThenBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||
|
||||
var index = lastOrder + 1;
|
||||
foreach (var chapter in chaptersForSeries)
|
||||
{
|
||||
if (existingChapterExists.Contains(chapter.Id)) continue;
|
||||
readingList.Items.Add(DbFactory.ReadingListItem(index, seriesId, chapter.VolumeId, chapter.Id));
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return index > lastOrder + 1;
|
||||
}
|
||||
}
|
@ -78,7 +78,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
_logger = logger;
|
||||
_scannerService = scannerService;
|
||||
|
||||
_queueWaitTime = environment.IsDevelopment() ? TimeSpan.FromSeconds(10) : TimeSpan.FromSeconds(30);
|
||||
_queueWaitTime = environment.IsDevelopment() ? TimeSpan.FromSeconds(10) : TimeSpan.FromMinutes(1);
|
||||
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
.ToList();
|
||||
foreach (var libraryFolder in _libraryFolders)
|
||||
{
|
||||
_logger.LogInformation("Watching {FolderPath}", libraryFolder);
|
||||
_logger.LogDebug("Watching {FolderPath}", libraryFolder);
|
||||
var watcher = new FileSystemWatcher(libraryFolder);
|
||||
watcher.NotifyFilter = NotifyFilters.CreationTime
|
||||
| NotifyFilters.DirectoryName
|
||||
@ -178,7 +178,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
private void ProcessChange(string filePath, bool isDirectoryChange = false)
|
||||
{
|
||||
// We need to check if directory or not
|
||||
if (!isDirectoryChange && !new Regex(Parser.Parser.SupportedExtensions).IsMatch(new FileInfo(filePath).Extension)) return;
|
||||
if (!isDirectoryChange && !new Regex(Parser.Parser.SupportedExtensions).IsMatch(new FileInfo(filePath).Extension)) return;
|
||||
|
||||
var parentDirectory = _directoryService.GetParentDirectoryName(filePath);
|
||||
if (string.IsNullOrEmpty(parentDirectory)) return;
|
||||
@ -231,7 +231,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
|
||||
if (_scanQueue.Count > 0)
|
||||
{
|
||||
Task.Delay(_queueWaitTime).ContinueWith(t=> ProcessQueue());
|
||||
Task.Delay(TimeSpan.FromSeconds(30)).ContinueWith(t=> ProcessQueue());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -109,9 +109,11 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
async deleteUser(member: Member) {
|
||||
if (await this.confirmService.confirm('Are you sure you want to delete this user?')) {
|
||||
this.memberService.deleteMember(member.username).subscribe(() => {
|
||||
this.loadMembers();
|
||||
this.loadPendingInvites();
|
||||
this.toastr.success(member.username + ' has been deleted.');
|
||||
setTimeout(() => {
|
||||
this.loadMembers();
|
||||
this.loadPendingInvites();
|
||||
this.toastr.success(member.username + ' has been deleted.');
|
||||
}, 30); // SetTimeout because I've noticed this can run super fast and not give enough time for data to flush
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
[items]="collections"
|
||||
[filterOpen]="filterOpen"
|
||||
[jumpBarKeys]="jumpbarKeys"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions" [imageUrl]="imageSerivce.getCollectionCoverImage(item.id)" (clicked)="loadCollection(item)"></app-card-item>
|
||||
|
@ -24,6 +24,7 @@ export class AllCollectionsComponent implements OnInit {
|
||||
collections: CollectionTag[] = [];
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
trackByIdentity = (index: number, item: CollectionTag) => `${item.id}_${item.title}`;
|
||||
|
||||
filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
|
@ -11,42 +11,44 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-template #extrasDrawer let-offcanvas>
|
||||
<div class="offcanvas-header">
|
||||
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<form [formGroup]="pageExtrasGroup">
|
||||
<!-- <div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-reading-direction" class="form-label">Sort Order</label>
|
||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
|
||||
<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>
|
||||
</ng-template>
|
||||
</button>
|
||||
|
||||
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
|
||||
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
||||
<label id="list-layout-mode-label" class="form-label">Layout Mode</label>
|
||||
<br/>
|
||||
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
|
||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
|
||||
|
||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
|
||||
|
||||
<div style="margin-top: 56px">
|
||||
<div class="offcanvas-header">
|
||||
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<form [formGroup]="pageExtrasGroup">
|
||||
<!-- <div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-reading-direction" class="form-label">Sort Order</label>
|
||||
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
|
||||
<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>
|
||||
</ng-template>
|
||||
</button>
|
||||
|
||||
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
|
||||
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="row g-0">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-3">
|
||||
<label id="list-layout-mode-label" class="form-label">Layout Mode</label>
|
||||
<br/>
|
||||
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
|
||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
|
||||
|
||||
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2020",
|
||||
"target": "ES6",
|
||||
"module": "es2020",
|
||||
"lib": [
|
||||
"es2019",
|
||||
|
Loading…
x
Reference in New Issue
Block a user