mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-23 22:07:07 -04:00
Convert EditSeriesModal to TypeScript
This commit is contained in:
parent
e361f18837
commit
2f62494adc
18 changed files with 529 additions and 450 deletions
|
@ -1,11 +1,16 @@
|
|||
import Column from 'Components/Table/Column';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import { ValidationFailure } from 'typings/pending';
|
||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
message: string;
|
||||
};
|
||||
status?: number;
|
||||
responseJSON:
|
||||
| {
|
||||
message: string | undefined;
|
||||
}
|
||||
| ValidationFailure[]
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export interface AppSectionDeleteState {
|
||||
|
|
|
@ -59,6 +59,8 @@ interface SeriesAppState
|
|||
deleteOptions: {
|
||||
addImportListExclusion: boolean;
|
||||
};
|
||||
|
||||
pendingChanges: Partial<Series>;
|
||||
}
|
||||
|
||||
export default SeriesAppState;
|
||||
|
|
|
@ -24,7 +24,7 @@ import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'He
|
|||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
||||
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
|
||||
import EditSeriesModal from 'Series/Edit/EditSeriesModal';
|
||||
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
|
||||
import MonitoringOptionsModal from 'Series/MonitoringOptions/MonitoringOptionsModal';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
|
@ -709,7 +709,7 @@ class SeriesDetails extends Component {
|
|||
onModalClose={this.onSeriesHistoryModalClose}
|
||||
/>
|
||||
|
||||
<EditSeriesModalConnector
|
||||
<EditSeriesModal
|
||||
isOpen={isEditSeriesModalOpen}
|
||||
seriesId={id}
|
||||
onModalClose={this.onEditSeriesModalClose}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditSeriesModalContentConnector from './EditSeriesModalContentConnector';
|
||||
|
||||
function EditSeriesModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditSeriesModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditSeriesModal.propTypes = {
|
||||
...EditSeriesModalContentConnector.propTypes,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditSeriesModal;
|
34
frontend/src/Series/Edit/EditSeriesModal.tsx
Normal file
34
frontend/src/Series/Edit/EditSeriesModal.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { EditSeriesModalContentProps } from './EditSeriesModalContent';
|
||||
import EditSeriesModalContentConnector from './EditSeriesModalContentConnector';
|
||||
|
||||
interface EditSeriesModalProps extends EditSeriesModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditSeriesModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditSeriesModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'series' }));
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditSeriesModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditSeriesModal;
|
|
@ -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 EditSeriesModal from './EditSeriesModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditSeriesModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'series' });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditSeriesModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditSeriesModalConnector.propTypes = {
|
||||
...EditSeriesModal.propTypes,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(EditSeriesModalConnector);
|
|
@ -1,240 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
|
||||
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 MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditSeriesModalContent.css';
|
||||
|
||||
class EditSeriesModalContent 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);
|
||||
}
|
||||
};
|
||||
|
||||
onMoveSeriesPress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
this.props.onSavePress(true);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
item,
|
||||
isSaving,
|
||||
originalPath,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
onDeleteSeriesPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
seasonFolder,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
path,
|
||||
tags
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('EditSeriesModalHeader', { title })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText={translate('MonitoredEpisodesHelpText')}
|
||||
{...monitored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewSeasons')}
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MonitorNewSeasons')}
|
||||
body={<SeriesMonitorNewItemsOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||
name="monitorNewItems"
|
||||
helpText={translate('MonitorNewSeasonsHelpText')}
|
||||
{...monitorNewItems}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('UseSeasonFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="seasonFolder"
|
||||
helpText={translate('UseSeasonFolderHelpText')}
|
||||
{...seasonFolder}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...qualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SeriesType')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SERIES_TYPE_SELECT}
|
||||
name="seriesType"
|
||||
{...seriesType}
|
||||
helpText={translate('SeriesTypesHelpText')}
|
||||
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={onDeleteSeriesPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveSeriesModal
|
||||
originalPath={originalPath}
|
||||
destinationPath={path.value}
|
||||
isOpen={this.state.isConfirmMoveModalOpen}
|
||||
onModalClose={this.onCancelPress}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveSeriesPress={this.onMoveSeriesPress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditSeriesModalContent.propTypes = {
|
||||
seriesId: 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,
|
||||
onDeleteSeriesPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditSeriesModalContent;
|
248
frontend/src/Series/Edit/EditSeriesModalContent.tsx
Normal file
248
frontend/src/Series/Edit/EditSeriesModalContent.tsx
Normal file
|
@ -0,0 +1,248 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
|
||||
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 { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import { saveSeries, setSeriesValue } from 'Store/Actions/seriesActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditSeriesModalContent.css';
|
||||
|
||||
export interface EditSeriesModalContentProps {
|
||||
seriesId: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteSeriesPress: () => void;
|
||||
}
|
||||
function EditSeriesModalContent({
|
||||
seriesId,
|
||||
onModalClose,
|
||||
onDeleteSeriesPress,
|
||||
}: EditSeriesModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
title,
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
seasonFolder,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
path,
|
||||
tags,
|
||||
} = useSeries(seriesId)!;
|
||||
const { isSaving, saveError, pendingChanges } = useSelector(
|
||||
(state: AppState) => state.series
|
||||
);
|
||||
|
||||
const isPathChanging = pendingChanges.path && path !== pendingChanges.path;
|
||||
|
||||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||
|
||||
const { settings, ...otherSettings } = useMemo(() => {
|
||||
return selectSettings(
|
||||
{
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
seasonFolder,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
path,
|
||||
tags,
|
||||
},
|
||||
pendingChanges,
|
||||
saveError
|
||||
);
|
||||
}, [
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
seasonFolder,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
path,
|
||||
tags,
|
||||
pendingChanges,
|
||||
saveError,
|
||||
]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
// @ts-expect-error actions aren't typed
|
||||
dispatch(setSeriesValue({ name, value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
if (isPathChanging && !isConfirmMoveModalOpen) {
|
||||
setIsConfirmMoveModalOpen(true);
|
||||
} else {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveSeries({
|
||||
id: seriesId,
|
||||
moveFiles: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [seriesId, isPathChanging, isConfirmMoveModalOpen, dispatch]);
|
||||
|
||||
const handleMoveSeriesPress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveSeries({
|
||||
id: seriesId,
|
||||
moveFiles: true,
|
||||
})
|
||||
);
|
||||
}, [seriesId, dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('EditSeriesModalHeader', { title })}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherSettings}>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText={translate('MonitoredEpisodesHelpText')}
|
||||
{...settings.monitored}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewSeasons')}
|
||||
<Popover
|
||||
anchor={<Icon className={styles.labelIcon} name={icons.INFO} />}
|
||||
title={translate('MonitorNewSeasons')}
|
||||
body={<SeriesMonitorNewItemsOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||
name="monitorNewItems"
|
||||
helpText={translate('MonitorNewSeasonsHelpText')}
|
||||
{...settings.monitorNewItems}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('UseSeasonFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="seasonFolder"
|
||||
helpText={translate('UseSeasonFolderHelpText')}
|
||||
{...settings.seasonFolder}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...settings.qualityProfileId}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SeriesType')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SERIES_TYPE_SELECT}
|
||||
name="seriesType"
|
||||
{...settings.seriesType}
|
||||
helpText={translate('SeriesTypesHelpText')}
|
||||
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={onDeleteSeriesPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
error={saveError}
|
||||
isSpinning={isSaving}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveSeriesModal
|
||||
originalPath={path}
|
||||
destinationPath={pendingChanges.path}
|
||||
isOpen={isConfirmMoveModalOpen}
|
||||
onModalClose={handleCancelPress}
|
||||
onSavePress={handleSavePress}
|
||||
onMoveSeriesPress={handleMoveSeriesPress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditSeriesModalContent;
|
|
@ -9,7 +9,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
||||
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
|
||||
import EditSeriesModal from 'Series/Edit/EditSeriesModal';
|
||||
import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar';
|
||||
import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect';
|
||||
import { Statistics } from 'Series/Series';
|
||||
|
@ -252,7 +252,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<EditSeriesModalConnector
|
||||
<EditSeriesModal
|
||||
isOpen={isEditSeriesModalOpen}
|
||||
seriesId={seriesId}
|
||||
onModalClose={onEditSeriesModalClose}
|
||||
|
|
|
@ -9,7 +9,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
||||
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
|
||||
import EditSeriesModal from 'Series/Edit/EditSeriesModal';
|
||||
import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar';
|
||||
import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect';
|
||||
import { Statistics } from 'Series/Series';
|
||||
|
@ -268,7 +268,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
|||
showTags={showTags}
|
||||
/>
|
||||
|
||||
<EditSeriesModalConnector
|
||||
<EditSeriesModal
|
||||
isOpen={isEditSeriesModalOpen}
|
||||
seriesId={seriesId}
|
||||
onModalClose={onEditSeriesModalClose}
|
||||
|
|
|
@ -15,7 +15,7 @@ import Column from 'Components/Table/Column';
|
|||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
||||
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
|
||||
import EditSeriesModal from 'Series/Edit/EditSeriesModal';
|
||||
import createSeriesIndexItemSelector from 'Series/Index/createSeriesIndexItemSelector';
|
||||
import { Statistics } from 'Series/Series';
|
||||
import SeriesBanner from 'Series/SeriesBanner';
|
||||
|
@ -492,7 +492,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
return null;
|
||||
})}
|
||||
|
||||
<EditSeriesModalConnector
|
||||
<EditSeriesModal
|
||||
isOpen={isEditSeriesModalOpen}
|
||||
seriesId={seriesId}
|
||||
onModalClose={onEditSeriesModalClose}
|
||||
|
|
|
@ -39,7 +39,7 @@ function createImportListExclusionSelector(id?: number) {
|
|||
importListExclusions;
|
||||
|
||||
const mapping = id
|
||||
? items.find((i) => i.id === id)
|
||||
? items.find((i) => i.id === id)!
|
||||
: newImportListExclusion;
|
||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||
|
||||
|
|
|
@ -19,14 +19,15 @@ import {
|
|||
setReleaseProfileValue,
|
||||
} from 'Store/Actions/Settings/releaseProfiles';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { PendingSection } from 'typings/pending';
|
||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditReleaseProfileModalContent.css';
|
||||
|
||||
const tagInputDelimiters = ['Tab', 'Enter'];
|
||||
|
||||
const newReleaseProfile = {
|
||||
const newReleaseProfile: ReleaseProfile = {
|
||||
id: 0,
|
||||
name: '',
|
||||
enabled: true,
|
||||
required: [],
|
||||
ignored: [],
|
||||
|
@ -41,8 +42,12 @@ function createReleaseProfileSelector(id?: number) {
|
|||
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
|
||||
releaseProfiles;
|
||||
|
||||
const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile;
|
||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||
const mapping = id ? items.find((i) => i.id === id)! : newReleaseProfile;
|
||||
const settings = selectSettings<ReleaseProfile>(
|
||||
mapping,
|
||||
pendingChanges,
|
||||
saveError
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
@ -50,7 +55,7 @@ function createReleaseProfileSelector(id?: number) {
|
|||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings as PendingSection<ReleaseProfile>,
|
||||
item: settings.settings,
|
||||
...settings,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
function getValidationFailures(saveError) {
|
||||
if (!saveError || saveError.status !== 400) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _.cloneDeep(saveError.responseJSON);
|
||||
}
|
||||
|
||||
function mapFailure(failure) {
|
||||
return {
|
||||
errorMessage: failure.errorMessage,
|
||||
infoLink: failure.infoLink,
|
||||
detailedDescription: failure.detailedDescription,
|
||||
|
||||
// TODO: Remove these renamed properties
|
||||
message: failure.errorMessage,
|
||||
link: failure.infoLink,
|
||||
detailedMessage: failure.detailedDescription
|
||||
};
|
||||
}
|
||||
|
||||
function selectSettings(item, pendingChanges, saveError) {
|
||||
const validationFailures = getValidationFailures(saveError);
|
||||
|
||||
// Merge all settings from the item along with pending
|
||||
// changes to ensure any settings that were not included
|
||||
// with the item are included.
|
||||
const allSettings = Object.assign({}, item, pendingChanges);
|
||||
|
||||
const settings = _.reduce(allSettings, (result, value, key) => {
|
||||
if (key === 'fields') {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return a flattened value
|
||||
if (key === 'implementationName') {
|
||||
result.implementationName = item[key];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const setting = {
|
||||
value: item[key],
|
||||
errors: _.map(_.remove(validationFailures, (failure) => {
|
||||
return failure.propertyName.toLowerCase() === key.toLowerCase() && !failure.isWarning;
|
||||
}), mapFailure),
|
||||
|
||||
warnings: _.map(_.remove(validationFailures, (failure) => {
|
||||
return failure.propertyName.toLowerCase() === key.toLowerCase() && failure.isWarning;
|
||||
}), mapFailure)
|
||||
};
|
||||
|
||||
if (pendingChanges.hasOwnProperty(key)) {
|
||||
setting.previousValue = setting.value;
|
||||
setting.value = pendingChanges[key];
|
||||
setting.pending = true;
|
||||
}
|
||||
|
||||
result[key] = setting;
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const fields = _.reduce(item.fields, (result, f) => {
|
||||
const field = Object.assign({ pending: false }, f);
|
||||
const hasPendingFieldChange = pendingChanges.fields && pendingChanges.fields.hasOwnProperty(field.name);
|
||||
|
||||
if (hasPendingFieldChange) {
|
||||
field.previousValue = field.value;
|
||||
field.value = pendingChanges.fields[field.name];
|
||||
field.pending = true;
|
||||
}
|
||||
|
||||
field.errors = _.map(_.remove(validationFailures, (failure) => {
|
||||
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && !failure.isWarning;
|
||||
}), mapFailure);
|
||||
|
||||
field.warnings = _.map(_.remove(validationFailures, (failure) => {
|
||||
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && failure.isWarning;
|
||||
}), mapFailure);
|
||||
|
||||
result.push(field);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
if (fields.length) {
|
||||
settings.fields = fields;
|
||||
}
|
||||
|
||||
const validationErrors = _.filter(validationFailures, (failure) => {
|
||||
return !failure.isWarning;
|
||||
});
|
||||
|
||||
const validationWarnings = _.filter(validationFailures, (failure) => {
|
||||
return failure.isWarning;
|
||||
});
|
||||
|
||||
return {
|
||||
settings,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
hasPendingChanges: !_.isEmpty(pendingChanges),
|
||||
hasSettings: !_.isEmpty(settings),
|
||||
pendingChanges
|
||||
};
|
||||
}
|
||||
|
||||
export default selectSettings;
|
167
frontend/src/Store/Selectors/selectSettings.ts
Normal file
167
frontend/src/Store/Selectors/selectSettings.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { cloneDeep, isEmpty } from 'lodash';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import Field from 'typings/Field';
|
||||
import {
|
||||
Failure,
|
||||
Pending,
|
||||
PendingField,
|
||||
PendingSection,
|
||||
ValidationError,
|
||||
ValidationFailure,
|
||||
ValidationWarning,
|
||||
} from 'typings/pending';
|
||||
|
||||
interface ValidationFailures {
|
||||
errors: ValidationError[];
|
||||
warnings: ValidationWarning[];
|
||||
}
|
||||
|
||||
function getValidationFailures(saveError?: Error): ValidationFailures {
|
||||
if (!saveError || saveError.status !== 400) {
|
||||
return {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
return cloneDeep(saveError.responseJSON as ValidationFailure[]).reduce(
|
||||
(acc: ValidationFailures, failure: ValidationFailure) => {
|
||||
if (failure.isWarning) {
|
||||
acc.warnings.push(failure as ValidationWarning);
|
||||
} else {
|
||||
acc.errors.push(failure as ValidationError);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getFailures(failures: ValidationFailure[], key: string) {
|
||||
const result = [];
|
||||
|
||||
for (let i = failures.length - 1; i >= 0; i--) {
|
||||
if (failures[i].propertyName.toLowerCase() === key.toLowerCase()) {
|
||||
result.unshift(mapFailure(failures[i]));
|
||||
|
||||
failures.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function mapFailure(failure: ValidationFailure): Failure {
|
||||
return {
|
||||
errorMessage: failure.errorMessage,
|
||||
infoLink: failure.infoLink,
|
||||
detailedDescription: failure.detailedDescription,
|
||||
|
||||
// TODO: Remove these renamed properties
|
||||
message: failure.errorMessage,
|
||||
link: failure.infoLink,
|
||||
detailedMessage: failure.detailedDescription,
|
||||
};
|
||||
}
|
||||
|
||||
interface ModelBaseSetting {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[id: string]: any;
|
||||
}
|
||||
|
||||
function selectSettings<T extends ModelBaseSetting>(
|
||||
item: T,
|
||||
pendingChanges: Partial<ModelBaseSetting>,
|
||||
saveError?: Error
|
||||
) {
|
||||
const { errors, warnings } = getValidationFailures(saveError);
|
||||
|
||||
// Merge all settings from the item along with pending
|
||||
// changes to ensure any settings that were not included
|
||||
// with the item are included.
|
||||
const allSettings = Object.assign({}, item, pendingChanges);
|
||||
|
||||
const settings = Object.keys(allSettings).reduce(
|
||||
(acc: PendingSection<T>, key) => {
|
||||
if (key === 'fields') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Return a flattened value
|
||||
if (key === 'implementationName') {
|
||||
// acc.implementationName = item[key];
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
const setting: Pending<T> = {
|
||||
value: item[key],
|
||||
errors: getFailures(errors, key),
|
||||
warnings: getFailures(warnings, key),
|
||||
};
|
||||
|
||||
if (pendingChanges.hasOwnProperty(key)) {
|
||||
setting.previousValue = setting.value;
|
||||
setting.value = pendingChanges[key];
|
||||
setting.pending = true;
|
||||
}
|
||||
|
||||
// @ts-expect-error - This is a valid key
|
||||
acc[key] = setting;
|
||||
return acc;
|
||||
},
|
||||
{} as PendingSection<T>
|
||||
);
|
||||
|
||||
if ('fields' in item) {
|
||||
const fields =
|
||||
(item.fields as Field[]).reduce((acc: PendingField<T>[], f) => {
|
||||
const field: PendingField<T> = Object.assign(
|
||||
{ pending: false, errors: [], warnings: [] },
|
||||
f
|
||||
);
|
||||
|
||||
if ('fields' in pendingChanges) {
|
||||
const pendingChangesFields = pendingChanges.fields as Record<
|
||||
string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
any
|
||||
>;
|
||||
|
||||
if (pendingChangesFields.hasOwnProperty(field.name)) {
|
||||
field.previousValue = field.value;
|
||||
field.value = pendingChangesFields[field.name];
|
||||
field.pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
field.errors = getFailures(errors, field.name);
|
||||
field.warnings = getFailures(warnings, field.name);
|
||||
|
||||
acc.push(field);
|
||||
return acc;
|
||||
}, []) ?? [];
|
||||
|
||||
if (fields.length) {
|
||||
settings.fields = fields;
|
||||
}
|
||||
}
|
||||
|
||||
const validationErrors = errors;
|
||||
const validationWarnings = warnings;
|
||||
|
||||
return {
|
||||
settings,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
hasPendingChanges: !isEmpty(pendingChanges),
|
||||
hasSettings: !isEmpty(settings),
|
||||
pendingChanges,
|
||||
};
|
||||
}
|
||||
|
||||
export default selectSettings;
|
|
@ -1,19 +1,15 @@
|
|||
interface AjaxResponse {
|
||||
responseJSON:
|
||||
| {
|
||||
message: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
|
||||
function getErrorMessage(xhr: AjaxResponse, fallbackErrorMessage?: string) {
|
||||
if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) {
|
||||
function getErrorMessage(xhr: Error, fallbackErrorMessage?: string) {
|
||||
if (!xhr || !xhr.responseJSON) {
|
||||
return fallbackErrorMessage;
|
||||
}
|
||||
|
||||
const message = xhr.responseJSON.message;
|
||||
if ('message' in xhr.responseJSON && xhr.responseJSON.message) {
|
||||
return xhr.responseJSON.message;
|
||||
}
|
||||
|
||||
return message || fallbackErrorMessage;
|
||||
return fallbackErrorMessage;
|
||||
}
|
||||
|
||||
export default getErrorMessage;
|
||||
|
|
|
@ -12,7 +12,7 @@ interface Field {
|
|||
order: number;
|
||||
name: string;
|
||||
label: string;
|
||||
value: boolean | number | string;
|
||||
value: boolean | number | string | number[];
|
||||
type: string;
|
||||
advanced: boolean;
|
||||
privacy: string;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import Field from './Field';
|
||||
|
||||
export interface ValidationFailure {
|
||||
isWarning: boolean;
|
||||
propertyName: string;
|
||||
errorMessage: string;
|
||||
infoLink?: string;
|
||||
|
@ -14,12 +17,46 @@ export interface ValidationWarning extends ValidationFailure {
|
|||
isWarning: true;
|
||||
}
|
||||
|
||||
export interface Pending<T> {
|
||||
value: T;
|
||||
errors: ValidationError[];
|
||||
warnings: ValidationWarning[];
|
||||
export interface Failure {
|
||||
errorMessage: ValidationFailure['errorMessage'];
|
||||
infoLink: ValidationFailure['infoLink'];
|
||||
detailedDescription: ValidationFailure['detailedDescription'];
|
||||
|
||||
// TODO: Remove these renamed properties
|
||||
|
||||
message: ValidationFailure['errorMessage'];
|
||||
link: ValidationFailure['infoLink'];
|
||||
detailedMessage: ValidationFailure['detailedDescription'];
|
||||
}
|
||||
|
||||
export type PendingSection<T> = {
|
||||
[K in keyof T]: Pending<T[K]>;
|
||||
export interface Pending<T> {
|
||||
value: T;
|
||||
errors: Failure[];
|
||||
warnings: Failure[];
|
||||
pending?: boolean;
|
||||
previousValue?: T;
|
||||
}
|
||||
|
||||
export interface PendingField<T>
|
||||
extends Field,
|
||||
Omit<Pending<T>, 'previousValue' | 'value'> {
|
||||
previousValue?: Field['value'];
|
||||
}
|
||||
|
||||
// export type PendingSection<T> = {
|
||||
// [K in keyof T]: Pending<T[K]>;
|
||||
// };
|
||||
|
||||
type Mapped<T> = {
|
||||
[Prop in keyof T]: {
|
||||
value: T[Prop];
|
||||
errors: Failure[];
|
||||
warnings: Failure[];
|
||||
pending?: boolean;
|
||||
previousValue?: T[Prop];
|
||||
};
|
||||
};
|
||||
|
||||
export type PendingSection<T> = Mapped<T> & {
|
||||
fields?: PendingField<T>[];
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue