Convert EditSeriesModal to TypeScript

pull/7426/head
Mark McDowall 3 months ago
parent e361f18837
commit 2f62494adc

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

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

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

@ -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 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 interface Pending<T> {
value: T;
errors: ValidationError[];
warnings: ValidationWarning[];
errors: Failure[];
warnings: Failure[];
pending?: boolean;
previousValue?: T;
}
export type PendingSection<T> = {
[K in keyof T]: Pending<T[K]>;
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…
Cancel
Save