diff --git a/frontend/src/Author/Details/AuthorDetails.js b/frontend/src/Author/Details/AuthorDetails.js index 9db384f17..51590b81e 100644 --- a/frontend/src/Author/Details/AuthorDetails.js +++ b/frontend/src/Author/Details/AuthorDetails.js @@ -4,6 +4,7 @@ import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal'; import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector'; import AuthorHistoryTable from 'Author/History/AuthorHistoryTable'; +import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal'; import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; @@ -51,6 +52,7 @@ class AuthorDetails extends Component { isEditAuthorModalOpen: false, isDeleteAuthorModalOpen: false, isInteractiveImportModalOpen: false, + isMonitorOptionsModalOpen: false, allExpanded: false, allCollapsed: false, expandedState: {}, @@ -104,6 +106,14 @@ class AuthorDetails extends Component { this.setState({ isDeleteAuthorModalOpen: false }); } + onMonitorOptionsPress = () => { + this.setState({ isMonitorOptionsModalOpen: true }); + } + + onMonitorOptionsClose = () => { + this.setState({ isMonitorOptionsModalOpen: false }); + } + onExpandAllPress = () => { const { allExpanded, @@ -163,6 +173,7 @@ class AuthorDetails extends Component { isEditAuthorModalOpen, isDeleteAuthorModalOpen, isInteractiveImportModalOpen, + isMonitorOptionsModalOpen, allExpanded, allCollapsed, expandedState, @@ -223,6 +234,12 @@ class AuthorDetails extends Component { + +
- Missing or too many books? Modify or create a new - Metadata Profile + {translate('TooManyBooks')} + {translate('MetadataProfile')} or manually - Search + {translate('Search')} for new items!
@@ -449,6 +466,12 @@ class AuthorDetails extends Component { showImportMode={false} onModalClose={this.onInteractiveImportModalClose} /> + + ); diff --git a/frontend/src/Author/MonitoringOptions/MonitoringOptionModalConnector.js b/frontend/src/Author/MonitoringOptions/MonitoringOptionModalConnector.js new file mode 100644 index 000000000..fd87993e5 --- /dev/null +++ b/frontend/src/Author/MonitoringOptions/MonitoringOptionModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import MonitoringOptionsModal from './EditAuthorModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class MonitoringOptionsModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'authors' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MonitoringOptionsModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(undefined, mapDispatchToProps)(MonitoringOptionsModalConnector); diff --git a/frontend/src/Author/MonitoringOptions/MonitoringOptionsModal.js b/frontend/src/Author/MonitoringOptions/MonitoringOptionsModal.js new file mode 100644 index 000000000..4071e7f9a --- /dev/null +++ b/frontend/src/Author/MonitoringOptions/MonitoringOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import MonitoringOptionsModalContentConnector from './MonitoringOptionsModalContentConnector'; + +function MonitoringOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +MonitoringOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default MonitoringOptionsModal; diff --git a/frontend/src/Author/MonitoringOptions/MonitoringOptionsModalContent.js b/frontend/src/Author/MonitoringOptions/MonitoringOptionsModalContent.js new file mode 100644 index 000000000..248ac8347 --- /dev/null +++ b/frontend/src/Author/MonitoringOptions/MonitoringOptionsModalContent.js @@ -0,0 +1,142 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +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 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 translate from 'Utilities/String/translate'; + +const NO_CHANGE = 'noChange'; + +class MonitoringOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + monitor: NO_CHANGE + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = prevProps; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.setState({ + monitor: NO_CHANGE + }); + } + } + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + } + + // + // Listeners + + onSavePress = () => { + const { + onSavePress, + isSaving + } = this.props; + const { + monitor + } = this.state; + + if (monitor !== NO_CHANGE) { + onSavePress({ monitor }); + } + + if (!isSaving) { + this.onModalClose(); + } + } + + onModalClose = () => { + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + isSaving, + onInputChange, + onModalClose, + ...otherProps + } = this.props; + + const { + monitor + } = this.state; + + return ( + + + {translate('MonitorBook')} + + + +
+ + {translate('Monitoring')} + + + +
+
+ + + + + + {translate('Save')} + + +
+ ); + } +} + +MonitoringOptionsModalContent.propTypes = { + authorId: PropTypes.number.isRequired, + saveError: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +MonitoringOptionsModalContent.defaultProps = { + isSaving: false +}; + +export default MonitoringOptionsModalContent; diff --git a/frontend/src/Author/MonitoringOptions/MonitoringOptionsModalContentConnector.js b/frontend/src/Author/MonitoringOptions/MonitoringOptionsModalContentConnector.js new file mode 100644 index 000000000..2fbac8e39 --- /dev/null +++ b/frontend/src/Author/MonitoringOptions/MonitoringOptionsModalContentConnector.js @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { updateBookMonitor } from 'Store/Actions/authorActions'; +import MonitoringOptionsModalContent from './MonitoringOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.authors, + (authorState) => { + const { + isSaving, + saveError + } = authorState; + + return { + isSaving, + saveError + }; + } + ); +} + +const mapDispatchToProps = { + dispatchUpdateMonitoringOptions: updateBookMonitor +}; + +class MonitoringOptionsModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(true); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ name, value }); + } + + onSavePress = ({ monitor }) => { + this.props.dispatchUpdateMonitoringOptions({ + id: this.props.authorId, + monitor + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MonitoringOptionsModalContentConnector.propTypes = { + authorId: PropTypes.number.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + dispatchUpdateMonitoringOptions: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MonitoringOptionsModalContentConnector); diff --git a/frontend/src/Store/Actions/authorActions.js b/frontend/src/Store/Actions/authorActions.js index 026c71609..76934200c 100644 --- a/frontend/src/Store/Actions/authorActions.js +++ b/frontend/src/Store/Actions/authorActions.js @@ -5,7 +5,8 @@ import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; -import { updateItem } from './baseActions'; +import { set, updateItem } from './baseActions'; +import { fetchBooks } from './bookActions'; import createFetchHandler from './Creators/createFetchHandler'; import createHandleActions from './Creators/createHandleActions'; import createRemoveItemHandler from './Creators/createRemoveItemHandler'; @@ -169,6 +170,7 @@ export const DELETE_AUTHOR = 'authors/deleteAuthor'; export const TOGGLE_AUTHOR_MONITORED = 'authors/toggleAuthorMonitored'; export const TOGGLE_BOOK_MONITORED = 'authors/toggleBookMonitored'; +export const UPDATE_BOOK_MONITORED = 'authors/updateBookMonitored'; // // Action Creators @@ -202,6 +204,7 @@ export const deleteAuthor = createThunk(DELETE_AUTHOR, (payload) => { export const toggleAuthorMonitored = createThunk(TOGGLE_AUTHOR_MONITORED); export const toggleBookMonitored = createThunk(TOGGLE_BOOK_MONITORED); +export const updateBookMonitor = createThunk(UPDATE_BOOK_MONITORED); export const setAuthorValue = createAction(SET_AUTHOR_VALUE, (payload) => { return { @@ -330,8 +333,53 @@ export const actionHandlers = handleThunks({ seasons: author.seasons })); }); - } + }, + + [UPDATE_BOOK_MONITORED]: function(getState, payload, dispatch) { + const { + id, + monitor + } = payload; + + const authorToUpdate = { id }; + + if (monitor !== 'None') { + authorToUpdate.monitored = true; + } + + dispatch(set({ + section, + isSaving: true + })); + + const promise = createAjaxRequest({ + url: '/bookshelf', + method: 'POST', + data: JSON.stringify({ + authors: [{ id }], + monitoringOptions: { monitor } + }), + dataType: 'json' + }).request; + + promise.done((data) => { + dispatch(fetchBooks({ authorId: id })); + + dispatch(set({ + section, + isSaving: false, + saveError: null + })); + }); + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + } }); // diff --git a/frontend/src/Store/Actions/bookActions.js b/frontend/src/Store/Actions/bookActions.js index fcbfb61b8..5d5cfc1d7 100644 --- a/frontend/src/Store/Actions/bookActions.js +++ b/frontend/src/Store/Actions/bookActions.js @@ -6,8 +6,7 @@ import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; -import { removeItem, updateItem } from './baseActions'; -import createFetchHandler from './Creators/createFetchHandler'; +import { removeItem, set, update, updateItem } from './baseActions'; import createHandleActions from './Creators/createHandleActions'; import createRemoveItemHandler from './Creators/createRemoveItemHandler'; import createSaveProviderHandler from './Creators/createSaveProviderHandler'; @@ -259,7 +258,47 @@ export const setBookValue = createAction(SET_BOOK_VALUE, (payload) => { // Action Handlers export const actionHandlers = handleThunks({ - [FETCH_BOOKS]: createFetchHandler(section, '/book'), + [FETCH_BOOKS]: function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); + + const { request, abortRequest } = createAjaxRequest({ + url: '/book', + data: payload, + traditional: true + }); + + request.done((data) => { + // Preserve books for other authors we didn't fetch + if (payload.hasOwnProperty('authorId')) { + const oldBooks = getState().books.items; + const newBooks = oldBooks.filter((x) => x.authorId !== payload.authorId); + data = newBooks.concat(data); + } + + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + request.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr.aborted ? null : xhr + })); + }); + + return abortRequest; + }, + [SAVE_BOOK]: createSaveProviderHandler(section, '/book'), [DELETE_BOOK]: createRemoveItemHandler(section, '/book'), diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index e3e025fb1..0ed482b47 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -73,6 +73,7 @@ "BookIsDownloadingInterp": "Book is downloading - {0}% {1}", "BookIsNotMonitored": "Book is not monitored", "BookMissingFromDisk": "Book missing from disk", + "BookMonitoring": "Book Monitoring", "BookNaming": "Book Naming", "Books": "Books", "BookStudio": "Book Studio", @@ -367,10 +368,12 @@ "MissingBooksAuthorNotMonitored": "Missing Books (Author not monitored)", "Mode": "Mode", "MonitorAuthor": "Monitor Author", + "MonitorBook": "Monitor Book", "Monitored": "Monitored", "MonitoredAuthorIsMonitored": "Author is monitored", "MonitoredAuthorIsUnmonitored": "Author is unmonitored", "MonitoredHelpText": "Readarr will search for and download book", + "Monitoring": "Monitoring", "MonitoringOptions": "Monitoring Options", "MonoVersion": "Mono Version", "MoreInfo": "More Info", @@ -635,6 +638,7 @@ "Titles": "Titles", "Today": "Today", "Tomorrow": "Tomorrow", + "TooManyBooks": "Missing or too many books? Modify or create a new", "TorrentDelay": "Torrent Delay", "TorrentDelayHelpText": "Delay in minutes to wait before grabbing a torrent", "Torrents": "Torrents",