parent
e361f18837
commit
2f62494adc
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
Loading…
Reference in new issue