diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index cc7d0d857..ee5bbafcf 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -459,7 +459,7 @@ public class AccountController : BaseApiController if (adminUser == null) return Unauthorized(); if (!await _unitOfWork.UserRepository.IsUserAdminAsync(adminUser)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId); + var user = await _unitOfWork.UserRepository.GetUserByIdAsync(dto.UserId, AppUserIncludes.SideNavStreams); if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "no-user")); // Check if username is changing @@ -509,6 +509,7 @@ public class AccountController : BaseApiController { lib.AppUsers ??= new List(); lib.AppUsers.Remove(user); + user.RemoveSideNavFromLibrary(lib); } libraries = (await _unitOfWork.LibraryRepository.GetLibraryForIdsAsync(dto.Libraries, LibraryIncludes.AppUser)).ToList(); @@ -518,6 +519,7 @@ public class AccountController : BaseApiController { lib.AppUsers ??= new List(); lib.AppUsers.Add(user); + user.CreateSideNavFromLibrary(lib); } user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; @@ -528,6 +530,9 @@ public class AccountController : BaseApiController if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync()) { await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id); + await _eventHub.SendMessageToAsync(MessageFactory.SideNavUpdate, MessageFactory.SideNavUpdateEvent(user.Id), user.Id); + // If we adjust library access, dashboards should re-render + await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id), user.Id); return Ok(); } @@ -627,8 +632,10 @@ public class AccountController : BaseApiController { lib.AppUsers ??= new List(); lib.AppUsers.Add(user); + user.CreateSideNavFromLibrary(lib); } + _unitOfWork.UserRepository.Update(user); user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction.AgeRating; user.AgeRestrictionIncludeUnknowns = hasAdminRole || dto.AgeRestriction.IncludeUnknowns; @@ -649,6 +656,7 @@ public class AccountController : BaseApiController await _unitOfWork.CommitAsync(); } + try { var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email); diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index fbe280d64..a4f2a98b8 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -109,16 +109,7 @@ public class LibraryController : BaseApiController foreach (var user in userNeedingNewLibrary) { - var maxCount = user.SideNavStreams.Select(s => s.Order).Max(); - user.SideNavStreams.Add(new AppUserSideNavStream() - { - Name = library.Name, - Order = maxCount + 1, - IsProvided = false, - StreamType = SideNavStreamType.Library, - LibraryId = library.Id, - Visible = true, - }); + user.CreateSideNavFromLibrary(library); _unitOfWork.UserRepository.Update(user); } @@ -201,7 +192,7 @@ public class LibraryController : BaseApiController [HttpPost("grant-access")] public async Task> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) { - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username); + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username, AppUserIncludes.SideNavStreams); if (user == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-doesnt-exist")); var libraryString = string.Join(',', updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name)); @@ -217,10 +208,12 @@ public class LibraryController : BaseApiController { // Remove library.AppUsers.Remove(user); + user.RemoveSideNavFromLibrary(library); } else if (!libraryContainsUser && libraryIsSelected) { library.AppUsers.Add(user); + user.CreateSideNavFromLibrary(library); } } @@ -235,6 +228,11 @@ public class LibraryController : BaseApiController _logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username); // Bust cache await _libraryCacheProvider.RemoveByPrefixAsync(CacheKey); + + // TODO: Update a user's SideNav based on library access + + _unitOfWork.UserRepository.Update(user); + return Ok(_mapper.Map(user)); } diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 6ab896688..8d90fa1e1 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -929,6 +929,12 @@ public class SeriesRepository : ISeriesRepository query ??= _context.Series .AsNoTracking(); + // When the user has no access, just return instantly + if (userLibraries.Count == 0) + { + return query.Where(s => false); + } + // First setup any FilterField.Libraries in the statements, as these don't have any traditional query statements applied here diff --git a/API/Extensions/AppUserExtensions.cs b/API/Extensions/AppUserExtensions.cs new file mode 100644 index 000000000..2fbf865d9 --- /dev/null +++ b/API/Extensions/AppUserExtensions.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using API.Entities; +using API.Helpers; + +namespace API.Extensions; + +public static class AppUserExtensions +{ + /// + /// Adds a new SideNavStream to the user's SideNavStreams. This user should have these streams already loaded + /// + /// + /// + public static void CreateSideNavFromLibrary(this AppUser user, Library library) + { + user.SideNavStreams ??= new List(); + var maxCount = user.SideNavStreams.Select(s => s.Order).DefaultIfEmpty().Max(); + + if (user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id) != null) return; + + user.SideNavStreams.Add(new AppUserSideNavStream() + { + Name = library.Name, + Order = maxCount + 1, + IsProvided = false, + StreamType = SideNavStreamType.Library, + LibraryId = library.Id, + Visible = true, + }); + } + + + public static void RemoveSideNavFromLibrary(this AppUser user, Library library) + { + user.SideNavStreams ??= new List(); + + // Find the library and remove it + var item = user.SideNavStreams.FirstOrDefault(s => s.LibraryId == library.Id); + if (item == null) return; + user.SideNavStreams.Remove(item); + + OrderableHelper.ReorderItems(user.SideNavStreams); + + } +} diff --git a/API/Helpers/OrderableHelper.cs b/API/Helpers/OrderableHelper.cs new file mode 100644 index 000000000..f45a7abc0 --- /dev/null +++ b/API/Helpers/OrderableHelper.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using API.Entities; + +namespace API.Helpers; + +public static class OrderableHelper +{ + public static void ReorderItems(List items, int itemId, int toPosition) + { + var item = items.Find(r => r.Id == itemId); + if (item != null) + { + items.Remove(item); + items.Insert(toPosition, item); + } + + for (var i = 0; i < items.Count; i++) + { + items[i].Order = i; + } + } + + public static void ReorderItems(List items, int itemId, int toPosition) + { + var item = items.Find(r => r.Id == itemId); + if (item != null) + { + items.Remove(item); + items.Insert(toPosition, item); + } + + for (var i = 0; i < items.Count; i++) + { + items[i].Order = i; + } + } + + public static void ReorderItems(IList items) + { + for (var i = 0; i < items.Count; i++) + { + items[i].Order = i; + } + } + + public static void ReorderItems(List items, int readingListItemId, int toPosition) + { + var item = items.Find(r => r.Id == readingListItemId); + if (item != null) + { + items.Remove(item); + items.Insert(toPosition, item); + } + + for (var i = 0; i < items.Count; i++) + { + items[i].Order = i; + } + } +} diff --git a/API/Services/ReadingListService.cs b/API/Services/ReadingListService.cs index a9d839069..a48e5a4d7 100644 --- a/API/Services/ReadingListService.cs +++ b/API/Services/ReadingListService.cs @@ -236,28 +236,13 @@ public class ReadingListService : IReadingListService public async Task UpdateReadingListItemPosition(UpdateReadingListPosition dto) { var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList(); - ReorderItems(items, dto.ReadingListItemId, dto.ToPosition); + OrderableHelper.ReorderItems(items, dto.ReadingListItemId, dto.ToPosition); if (!_unitOfWork.HasChanges()) return true; return await _unitOfWork.CommitAsync(); } - private static void ReorderItems(List items, int readingListItemId, int toPosition) - { - var item = items.Find(r => r.Id == readingListItemId); - if (item != null) - { - items.Remove(item); - items.Insert(toPosition, item); - } - - for (var i = 0; i < items.Count; i++) - { - items[i].Order = i; - } - } - /// /// Removes a certain reading list item from a reading list /// @@ -477,7 +462,7 @@ public class ReadingListService : IReadingListService } else { - ReorderItems(items, readingListItem.Id, order); + OrderableHelper.ReorderItems(items, readingListItem.Id, order); } } diff --git a/API/Services/StreamService.cs b/API/Services/StreamService.cs index 54edbbd12..1609ffa89 100644 --- a/API/Services/StreamService.cs +++ b/API/Services/StreamService.cs @@ -7,6 +7,7 @@ using API.DTOs.Dashboard; using API.DTOs.SideNav; using API.Entities; using API.Entities.Enums; +using API.Helpers; using API.SignalR; using Kavita.Common; using Kavita.Common.Helpers; @@ -127,7 +128,7 @@ public class StreamService : IStreamService if (stream.Order == dto.ToPosition) return; var list = user!.DashboardStreams.ToList(); - ReorderItems(list, stream.Id, dto.ToPosition); + OrderableHelper.ReorderItems(list, stream.Id, dto.ToPosition); user.DashboardStreams = list; _unitOfWork.UserRepository.Update(user); @@ -263,7 +264,7 @@ public class StreamService : IStreamService if (stream.Order == dto.ToPosition) return; var list = user!.SideNavStreams.ToList(); - ReorderItems(list, stream.Id, dto.ToPosition); + OrderableHelper.ReorderItems(list, stream.Id, dto.ToPosition); user.SideNavStreams = list; _unitOfWork.UserRepository.Update(user); @@ -340,33 +341,5 @@ public class StreamService : IStreamService await _unitOfWork.CommitAsync(); } - private static void ReorderItems(List items, int itemId, int toPosition) - { - var item = items.Find(r => r.Id == itemId); - if (item != null) - { - items.Remove(item); - items.Insert(toPosition, item); - } - for (var i = 0; i < items.Count; i++) - { - items[i].Order = i; - } - } - - private static void ReorderItems(List items, int itemId, int toPosition) - { - var item = items.Find(r => r.Id == itemId); - if (item != null) - { - items.Remove(item); - items.Insert(toPosition, item); - } - - for (var i = 0; i < items.Count; i++) - { - items[i].Order = i; - } - } } diff --git a/UI/Web/src/app/admin/dashboard/dashboard.component.ts b/UI/Web/src/app/admin/dashboard/dashboard.component.ts index 784ebde27..5dfd2c449 100644 --- a/UI/Web/src/app/admin/dashboard/dashboard.component.ts +++ b/UI/Web/src/app/admin/dashboard/dashboard.component.ts @@ -1,7 +1,5 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {ActivatedRoute, RouterLink} from '@angular/router'; -import {ToastrService} from 'ngx-toastr'; -import {ServerService} from 'src/app/_services/server.service'; import {Title} from '@angular/platform-browser'; import {NavService} from '../../_services/nav.service'; import {SentenceCasePipe} from '../../pipe/sentence-case.pipe'; @@ -20,7 +18,7 @@ import {NgbNav, NgbNavContent, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavOut import { SideNavCompanionBarComponent } from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; -import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; +import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco"; enum TabID { General = '', @@ -66,8 +64,7 @@ export class DashboardComponent implements OnInit { return TabID; } - constructor(public route: ActivatedRoute, private serverService: ServerService, - private toastr: ToastrService, private titleService: Title, public navService: NavService) { + constructor(public route: ActivatedRoute, private titleService: Title, public navService: NavService) { this.route.fragment.subscribe(frag => { const tab = this.tabs.filter(item => item.fragment === frag); if (tab.length > 0) { @@ -81,6 +78,6 @@ export class DashboardComponent implements OnInit { } ngOnInit() { - this.titleService.setTitle('Kavita - ' + this.translocoService.translate('admin-dashboard.title')); + this.titleService.setTitle('Kavita - ' + translate('admin-dashboard.title')); } } diff --git a/openapi.json b/openapi.json index 500c8dfac..f4f6b629e 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.8.11" + "version": "0.7.8.12" }, "servers": [ {