diff --git a/frontend/src/App/State/MovieCollectionAppState.ts b/frontend/src/App/State/MovieCollectionAppState.ts index b8bf69ed86..615e4c95d0 100644 --- a/frontend/src/App/State/MovieCollectionAppState.ts +++ b/frontend/src/App/State/MovieCollectionAppState.ts @@ -7,6 +7,8 @@ interface MovieCollectionAppState extends AppSectionState, AppSectionSaveState { itemMap: Record; + + pendingChanges: Partial; } export default MovieCollectionAppState; diff --git a/frontend/src/Collection/Edit/EditCollectionModal.js b/frontend/src/Collection/Edit/EditCollectionModal.js deleted file mode 100644 index 1017aad0ea..0000000000 --- a/frontend/src/Collection/Edit/EditCollectionModal.js +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import EditCollectionModalContentConnector from './EditCollectionModalContentConnector'; - -function EditCollectionModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditCollectionModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditCollectionModal; diff --git a/frontend/src/Collection/Edit/EditCollectionModalConnector.js b/frontend/src/Collection/Edit/EditCollectionModalConnector.js deleted file mode 100644 index c73e7f186c..0000000000 --- a/frontend/src/Collection/Edit/EditCollectionModalConnector.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditCollectionModal from './EditCollectionModal'; - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditCollectionModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'movieCollections' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditCollectionModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector); diff --git a/frontend/src/Collection/Edit/EditCollectionModalContent.js b/frontend/src/Collection/Edit/EditCollectionModalContent.js deleted file mode 100644 index 5063fb28d7..0000000000 --- a/frontend/src/Collection/Edit/EditCollectionModalContent.js +++ /dev/null @@ -1,190 +0,0 @@ -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 MoviePoster from 'Movie/MoviePoster'; -import translate from 'Utilities/String/translate'; -import styles from './EditCollectionModalContent.css'; - -class EditCollectionModalContent extends Component { - - // - // Listeners - - onSavePress = () => { - const { - onSavePress - } = this.props; - - onSavePress(false); - }; - - // - // Render - - render() { - const { - title, - images, - overview, - item, - isSaving, - onInputChange, - onModalClose, - isSmallScreen, - ...otherProps - } = this.props; - - const { - monitored, - qualityProfileId, - minimumAvailability, - // Id, - rootFolderPath, - tags, - searchOnAdd - } = item; - - return ( - - - {translate('Edit')} - {title} - - - -
- { - !isSmallScreen && -
- -
- } - -
-
- {overview} -
- -
- - {translate('Monitored')} - - - - - - {translate('MinimumAvailability')} - - - - - - {translate('QualityProfile')} - - - - - - {translate('RootFolder')} - - - - - - {translate('Tags')} - - - - - - {translate('SearchOnAdd')} - - - -
-
-
-
- - - - - - {translate('Save')} - - -
- ); - } -} - -EditCollectionModalContent.propTypes = { - collectionId: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - overview: PropTypes.string.isRequired, - images: PropTypes.arrayOf(PropTypes.object).isRequired, - item: PropTypes.object.isRequired, - isSaving: PropTypes.bool.isRequired, - isPathChanging: PropTypes.bool.isRequired, - isSmallScreen: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditCollectionModalContent; diff --git a/frontend/src/Collection/Edit/EditCollectionModalContentConnector.js b/frontend/src/Collection/Edit/EditCollectionModalContentConnector.js deleted file mode 100644 index 32f639ca98..0000000000 --- a/frontend/src/Collection/Edit/EditCollectionModalContentConnector.js +++ /dev/null @@ -1,120 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveMovieCollection, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions'; -import createCollectionSelector from 'Store/Selectors/createCollectionSelector'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditCollectionModalContent from './EditCollectionModalContent'; - -function createIsPathChangingSelector() { - return createSelector( - (state) => state.movieCollections.pendingChanges, - createCollectionSelector(), - (pendingChanges, collection) => { - const rootFolderPath = pendingChanges.rootFolderPath; - - if (rootFolderPath == null) { - return false; - } - - return collection.rootFolderPath !== rootFolderPath; - } - ); -} - -function createMapStateToProps() { - return createSelector( - (state) => state.movieCollections, - createCollectionSelector(), - createIsPathChangingSelector(), - createDimensionsSelector(), - (moviesState, collection, isPathChanging, dimensions) => { - const { - isSaving, - saveError, - pendingChanges - } = moviesState; - - const movieSettings = { - monitored: collection.monitored, - qualityProfileId: collection.qualityProfileId, - minimumAvailability: collection.minimumAvailability, - rootFolderPath: collection.rootFolderPath, - tags: collection.tags, - searchOnAdd: collection.searchOnAdd - }; - - const settings = selectSettings(movieSettings, pendingChanges, saveError); - - return { - title: collection.title, - images: collection.images, - overview: collection.overview, - isSaving, - saveError, - isPathChanging, - originalPath: collection.path, - item: settings.settings, - isSmallScreen: dimensions.isSmallScreen, - ...settings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchSetMovieCollectionValue: setMovieCollectionValue, - dispatchSaveMovieCollection: saveMovieCollection -}; - -class EditCollectionModalContentConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetMovieCollectionValue({ name, value }); - }; - - onSavePress = () => { - this.props.dispatchSaveMovieCollection({ - id: this.props.collectionId - }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditCollectionModalContentConnector.propTypes = { - collectionId: PropTypes.number, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - dispatchSetMovieCollectionValue: PropTypes.func.isRequired, - dispatchSaveMovieCollection: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditCollectionModalContentConnector); diff --git a/frontend/src/Collection/Edit/EditMovieCollectionModal.tsx b/frontend/src/Collection/Edit/EditMovieCollectionModal.tsx new file mode 100644 index 0000000000..02c3c7b21a --- /dev/null +++ b/frontend/src/Collection/Edit/EditMovieCollectionModal.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditMovieCollectionModalContent, { + EditMovieCollectionModalContentProps, +} from './EditMovieCollectionModalContent'; + +interface EditMovieCollectionModalProps + extends EditMovieCollectionModalContentProps { + isOpen: boolean; +} + +function EditMovieCollectionModal({ + isOpen, + onModalClose, + ...otherProps +}: EditMovieCollectionModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch(clearPendingChanges({ section: 'movieCollections' })); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditMovieCollectionModal; diff --git a/frontend/src/Collection/Edit/EditCollectionModalContent.css b/frontend/src/Collection/Edit/EditMovieCollectionModalContent.css similarity index 100% rename from frontend/src/Collection/Edit/EditCollectionModalContent.css rename to frontend/src/Collection/Edit/EditMovieCollectionModalContent.css diff --git a/frontend/src/Collection/Edit/EditCollectionModalContent.css.d.ts b/frontend/src/Collection/Edit/EditMovieCollectionModalContent.css.d.ts similarity index 100% rename from frontend/src/Collection/Edit/EditCollectionModalContent.css.d.ts rename to frontend/src/Collection/Edit/EditMovieCollectionModalContent.css.d.ts diff --git a/frontend/src/Collection/Edit/EditMovieCollectionModalContent.tsx b/frontend/src/Collection/Edit/EditMovieCollectionModalContent.tsx new file mode 100644 index 0000000000..6d8dc1ba50 --- /dev/null +++ b/frontend/src/Collection/Edit/EditMovieCollectionModalContent.tsx @@ -0,0 +1,214 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import useMovieCollection from 'Collection/useMovieCollection'; +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 SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +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 usePrevious from 'Helpers/Hooks/usePrevious'; +import { inputTypes } from 'Helpers/Props'; +import MoviePoster from 'Movie/MoviePoster'; +import { + saveMovieCollection, + setMovieCollectionValue, +} from 'Store/Actions/movieCollectionActions'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './EditMovieCollectionModalContent.css'; + +export interface EditMovieCollectionModalContentProps { + collectionId: number; + onModalClose: () => void; +} + +function EditMovieCollectionModalContent({ + collectionId, + onModalClose, +}: EditMovieCollectionModalContentProps) { + const dispatch = useDispatch(); + + const { + title, + overview, + monitored, + qualityProfileId, + minimumAvailability, + rootFolderPath, + searchOnAdd, + images, + tags, + } = useMovieCollection(collectionId)!; + + const { isSaving, saveError, pendingChanges } = useSelector( + (state: AppState) => state.movieCollections + ); + const { isSmallScreen } = useSelector(createDimensionsSelector()); + + const wasSaving = usePrevious(isSaving); + + const { settings, ...otherSettings } = useMemo(() => { + return selectSettings( + { + monitored, + minimumAvailability, + qualityProfileId, + rootFolderPath, + searchOnAdd, + tags, + }, + pendingChanges, + saveError + ); + }, [ + monitored, + minimumAvailability, + qualityProfileId, + rootFolderPath, + searchOnAdd, + tags, + pendingChanges, + saveError, + ]); + + const handleInputChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error actions aren't typed + dispatch(setMovieCollectionValue({ name, value })); + }, + [dispatch] + ); + + const handleSavePress = useCallback(() => { + dispatch( + saveMovieCollection({ + id: collectionId, + }) + ); + }, [collectionId, dispatch]); + + useEffect(() => { + if (!isSaving && wasSaving && !saveError) { + onModalClose(); + } + }, [isSaving, wasSaving, saveError, onModalClose]); + + return ( + + + {translate('EditMovieCollectionModalHeader', { title })} + + + +
+ {isSmallScreen ? null : ( +
+ +
+ )} + +
+
{overview}
+ +
+ + {translate('Monitored')} + + + + + + {translate('MinimumAvailability')} + + + + + + {translate('QualityProfile')} + + + + + + {translate('RootFolder')} + + + + + + {translate('Tags')} + + + + + + {translate('SearchOnAdd')} + + + +
+
+
+
+ + + + + + {translate('Save')} + + +
+ ); +} + +export default EditMovieCollectionModalContent; diff --git a/frontend/src/Collection/Overview/CollectionOverview.js b/frontend/src/Collection/Overview/CollectionOverview.js index 708a9fbbd7..9c8ed13973 100644 --- a/frontend/src/Collection/Overview/CollectionOverview.js +++ b/frontend/src/Collection/Overview/CollectionOverview.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import TextTruncate from 'react-text-truncate'; import { Navigation } from 'swiper'; import { Swiper, SwiperSlide } from 'swiper/react'; -import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector'; +import EditMovieCollectionModal from 'Collection/Edit/EditMovieCollectionModal'; import CheckInput from 'Components/Form/CheckInput'; import Icon from 'Components/Icon'; import Label from 'Components/Label'; @@ -311,7 +311,7 @@ class CollectionOverview extends Component { - state.movieCollections.itemMap, + (state: AppState) => state.movieCollections.items, + (itemMap, allMovieCollections) => { + return collectionId + ? allMovieCollections[itemMap[collectionId]] + : undefined; + } + ); +} + +function useMovieCollection(collectionId: number | undefined) { + return useSelector(createMovieCollectionSelector(collectionId)); +} + +export default useMovieCollection; diff --git a/frontend/src/Movie/Movie.ts b/frontend/src/Movie/Movie.ts index 6946fb41d4..84fe463ae1 100644 --- a/frontend/src/Movie/Movie.ts +++ b/frontend/src/Movie/Movie.ts @@ -9,6 +9,8 @@ export type MovieStatus = | 'released' | 'deleted'; +export type MovieAvailability = 'announced' | 'inCinemas' | 'released'; + export type CoverType = 'poster' | 'fanart' | 'headshot'; export interface Image { @@ -70,7 +72,7 @@ interface Movie extends ModelBase { releaseDate?: string; rootFolderPath: string; runtime: number; - minimumAvailability: string; + minimumAvailability: MovieAvailability; path: string; genres: string[]; ratings: Ratings; diff --git a/frontend/src/typings/MovieCollection.ts b/frontend/src/typings/MovieCollection.ts index b0b7d6b1c4..89525668ba 100644 --- a/frontend/src/typings/MovieCollection.ts +++ b/frontend/src/typings/MovieCollection.ts @@ -1,14 +1,17 @@ import ModelBase from 'App/ModelBase'; -import Movie from 'Movie/Movie'; +import Movie, { Image, MovieAvailability } from 'Movie/Movie'; interface MovieCollection extends ModelBase { - title: string; - sortTitle: string; tmdbId: number; + sortTitle: string; + title: string; overview: string; monitored: boolean; - rootFolderPath: string; + minimumAvailability: MovieAvailability; qualityProfileId: number; + rootFolderPath: string; + searchOnAdd: boolean; + images: Image[]; movies: Movie[]; missingMovies: number; tags: number[]; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index e5df77133d..d00ec6b569 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -593,6 +593,7 @@ "EditIndexerImplementation": "Edit Indexer - {implementationName}", "EditMetadata": "Edit {metadataType} Metadata", "EditMovie": "Edit Movie", + "EditMovieCollectionModalHeader": "Edit - {title}", "EditMovieFile": "Edit Movie File", "EditMovieModalHeader": "Edit - {title}", "EditMovies": "Edit Movies",