diff --git a/frontend/src/Author/Details/AuthorDetails.js b/frontend/src/Author/Details/AuthorDetails.js index 9db384f17..77fddd0b7 100644 --- a/frontend/src/Author/Details/AuthorDetails.js +++ b/frontend/src/Author/Details/AuthorDetails.js @@ -28,6 +28,7 @@ import AuthorDetailsHeaderConnector from './AuthorDetailsHeaderConnector'; import AuthorDetailsSeasonConnector from './AuthorDetailsSeasonConnector'; import AuthorDetailsSeriesConnector from './AuthorDetailsSeriesConnector'; import styles from './AuthorDetails.css'; +import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal'; function getExpandedState(newState) { return { @@ -50,7 +51,8 @@ class AuthorDetails extends Component { isRetagModalOpen: false, isEditAuthorModalOpen: false, isDeleteAuthorModalOpen: false, - isInteractiveImportModalOpen: 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..0770d2948 --- /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: 'series' }); + 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..c79a1dc5d --- /dev/null +++ b/frontend/src/Author/MonitoringOptions/MonitoringOptionsModalContent.js @@ -0,0 +1,133 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +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 + } = this.props; + const { + monitor + } = this.state; + + if (monitor !== NO_CHANGE) { + onSavePress({ monitor }); + } + } + + // + // 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..cfcaed09b --- /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.series, + (seriesState) => { + const { + isSaving, + saveError + } = seriesState; + + 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..dc8930632 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 { updateItem, set } 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,52 @@ 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/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 8f91bc8ed..53f5baef2 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", @@ -365,10 +366,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", @@ -630,6 +633,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",