1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Convert EditMovieModal to TypeScript

Towards #10700

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
This commit is contained in:
Bogdan
2025-03-07 18:12:02 +02:00
parent 28dee7bc01
commit 7db12b6e58
42 changed files with 304 additions and 438 deletions
+2
View File
@@ -64,6 +64,8 @@ interface MoviesAppState
deleteOptions: {
addImportExclusion: boolean;
};
pendingChanges: Partial<Movie>;
}
export default MoviesAppState;
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
@@ -172,7 +172,7 @@ class CollectionMovie extends Component {
collectionId={collectionId}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}
+2 -2
View File
@@ -24,7 +24,7 @@ import TraktRating from 'Components/TraktRating';
import { icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MovieCollectionLabel from 'Movie/MovieCollectionLabel';
@@ -730,7 +730,7 @@ class MovieDetails extends Component {
onModalClose={this.onOrganizeModalClose}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}
-26
View File
@@ -1,26 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditMovieModalContentConnector from './EditMovieModalContentConnector';
function EditMovieModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditMovieModal.propTypes = {
...EditMovieModalContentConnector.propTypes,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditMovieModal;
@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMovieModalContent, {
EditMovieModalContentProps,
} from './EditMovieModalContent';
interface EditMovieModalProps extends EditMovieModalContentProps {
isOpen: boolean;
}
function EditMovieModal({
isOpen,
onModalClose,
...otherProps
}: EditMovieModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section: 'movies' }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<EditMovieModalContent {...otherProps} onModalClose={handleModalClose} />
</Modal>
);
}
export default EditMovieModal;
@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMovieModal from './EditMovieModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditMovieModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'movies' });
this.props.onModalClose();
};
//
// Render
render() {
return (
<EditMovieModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditMovieModalConnector.propTypes = {
...EditMovieModal.propTypes,
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditMovieModalConnector);
@@ -1,217 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
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 Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import translate from 'Utilities/String/translate';
import styles from './EditMovieModalContent.css';
class EditMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmMoveModalOpen: false
};
}
//
// Listeners
onCancelPress = () => {
this.setState({ isConfirmMoveModalOpen: false });
};
onSavePress = () => {
const {
isPathChanging,
onSavePress
} = this.props;
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
this.setState({ isConfirmMoveModalOpen: true });
} else {
this.setState({ isConfirmMoveModalOpen: false });
onSavePress(false);
}
};
onMoveMoviePress = () => {
this.setState({ isConfirmMoveModalOpen: false });
this.props.onSavePress(true);
};
//
// Render
render() {
const {
title,
item,
isSaving,
originalPath,
onInputChange,
onModalClose,
onDeleteMoviePress,
...otherProps
} = this.props;
const {
monitored,
qualityProfileId,
minimumAvailability,
// Id,
path,
tags
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Edit')} - {title}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredHelpText')}
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Path')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="path"
{...path}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteMoviePress}
>
{translate('Delete')}
</Button>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
<MoveMovieModal
originalPath={originalPath}
destinationPath={path.value}
isOpen={this.state.isConfirmMoveModalOpen}
onModalClose={this.onCancelPress}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
</ModalContent>
);
}
}
EditMovieModalContent.propTypes = {
movieId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
isPathChanging: PropTypes.bool.isRequired,
originalPath: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteMoviePress: PropTypes.func.isRequired
};
export default EditMovieModalContent;
@@ -0,0 +1,229 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import AppState from 'App/State/AppState';
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 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 Popover from 'Components/Tooltip/Popover';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import useMovie from 'Movie/useMovie';
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
import selectSettings from 'Store/Selectors/selectSettings';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditMovieModalContent.css';
export interface EditMovieModalContentProps {
movieId: number;
onModalClose: () => void;
onDeleteMoviePress: () => void;
}
function EditMovieModalContent({
movieId,
onModalClose,
onDeleteMoviePress,
}: EditMovieModalContentProps) {
const dispatch = useDispatch();
const {
title,
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
} = useMovie(movieId)!;
const { isSaving, saveError, pendingChanges } = useSelector(
(state: AppState) => state.movies
);
const wasSaving = usePrevious(isSaving);
const isPathChanging = pendingChanges.path && path !== pendingChanges.path;
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const { settings, ...otherSettings } = useMemo(() => {
return selectSettings(
{
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
},
pendingChanges,
saveError
);
}, [
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
pendingChanges,
saveError,
]);
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
// @ts-expect-error actions aren't typed
dispatch(setMovieValue({ name, value }));
},
[dispatch]
);
const handleCancelPress = useCallback(() => {
setIsConfirmMoveModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
if (isPathChanging && !isConfirmMoveModalOpen) {
setIsConfirmMoveModalOpen(true);
} else {
setIsConfirmMoveModalOpen(false);
dispatch(
saveMovie({
id: movieId,
moveFiles: false,
})
);
}
}, [movieId, isPathChanging, isConfirmMoveModalOpen, dispatch]);
const handleMoveMoviePress = useCallback(() => {
setIsConfirmMoveModalOpen(false);
dispatch(
saveMovie({
id: movieId,
moveFiles: true,
})
);
}, [movieId, dispatch]);
useEffect(() => {
if (!isSaving && wasSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditMovieModalHeader', { title })}</ModalHeader>
<ModalBody>
<Form {...otherSettings}>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredMovieHelpText')}
{...settings.monitored}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={<Icon className={styles.labelIcon} name={icons.INFO} />}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...settings.minimumAvailability}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...settings.qualityProfileId}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Path')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="path"
{...settings.path}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...settings.tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteMoviePress}
>
{translate('Delete')}
</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
error={saveError}
isSpinning={isSaving}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
<MoveMovieModal
originalPath={path}
destinationPath={pendingChanges.path}
isOpen={isConfirmMoveModalOpen}
onModalClose={handleCancelPress}
onSavePress={handleSavePress}
onMoveMoviePress={handleMoveMoviePress}
/>
</ModalContent>
);
}
export default EditMovieModalContent;
@@ -1,115 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import EditMovieModalContent from './EditMovieModalContent';
function createIsPathChangingSelector() {
return createSelector(
(state) => state.movies.pendingChanges,
createMovieSelector(),
(pendingChanges, movie) => {
const path = pendingChanges.path;
if (path == null) {
return false;
}
return movie.path !== path;
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
createMovieSelector(),
createIsPathChangingSelector(),
(moviesState, movie, isPathChanging) => {
const {
isSaving,
saveError,
pendingChanges
} = moviesState;
const movieSettings = {
monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags
};
const settings = selectSettings(movieSettings, pendingChanges, saveError);
return {
title: movie.title,
isSaving,
saveError,
isPathChanging,
originalPath: movie.path,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
dispatchSetMovieValue: setMovieValue,
dispatchSaveMovie: saveMovie
};
class EditMovieModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetMovieValue({ name, value });
};
onSavePress = (moveFiles) => {
this.props.dispatchSaveMovie({
id: this.props.movieId,
moveFiles
});
};
//
// Render
render() {
return (
<EditMovieModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
);
}
}
EditMovieModalContentConnector.propTypes = {
movieId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetMovieValue: PropTypes.func.isRequired,
dispatchSaveMovie: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditMovieModalContentConnector);
@@ -11,7 +11,7 @@ import Popover from 'Components/Tooltip/Popover';
import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
@@ -250,7 +250,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
</div>
</div>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={movieId}
onModalClose={onEditMovieModalClose}
@@ -15,7 +15,7 @@ import TraktRating from 'Components/TraktRating';
import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
@@ -384,7 +384,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
showTags={showTags}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={movieId}
onModalClose={onEditMovieModalClose}
@@ -18,7 +18,7 @@ import TraktRating from 'Components/TraktRating';
import { icons, kinds } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
import { Statistics } from 'Movie/Movie';
import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
@@ -480,7 +480,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
return null;
})}
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={movieId}
onModalClose={onEditMovieModalClose}