diff --git a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css new file mode 100644 index 000000000..7393b9c35 --- /dev/null +++ b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css @@ -0,0 +1,5 @@ +.message { + composes: alert from '~Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css.d.ts b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css.d.ts new file mode 100644 index 000000000..65c237dff --- /dev/null +++ b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'message': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js index 653e313e1..d53bda8e3 100644 --- a/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js +++ b/frontend/src/AddArtist/ArtistMonitoringOptionsPopoverContent.js @@ -2,14 +2,17 @@ import React from 'react'; import Alert from 'Components/Alert'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import { kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +import styles from './ArtistMonitoringOptionsPopoverContent.css'; function ArtistMonitoringOptionsPopoverContent() { return ( <> - + This is a one time adjustment to set which albums are monitored + { + this.setState({ isMonitorOptionsModalOpen: true }); + }; + + onMonitorOptionsClose = () => { + this.setState({ isMonitorOptionsModalOpen: false }); + }; + onExpandAllPress = () => { const { allExpanded, @@ -224,6 +234,7 @@ class ArtistDetails extends Component { isArtistHistoryModalOpen, isInteractiveImportModalOpen, isInteractiveSearchModalOpen, + isMonitorOptionsModalOpen, allExpanded, allCollapsed, expandedState @@ -319,6 +330,12 @@ class ArtistDetails extends Component { + + + @@ -689,6 +707,12 @@ class ArtistDetails extends Component { artistId={id} onModalClose={this.onInteractiveSearchModalClose} /> + + ); diff --git a/frontend/src/Artist/MonitoringOptions/MonitoringOptionModalConnector.js b/frontend/src/Artist/MonitoringOptions/MonitoringOptionModalConnector.js new file mode 100644 index 000000000..6ea9e8290 --- /dev/null +++ b/frontend/src/Artist/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 './MonitoringOptionsModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class MonitoringOptionsModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'artist' }); + 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/Artist/MonitoringOptions/MonitoringOptionsModal.js b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModal.js new file mode 100644 index 000000000..4071e7f9a --- /dev/null +++ b/frontend/src/Artist/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/Artist/MonitoringOptions/MonitoringOptionsModalContent.css b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css new file mode 100644 index 000000000..3e9e3ffd0 --- /dev/null +++ b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css @@ -0,0 +1,9 @@ +.labelIcon { + margin-left: 8px; +} + +.message { + composes: alert from '~Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css.d.ts b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css.d.ts new file mode 100644 index 000000000..af0f6cd46 --- /dev/null +++ b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.css.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'labelIcon': string; + 'message': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js index 230b22f2a..b1550d39e 100644 --- a/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js +++ b/frontend/src/Artist/MonitoringOptions/MonitoringOptionsModalContent.js @@ -1,18 +1,22 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent'; import Alert from 'Components/Alert'; 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 Icon from 'Components/Icon'; 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, kinds } from 'Helpers/Props'; +import Popover from 'Components/Tooltip/Popover'; +import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +import styles from './MonitoringOptionsModalContent.css'; const NO_CHANGE = 'noChange'; @@ -51,8 +55,7 @@ class MonitoringOptionsModalContent extends Component { onSavePress = () => { const { - onSavePress, - isSaving + onSavePress } = this.props; const { monitor @@ -61,14 +64,6 @@ class MonitoringOptionsModalContent extends Component { if (monitor !== NO_CHANGE) { onSavePress({ monitor }); } - - if (!isSaving) { - this.onModalClose(); - } - }; - - onModalClose = () => { - this.props.onModalClose(); }; // @@ -89,19 +84,31 @@ class MonitoringOptionsModalContent extends Component { return ( - {translate('MonitorAlbum')} + {translate('MonitorArtist')} - -
- {translate('MonitorAlbumExistingOnlyWarning')} -
+ + {translate('MonitorAlbumExistingOnlyWarning')}
- {translate('Monitoring')} + + {translate('MonitorExistingAlbums')} + + + } + title={translate('MonitoringOptions')} + body={} + position={tooltipPositions.RIGHT} + /> + state.artist, + (artistState) => { + const { + isSaving, + saveError + } = artistState; + + return { + isSaving, + saveError + }; + } + ); +} + +const mapDispatchToProps = { + dispatchUpdateMonitoringOptions: updateArtistsMonitor +}; + +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({ + artistIds: [this.props.artistId], + monitor + }); + }; + + // + // Render + + render() { + return ( + + ); + } +} + +MonitoringOptionsModalContentConnector.propTypes = { + artistId: PropTypes.number.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + dispatchUpdateMonitoringOptions: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MonitoringOptionsModalContentConnector); diff --git a/frontend/src/Store/Actions/albumStudioActions.js b/frontend/src/Store/Actions/albumStudioActions.js index 0f543a7d6..39fbe8d20 100644 --- a/frontend/src/Store/Actions/albumStudioActions.js +++ b/frontend/src/Store/Actions/albumStudioActions.js @@ -102,21 +102,21 @@ export const actionHandlers = handleThunks({ [SAVE_ALBUM_STUDIO]: function(getState, payload, dispatch) { const { artistIds, - monitored, monitor, + monitored, monitorNewItems } = payload; - const artist = []; + const artists = []; artistIds.forEach((id) => { - const artistToUpdate = { id }; + const artistsToUpdate = { id }; if (payload.hasOwnProperty('monitored')) { - artistToUpdate.monitored = monitored; + artistsToUpdate.monitored = monitored; } - artist.push(artistToUpdate); + artists.push(artistsToUpdate); }); dispatch(set({ @@ -128,7 +128,7 @@ export const actionHandlers = handleThunks({ url: '/albumStudio', method: 'POST', data: JSON.stringify({ - artist, + artist: artists, monitoringOptions: { monitor }, monitorNewItems }), diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js index 3a1a404d9..97b30972b 100644 --- a/frontend/src/Store/Actions/artistActions.js +++ b/frontend/src/Store/Actions/artistActions.js @@ -2,11 +2,12 @@ import _ from 'lodash'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; +import { fetchAlbums } from 'Store/Actions/albumActions'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; import translate from 'Utilities/String/translate'; -import { updateItem } from './baseActions'; +import { set, updateItem } from './baseActions'; import createFetchHandler from './Creators/createFetchHandler'; import createHandleActions from './Creators/createHandleActions'; import createRemoveItemHandler from './Creators/createRemoveItemHandler'; @@ -177,6 +178,7 @@ export const DELETE_ARTIST = 'artist/deleteArtist'; export const TOGGLE_ARTIST_MONITORED = 'artist/toggleArtistMonitored'; export const TOGGLE_ALBUM_MONITORED = 'artist/toggleAlbumMonitored'; +export const UPDATE_ARTISTS_MONITOR = 'artist/updateArtistsMonitor'; export const SET_DELETE_OPTION = 'artist/setDeleteOption'; @@ -212,6 +214,7 @@ export const deleteArtist = createThunk(DELETE_ARTIST, (payload) => { export const toggleArtistMonitored = createThunk(TOGGLE_ARTIST_MONITORED); export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED); +export const updateArtistsMonitor = createThunk(UPDATE_ARTISTS_MONITOR); export const setArtistValue = createAction(SET_ARTIST_VALUE, (payload) => { return { @@ -342,6 +345,61 @@ export const actionHandlers = handleThunks({ seasons: artist.seasons })); }); + }, + + [UPDATE_ARTISTS_MONITOR]: function(getState, payload, dispatch) { + const { + artistIds, + monitor, + monitored, + monitorNewItems + } = payload; + + const artists = []; + + artistIds.forEach((id) => { + const artistsToUpdate = { id }; + + if (monitored != null) { + artistsToUpdate.monitored = monitored; + } + + artists.push(artistsToUpdate); + }); + + dispatch(set({ + section, + isSaving: true + })); + + const promise = createAjaxRequest({ + url: '/albumStudio', + method: 'POST', + data: JSON.stringify({ + artist: artists, + monitoringOptions: { monitor }, + monitorNewItems + }), + dataType: 'json' + }).request; + + promise.done((data) => { + dispatch(fetchAlbums({ artistId: artistIds[0] })); + + 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 80e2c3184..84f7161e6 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -98,6 +98,7 @@ "ArtistClickToChangeAlbum": "Click to change album", "ArtistEditor": "Artist Editor", "ArtistFolderFormat": "Artist Folder Format", + "ArtistMonitoring": "Artist Monitoring", "ArtistName": "Artist Name", "ArtistNameHelpText": "The name of the artist/album to exclude (can be anything meaningful)", "ArtistProgressBarText": "{trackFileCount} / {trackCount} (Total: {totalTrackCount})",