From bf852cadbe9b2e51b86c2f3694e6d1fc9724eaaa Mon Sep 17 00:00:00 2001 From: ta264 Date: Thu, 18 Nov 2021 21:42:59 +0000 Subject: [PATCH] Fixed: Prevent frontend errors when many books added to Readarr --- .../src/Book/Edit/EditBookModalContent.js | 50 ++++++++++++----- .../Edit/EditBookModalContentConnector.js | 28 +++++++++- frontend/src/Store/Actions/editionActions.js | 54 +++++++++++++++++++ frontend/src/Store/Actions/index.js | 2 + .../Books/Repositories/EditionRepository.cs | 6 +++ .../Books/Services/EditionService.cs | 6 +-- src/Readarr.Api.V1/Books/BookController.cs | 2 +- .../Editions/EditionController.cs | 27 ++++++++++ 8 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 frontend/src/Store/Actions/editionActions.js create mode 100644 src/Readarr.Api.V1/Editions/EditionController.cs diff --git a/frontend/src/Book/Edit/EditBookModalContent.js b/frontend/src/Book/Edit/EditBookModalContent.js index 7d58128a6..d2634461f 100644 --- a/frontend/src/Book/Edit/EditBookModalContent.js +++ b/frontend/src/Book/Edit/EditBookModalContent.js @@ -6,11 +6,13 @@ import FormInputGroup from 'Components/Form/FormInputGroup'; import FormLabel from 'Components/Form/FormLabel'; import Button from 'Components/Link/Button'; import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes } from 'Helpers/Props'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; class EditBookModalContent extends Component { @@ -36,6 +38,9 @@ class EditBookModalContent extends Component { authorName, statistics, item, + isFetching, + isPopulated, + error, isSaving, onInputChange, onModalClose, @@ -49,6 +54,7 @@ class EditBookModalContent extends Component { } = item; const hasFile = statistics ? statistics.bookFileCount : 0; + const errorMessage = getErrorMessage(error, 'Unable to load editions'); return ( @@ -88,20 +94,33 @@ class EditBookModalContent extends Component { /> - - - {translate('Edition')} - - - - + { + isFetching && + + } + + { + error && +
{errorMessage}
+ } + + { + isPopulated && !isFetching && !!editions.value.length && + + + {translate('Edition')} + + + + + } @@ -131,6 +150,9 @@ EditBookModalContent.propTypes = { authorName: PropTypes.string.isRequired, statistics: PropTypes.object.isRequired, item: PropTypes.object.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isPopulated: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired, onInputChange: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired, diff --git a/frontend/src/Book/Edit/EditBookModalContentConnector.js b/frontend/src/Book/Edit/EditBookModalContentConnector.js index ebf61ab2a..f4e9fbf83 100644 --- a/frontend/src/Book/Edit/EditBookModalContentConnector.js +++ b/frontend/src/Book/Edit/EditBookModalContentConnector.js @@ -4,6 +4,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { saveBook, setBookValue } from 'Store/Actions/bookActions'; +import { clearEditions, fetchEditions } from 'Store/Actions/editionActions'; import createAuthorSelector from 'Store/Selectors/createAuthorSelector'; import createBookSelector from 'Store/Selectors/createBookSelector'; import selectSettings from 'Store/Selectors/selectSettings'; @@ -12,15 +13,25 @@ import EditBookModalContent from './EditBookModalContent'; function createMapStateToProps() { return createSelector( (state) => state.books, + (state) => state.editions, createBookSelector(), createAuthorSelector(), - (bookState, book, author) => { + (bookState, editionState, book, author) => { const { isSaving, saveError, pendingChanges } = bookState; + const { + isFetching, + isPopulated, + error, + items + } = editionState; + + book.editions = items; + const bookSettings = _.pick(book, [ 'monitored', 'anyEditionOk', @@ -34,6 +45,9 @@ function createMapStateToProps() { authorName: author.authorName, bookType: book.bookType, statistics: book.statistics, + isFetching, + isPopulated, + error, isSaving, saveError, item: settings.settings, @@ -44,6 +58,8 @@ function createMapStateToProps() { } const mapDispatchToProps = { + dispatchFetchEditions: fetchEditions, + dispatchClearEditions: clearEditions, dispatchSetBookValue: setBookValue, dispatchSaveBook: saveBook }; @@ -53,12 +69,20 @@ class EditBookModalContentConnector extends Component { // // Lifecycle + componentDidMount() { + this.props.dispatchFetchEditions({ bookId: this.props.bookId }); + } + componentDidUpdate(prevProps, prevState) { if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { this.props.onModalClose(); } } + componentWillUnmount() { + this.props.dispatchClearEditions(); + } + // // Listeners @@ -90,6 +114,8 @@ EditBookModalContentConnector.propTypes = { bookId: PropTypes.number, isSaving: PropTypes.bool.isRequired, saveError: PropTypes.object, + dispatchFetchEditions: PropTypes.func.isRequired, + dispatchClearEditions: PropTypes.func.isRequired, dispatchSetBookValue: PropTypes.func.isRequired, dispatchSaveBook: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired diff --git a/frontend/src/Store/Actions/editionActions.js b/frontend/src/Store/Actions/editionActions.js new file mode 100644 index 000000000..f6e0c6f93 --- /dev/null +++ b/frontend/src/Store/Actions/editionActions.js @@ -0,0 +1,54 @@ +import { createAction } from 'redux-actions'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createFetchHandler from './Creators/createFetchHandler'; +import createHandleActions from './Creators/createHandleActions'; +import createClearReducer from './Creators/Reducers/createClearReducer'; + +// +// Variables + +export const section = 'editions'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + itemMap: {} +}; + +// +// Actions Types + +export const FETCH_EDITIONS = 'editions/fetchEditions'; +export const CLEAR_EDITIONS = 'editions/clearEditions'; + +// +// Action Creators + +export const fetchEditions = createThunk(FETCH_EDITIONS); +export const clearEditions = createAction(CLEAR_EDITIONS); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + [FETCH_EDITIONS]: createFetchHandler(section, '/edition') +}); + +// +// Reducers +export const reducers = createHandleActions({ + + [CLEAR_EDITIONS]: createClearReducer(section, { + isFetching: false, + isPopulated: false, + error: null, + items: [], + itemMap: {} + }) + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 705ec4164..7565fede3 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -14,6 +14,7 @@ import * as calendar from './calendarActions'; import * as captcha from './captchaActions'; import * as commands from './commandActions'; import * as customFilters from './customFilterActions'; +import * as editions from './editionActions'; import * as history from './historyActions'; import * as interactiveImportActions from './interactiveImportActions'; import * as oAuth from './oAuthActions'; @@ -47,6 +48,7 @@ export default [ captcha, commands, customFilters, + editions, history, interactiveImportActions, oAuth, diff --git a/src/NzbDrone.Core/Books/Repositories/EditionRepository.cs b/src/NzbDrone.Core/Books/Repositories/EditionRepository.cs index b7d767c64..3a49ead6c 100644 --- a/src/NzbDrone.Core/Books/Repositories/EditionRepository.cs +++ b/src/NzbDrone.Core/Books/Repositories/EditionRepository.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.Books { public interface IEditionRepository : IBasicRepository { + List GetAllMonitoredEditions(); Edition FindByForeignEditionId(string foreignEditionId); List FindByBook(int id); List FindByAuthor(int id); @@ -25,6 +26,11 @@ namespace NzbDrone.Core.Books { } + public List GetAllMonitoredEditions() + { + return Query(x => x.Monitored == true); + } + public Edition FindByForeignEditionId(string foreignEditionId) { var edition = Query(x => x.ForeignEditionId == foreignEditionId).SingleOrDefault(); diff --git a/src/NzbDrone.Core/Books/Services/EditionService.cs b/src/NzbDrone.Core/Books/Services/EditionService.cs index 81fd1563b..421a537ff 100644 --- a/src/NzbDrone.Core/Books/Services/EditionService.cs +++ b/src/NzbDrone.Core/Books/Services/EditionService.cs @@ -12,7 +12,7 @@ namespace NzbDrone.Core.Books { Edition GetEdition(int id); Edition GetEditionByForeignEditionId(string foreignEditionId); - List GetAllEditions(); + List GetAllMonitoredEditions(); void InsertMany(List editions); void UpdateMany(List editions); void DeleteMany(List editions); @@ -48,9 +48,9 @@ namespace NzbDrone.Core.Books return _editionRepository.FindByForeignEditionId(foreignEditionId); } - public List GetAllEditions() + public List GetAllMonitoredEditions() { - return _editionRepository.All().ToList(); + return _editionRepository.GetAllMonitoredEditions(); } public void InsertMany(List editions) diff --git a/src/Readarr.Api.V1/Books/BookController.cs b/src/Readarr.Api.V1/Books/BookController.cs index f8e708f60..ed4dbe9d6 100644 --- a/src/Readarr.Api.V1/Books/BookController.cs +++ b/src/Readarr.Api.V1/Books/BookController.cs @@ -73,7 +73,7 @@ namespace Readarr.Api.V1.Books var books = _bookService.GetAllBooks(); var authors = _authorService.GetAllAuthors().ToDictionary(x => x.AuthorMetadataId); - var editions = _editionService.GetAllEditions().GroupBy(x => x.BookId).ToDictionary(x => x.Key, y => y.ToList()); + var editions = _editionService.GetAllMonitoredEditions().GroupBy(x => x.BookId).ToDictionary(x => x.Key, y => y.ToList()); foreach (var book in books) { diff --git a/src/Readarr.Api.V1/Editions/EditionController.cs b/src/Readarr.Api.V1/Editions/EditionController.cs new file mode 100644 index 000000000..71aedc399 --- /dev/null +++ b/src/Readarr.Api.V1/Editions/EditionController.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Books; +using Readarr.Api.V1.Books; +using Readarr.Http; + +namespace NzbDrone.Api.V1.Editions +{ + [V1ApiController] + public class EditionController : Controller + { + private readonly IEditionService _editionService; + + public EditionController(IEditionService editionService) + { + _editionService = editionService; + } + + [HttpGet] + public List GetEditions(int bookId) + { + var editions = _editionService.GetEditionsByBook(bookId); + + return editions.ToResource(); + } + } +}