diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 08a13366f..e2072955b 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -78,8 +78,9 @@ namespace API.Controllers public async Task> 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 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(); + 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(); + } + /// /// Returns All volumes for a series with progress information and Chapters /// diff --git a/API/DTOs/DeleteSeriesDto.cs b/API/DTOs/DeleteSeriesDto.cs new file mode 100644 index 000000000..6908c21ac --- /dev/null +++ b/API/DTOs/DeleteSeriesDto.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace API.DTOs +{ + public class DeleteSeriesDto + { + public IList SeriesIds { get; set; } + } +} diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index d6f032dae..bed73e2f5 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -42,6 +42,11 @@ namespace API.Data.Repositories _context.Series.Remove(series); } + public void Remove(IEnumerable series) + { + _context.Series.RemoveRange(series); + } + public async Task DoesSeriesNameExistInLibrary(string name) { var libraries = _context.Series @@ -172,6 +177,21 @@ namespace API.Data.Repositories .SingleOrDefaultAsync(); } + /// + /// Returns Volumes, Metadata, and Collection Tags + /// + /// + /// + public async Task> GetSeriesByIdsAsync(IList 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 GetChapterIdsForSeriesAsync(int[] seriesIds) { var volumes = await _context.Volume diff --git a/API/Interfaces/Repositories/ISeriesRepository.cs b/API/Interfaces/Repositories/ISeriesRepository.cs index b080a904f..2129c894b 100644 --- a/API/Interfaces/Repositories/ISeriesRepository.cs +++ b/API/Interfaces/Repositories/ISeriesRepository.cs @@ -13,6 +13,7 @@ namespace API.Interfaces.Repositories void Attach(Series series); void Update(Series series); void Remove(Series series); + void Remove(IEnumerable series); Task DoesSeriesNameExistInLibrary(string name); /// /// Adds user information like progress, ratings, etc @@ -33,6 +34,7 @@ namespace API.Interfaces.Repositories Task GetSeriesDtoByIdAsync(int seriesId, int userId); Task DeleteSeriesAsync(int seriesId); Task GetSeriesByIdAsync(int seriesId); + Task> GetSeriesByIdsAsync(IList seriesIds); Task GetChapterIdsForSeriesAsync(int[] seriesIds); Task>> GetChapterIdWithSeriesIdForSeriesAsync(int[] seriesIds); /// diff --git a/UI/Web/src/app/_interceptors/error.interceptor.ts b/UI/Web/src/app/_interceptors/error.interceptor.ts index 20c48fd8a..575ed21d6 100644 --- a/UI/Web/src/app/_interceptors/error.interceptor.ts +++ b/UI/Web/src/app/_interceptors/error.interceptor.ts @@ -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(); }); - } } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index ad79a5b32..5ada05dc2 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -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, callback?: VoidActionCallback) { + this.seriesService.deleteMultipleSeries(seriesIds.map(s => s.id)).pipe(take(1)).subscribe(() => { + this.toastr.success('Series deleted'); + + if (callback) { + callback(); + } + }); + } + } diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index 1975fe49b..6d3693558 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -80,6 +80,10 @@ export class SeriesService { return this.httpClient.delete(this.baseUrl + 'series/' + seriesId); } + deleteMultipleSeries(seriesIds: Array) { + return this.httpClient.post(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}); } diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts index 1cc205725..a7f6eba70 100644 --- a/UI/Web/src/app/cards/bulk-selection.service.ts +++ b/UI/Web/src/app/cards/bulk-selection.service.ts @@ -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)); } diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts index 34ca7b659..2593aaba8 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -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; } } diff --git a/UI/Web/src/app/in-progress/in-progress.component.ts b/UI/Web/src/app/in-progress/in-progress.component.ts index a247ff4ab..88b06db0e 100644 --- a/UI/Web/src/app/in-progress/in-progress.component.ts +++ b/UI/Web/src/app/in-progress/in-progress.component.ts @@ -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; } } diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index 678a52d20..810f20131 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -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; } } diff --git a/UI/Web/src/app/recently-added/recently-added.component.ts b/UI/Web/src/app/recently-added/recently-added.component.ts index 22ea98b59..97640118d 100644 --- a/UI/Web/src/app/recently-added/recently-added.component.ts +++ b/UI/Web/src/app/recently-added/recently-added.component.ts @@ -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; } } }