diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index edf2b2d9d..fa55c8e38 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -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 { diff --git a/frontend/src/App/State/SeriesAppState.ts b/frontend/src/App/State/SeriesAppState.ts index 1f8a3427b..5da5987dd 100644 --- a/frontend/src/App/State/SeriesAppState.ts +++ b/frontend/src/App/State/SeriesAppState.ts @@ -59,6 +59,8 @@ interface SeriesAppState deleteOptions: { addImportListExclusion: boolean; }; + + pendingChanges: Partial; } export default SeriesAppState; diff --git a/frontend/src/Series/Details/SeriesDetails.js b/frontend/src/Series/Details/SeriesDetails.js index 116ce5d2f..211b40dd5 100644 --- a/frontend/src/Series/Details/SeriesDetails.js +++ b/frontend/src/Series/Details/SeriesDetails.js @@ -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} /> - - - - ); -} - -EditSeriesModal.propTypes = { - ...EditSeriesModalContentConnector.propTypes, - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditSeriesModal; diff --git a/frontend/src/Series/Edit/EditSeriesModal.tsx b/frontend/src/Series/Edit/EditSeriesModal.tsx new file mode 100644 index 000000000..5aabeb556 --- /dev/null +++ b/frontend/src/Series/Edit/EditSeriesModal.tsx @@ -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 ( + + + + ); +} + +export default EditSeriesModal; diff --git a/frontend/src/Series/Edit/EditSeriesModalConnector.js b/frontend/src/Series/Edit/EditSeriesModalConnector.js deleted file mode 100644 index ed1e5c6a6..000000000 --- a/frontend/src/Series/Edit/EditSeriesModalConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -EditSeriesModalConnector.propTypes = { - ...EditSeriesModal.propTypes, - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(undefined, mapDispatchToProps)(EditSeriesModalConnector); diff --git a/frontend/src/Series/Edit/EditSeriesModalContent.js b/frontend/src/Series/Edit/EditSeriesModalContent.js deleted file mode 100644 index 537d8990b..000000000 --- a/frontend/src/Series/Edit/EditSeriesModalContent.js +++ /dev/null @@ -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 ( - - - {translate('EditSeriesModalHeader', { title })} - - - -
- - {translate('Monitored')} - - - - - - - {translate('MonitorNewSeasons')} - - } - title={translate('MonitorNewSeasons')} - body={} - position={tooltipPositions.RIGHT} - /> - - - - - - - {translate('UseSeasonFolder')} - - - - - - {translate('QualityProfile')} - - - - - - {translate('SeriesType')} - - - - - - {translate('Path')} - - - - - - {translate('Tags')} - - - -
-
- - - - - - - - {translate('Save')} - - - - -
- ); - } -} - -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; diff --git a/frontend/src/Series/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Edit/EditSeriesModalContent.tsx new file mode 100644 index 000000000..f1a7ffca4 --- /dev/null +++ b/frontend/src/Series/Edit/EditSeriesModalContent.tsx @@ -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 ( + + {translate('EditSeriesModalHeader', { title })} + + +
+ + {translate('Monitored')} + + + + + + + {translate('MonitorNewSeasons')} + } + title={translate('MonitorNewSeasons')} + body={} + position={tooltipPositions.RIGHT} + /> + + + + + + + {translate('UseSeasonFolder')} + + + + + + {translate('QualityProfile')} + + + + + + {translate('SeriesType')} + + + + + + {translate('Path')} + + + + + + {translate('Tags')} + + + +
+
+ + + + + + + + {translate('Save')} + + + + +
+ ); +} + +export default EditSeriesModalContent; diff --git a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx index 5be820f87..dc2312193 100644 --- a/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx +++ b/frontend/src/Series/Index/Overview/SeriesIndexOverview.tsx @@ -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) { - - i.id === id) + ? items.find((i) => i.id === id)! : newImportListExclusion; const settings = selectSettings(mapping, pendingChanges, saveError); diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index 930064974..36ad57c47 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -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( + mapping, + pendingChanges, + saveError + ); return { id, @@ -50,7 +55,7 @@ function createReleaseProfileSelector(id?: number) { error, isSaving, saveError, - item: settings.settings as PendingSection, + item: settings.settings, ...settings, }; } diff --git a/frontend/src/Store/Selectors/selectSettings.js b/frontend/src/Store/Selectors/selectSettings.js deleted file mode 100644 index e3db2bf9d..000000000 --- a/frontend/src/Store/Selectors/selectSettings.js +++ /dev/null @@ -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; diff --git a/frontend/src/Store/Selectors/selectSettings.ts b/frontend/src/Store/Selectors/selectSettings.ts new file mode 100644 index 000000000..b7b6ab8c7 --- /dev/null +++ b/frontend/src/Store/Selectors/selectSettings.ts @@ -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( + item: T, + pendingChanges: Partial, + 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, key) => { + if (key === 'fields') { + return acc; + } + + // Return a flattened value + if (key === 'implementationName') { + // acc.implementationName = item[key]; + + return acc; + } + + const setting: Pending = { + 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 + ); + + if ('fields' in item) { + const fields = + (item.fields as Field[]).reduce((acc: PendingField[], f) => { + const field: PendingField = 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; diff --git a/frontend/src/Utilities/Object/getErrorMessage.ts b/frontend/src/Utilities/Object/getErrorMessage.ts index d757ceec3..72474b853 100644 --- a/frontend/src/Utilities/Object/getErrorMessage.ts +++ b/frontend/src/Utilities/Object/getErrorMessage.ts @@ -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; diff --git a/frontend/src/typings/Field.ts b/frontend/src/typings/Field.ts index 4ebb05278..24a0b35ac 100644 --- a/frontend/src/typings/Field.ts +++ b/frontend/src/typings/Field.ts @@ -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; diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index b84a60ada..13c2123cc 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -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 { value: T; - errors: ValidationError[]; - warnings: ValidationWarning[]; + errors: Failure[]; + warnings: Failure[]; + pending?: boolean; + previousValue?: T; } -export type PendingSection = { - [K in keyof T]: Pending; +export interface PendingField + extends Field, + Omit, 'previousValue' | 'value'> { + previousValue?: Field['value']; +} + +// export type PendingSection = { +// [K in keyof T]: Pending; +// }; + +type Mapped = { + [Prop in keyof T]: { + value: T[Prop]; + errors: Failure[]; + warnings: Failure[]; + pending?: boolean; + previousValue?: T[Prop]; + }; +}; + +export type PendingSection = Mapped & { + fields?: PendingField[]; };