Convert Edit Movie Collection modal to TypeScript

This commit is contained in:
Bogdan 2025-04-18 23:15:28 +03:00
parent 1d1aca1a04
commit ae5450f75d
14 changed files with 286 additions and 381 deletions

View file

@ -7,6 +7,8 @@ interface MovieCollectionAppState
extends AppSectionState<MovieCollection>,
AppSectionSaveState {
itemMap: Record<number, number>;
pendingChanges: Partial<MovieCollection>;
}
export default MovieCollectionAppState;

View file

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditCollectionModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditCollectionModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditCollectionModal;

View file

@ -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 (
<EditCollectionModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditCollectionModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector);

View file

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Edit')} - {title}
</ModalHeader>
<ModalBody>
<div className={styles.container}>
{
!isSmallScreen &&
<div className={styles.poster}>
<MoviePoster
className={styles.poster}
images={images}
size={250}
/>
</div>
}
<div className={styles.info}>
<div className={styles.overview}>
{overview}
</div>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredCollectionHelpText')}
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</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('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
{...rootFolderPath}
includeMissingValue={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
onChange={onInputChange}
{...tags}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchOnAdd"
helpText={translate('SearchOnAddCollectionHelpText')}
{...searchOnAdd}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
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;

View file

@ -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 (
<EditCollectionModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
);
}
}
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);

View file

@ -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 (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<EditMovieCollectionModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditMovieCollectionModal;

View file

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('EditMovieCollectionModalHeader', { title })}
</ModalHeader>
<ModalBody>
<div className={styles.container}>
{isSmallScreen ? null : (
<div className={styles.poster}>
<MoviePoster
className={styles.poster}
images={images}
size={250}
/>
</div>
)}
<div className={styles.info}>
<div className={styles.overview}>{overview}</div>
<Form {...otherSettings}>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredCollectionHelpText')}
{...settings.monitored}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</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('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
{...settings.rootFolderPath}
includeMissingValue={true}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...settings.tags}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchOnAdd"
helpText={translate('SearchOnAddCollectionHelpText')}
{...settings.searchOnAdd}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
error={saveError}
isSpinning={isSaving}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditMovieCollectionModalContent;

View file

@ -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 {
</div>
</div>
<EditCollectionModalConnector
<EditMovieCollectionModal
isOpen={isEditCollectionModalOpen}
collectionId={id}
onModalClose={this.onEditCollectionModalClose}

View file

@ -0,0 +1,21 @@
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createMovieCollectionSelector(collectionId?: number) {
return createSelector(
(state: AppState) => 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;

View file

@ -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;

View file

@ -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[];

View file

@ -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",