Bulk Deletion (#697)

* Implemented bulk deletion of series

* Don't show unauthorized exception on UI, just redirect to the login page.
This commit is contained in:
Joseph Milazzo 2021-10-20 10:49:58 -07:00 committed by GitHub
parent e3b33bcbf9
commit 6d6eee999a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 107 additions and 7 deletions

View File

@ -78,8 +78,9 @@ namespace API.Controllers
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
{
var username = User.GetUsername();
var chapterIds = (await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{seriesId}));
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
var chapterIds = (await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{seriesId}));
var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
if (result)
@ -92,6 +93,34 @@ namespace API.Controllers
return Ok(result);
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("delete-multiple")]
public async Task<ActionResult> DeleteMultipleSeries(DeleteSeriesDto dto)
{
var username = User.GetUsername();
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", dto.SeriesIds, username);
var chapterMappings =
await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
var allChapterIds = new List<int>();
foreach (var mapping in chapterMappings)
{
allChapterIds.AddRange(mapping.Value);
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(dto.SeriesIds);
_unitOfWork.SeriesRepository.Remove(series);
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
_taskScheduler.CleanupChapters(allChapterIds.ToArray());
}
return Ok();
}
/// <summary>
/// Returns All volumes for a series with progress information and Chapters
/// </summary>

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace API.DTOs
{
public class DeleteSeriesDto
{
public IList<int> SeriesIds { get; set; }
}
}

View File

@ -42,6 +42,11 @@ namespace API.Data.Repositories
_context.Series.Remove(series);
}
public void Remove(IEnumerable<Series> series)
{
_context.Series.RemoveRange(series);
}
public async Task<bool> DoesSeriesNameExistInLibrary(string name)
{
var libraries = _context.Series
@ -172,6 +177,21 @@ namespace API.Data.Repositories
.SingleOrDefaultAsync();
}
/// <summary>
/// Returns Volumes, Metadata, and Collection Tags
/// </summary>
/// <param name="seriesIds"></param>
/// <returns></returns>
public async Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds)
{
return await _context.Series
.Include(s => s.Volumes)
.Include(s => s.Metadata)
.ThenInclude(m => m.CollectionTags)
.Where(s => seriesIds.Contains(s.Id))
.ToListAsync();
}
public async Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds)
{
var volumes = await _context.Volume

View File

@ -13,6 +13,7 @@ namespace API.Interfaces.Repositories
void Attach(Series series);
void Update(Series series);
void Remove(Series series);
void Remove(IEnumerable<Series> series);
Task<bool> DoesSeriesNameExistInLibrary(string name);
/// <summary>
/// Adds user information like progress, ratings, etc
@ -33,6 +34,7 @@ namespace API.Interfaces.Repositories
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
Task<bool> DeleteSeriesAsync(int seriesId);
Task<Series> GetSeriesByIdAsync(int seriesId);
Task<IList<Series>> GetSeriesByIdsAsync(IList<int> seriesIds);
Task<int[]> GetChapterIdsForSeriesAsync(int[] seriesIds);
Task<IDictionary<int, IList<int>>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds);
/// <summary>

View File

@ -111,11 +111,7 @@ export class ErrorInterceptor implements HttpInterceptor {
// NOTE: Signin has error.error or error.statusText available.
// if statement is due to http/2 spec issue: https://github.com/angular/angular/issues/23334
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.toastr.error(error.statusText === 'OK' ? 'Unauthorized' : error.statusText, error.status);
}
this.accountService.logout();
});
}
}

View File

@ -467,4 +467,21 @@ export class ActionService implements OnDestroy {
});
}
/**
* Mark all chapters and the volumes as Read. All volumes and chapters must belong to a series
* @param seriesId Series Id
* @param volumes Volumes, should have id, chapters and pagesRead populated
* @param chapters? Chapters, should have id
* @param callback Optional callback to perform actions after API completes
*/
deleteMultipleSeries(seriesIds: Array<Series>, callback?: VoidActionCallback) {
this.seriesService.deleteMultipleSeries(seriesIds.map(s => s.id)).pipe(take(1)).subscribe(() => {
this.toastr.success('Series deleted');
if (callback) {
callback();
}
});
}
}

View File

@ -80,6 +80,10 @@ export class SeriesService {
return this.httpClient.delete<boolean>(this.baseUrl + 'series/' + seriesId);
}
deleteMultipleSeries(seriesIds: Array<number>) {
return this.httpClient.post<boolean>(this.baseUrl + 'series/delete-multiple', {seriesIds});
}
updateRating(seriesId: number, userRating: number, userReview: string) {
return this.httpClient.post(this.baseUrl + 'series/update-rating', {seriesId, userRating, userReview});
}

View File

@ -127,7 +127,7 @@ export class BulkSelectionService {
getActions(callback: (action: Action, data: any) => void) {
// checks if series is present. If so, returns only series actions
// else returns volume/chapter items
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection];
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection, Action.Delete];
if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) {
return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action));
}

View File

@ -72,6 +72,12 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
this.bulkSelectionService.deselectAll();
});
break;
case Action.Delete:
this.actionService.deleteMultipleSeries(selectedSeries, () => {
this.loadPage();
this.bulkSelectionService.deselectAll();
});
break;
}
}

View File

@ -112,7 +112,6 @@ export class InProgressComponent implements OnInit {
this.loadPage();
this.bulkSelectionService.deselectAll();
});
break;
case Action.MarkAsUnread:
this.actionService.markMultipleSeriesAsUnread(selectedSeries, () => {
@ -120,6 +119,12 @@ export class InProgressComponent implements OnInit {
this.bulkSelectionService.deselectAll();
});
break;
case Action.Delete:
this.actionService.deleteMultipleSeries(selectedSeries, () => {
this.loadPage();
this.bulkSelectionService.deselectAll();
});
break;
}
}

View File

@ -64,6 +64,12 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
this.bulkSelectionService.deselectAll();
});
break;
case Action.Delete:
this.actionService.deleteMultipleSeries(selectedSeries, () => {
this.loadPage();
this.bulkSelectionService.deselectAll();
});
break;
}
}

View File

@ -138,6 +138,12 @@ export class RecentlyAddedComponent implements OnInit, OnDestroy {
this.bulkSelectionService.deselectAll();
});
break;
case Action.Delete:
this.actionService.deleteMultipleSeries(selectedSeries, () => {
this.loadPage();
this.bulkSelectionService.deselectAll();
});
break;
}
}
}