From 1f5a84d20240dbb845716ca77145592f09607499 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 11 Aug 2024 12:33:01 +0300 Subject: [PATCH] Convert Import List Options to TypeScript Co-authored-by: The Dark <12370876+CheAle14@users.noreply.github.com> --- frontend/src/App/State/SettingsAppState.ts | 7 + .../ImportLists/ImportListSettings.js | 11 +- .../ImportLists/Options/ImportListOptions.js | 80 ----------- .../ImportLists/Options/ImportListOptions.tsx | 130 ++++++++++++++++++ .../Options/ImportListOptionsConnector.js | 101 -------------- .../createSettingsSectionSelector.js | 32 ----- .../createSettingsSectionSelector.ts | 49 +++++++ .../src/typings/ImportListOptionsSettings.ts | 10 ++ frontend/src/typings/pending.ts | 23 ++++ 9 files changed, 224 insertions(+), 219 deletions(-) delete mode 100644 frontend/src/Settings/ImportLists/Options/ImportListOptions.js create mode 100644 frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx delete mode 100644 frontend/src/Settings/ImportLists/Options/ImportListOptionsConnector.js delete mode 100644 frontend/src/Store/Selectors/createSettingsSectionSelector.js create mode 100644 frontend/src/Store/Selectors/createSettingsSectionSelector.ts create mode 100644 frontend/src/typings/ImportListOptionsSettings.ts create mode 100644 frontend/src/typings/pending.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 49bba4334..a0bea0973 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -7,6 +7,7 @@ import AppSectionState, { import Language from 'Language/Language'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; +import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; @@ -36,12 +37,18 @@ export interface QualityProfilesAppState extends AppSectionState, AppSectionSchemaState {} +export interface ImportListOptionsSettingsAppState + extends AppSectionItemState, + AppSectionSaveState {} + export type IndexerFlagSettingsAppState = AppSectionState; export type LanguageSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionItemState; interface SettingsAppState { + advancedSettings: boolean; downloadClients: DownloadClientAppState; + importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index 4244ba083..f7dabfc6b 100644 --- a/frontend/src/Settings/ImportLists/ImportListSettings.js +++ b/frontend/src/Settings/ImportLists/ImportListSettings.js @@ -10,7 +10,7 @@ import translate from 'Utilities/String/translate'; import ImportListExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector'; import ImportListsConnector from './ImportLists/ImportListsConnector'; import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal'; -import ImportListOptionsConnector from './Options/ImportListOptionsConnector'; +import ImportListOptions from './Options/ImportListOptions'; class ImportListSettings extends Component { @@ -32,7 +32,7 @@ class ImportListSettings extends Component { // // Listeners - onChildMounted = (saveCallback) => { + setChildSave = (saveCallback) => { this._saveCallback = saveCallback; }; @@ -54,8 +54,8 @@ class ImportListSettings extends Component { } }; - // Render // + // Render render() { const { @@ -98,8 +98,8 @@ class ImportListSettings extends Component { - @@ -109,7 +109,6 @@ class ImportListSettings extends Component { isOpen={isManageImportListsOpen} onModalClose={this.onManageImportListsModalClose} /> - ); diff --git a/frontend/src/Settings/ImportLists/Options/ImportListOptions.js b/frontend/src/Settings/ImportLists/Options/ImportListOptions.js deleted file mode 100644 index dcd963386..000000000 --- a/frontend/src/Settings/ImportLists/Options/ImportListOptions.js +++ /dev/null @@ -1,80 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import { inputTypes, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; - -function ImportListOptions(props) { - const { - advancedSettings, - isFetching, - error, - settings, - hasSettings, - onInputChange - } = props; - - const cleanLibraryLevelOptions = [ - { key: 'disabled', value: translate('Disabled') }, - { key: 'logOnly', value: translate('LogOnly') }, - { key: 'keepAndUnmonitor', value: translate('KeepAndUnmonitorMovie') }, - { key: 'removeAndKeep', value: translate('RemoveMovieAndKeepFiles') }, - { key: 'removeAndDelete', value: translate('RemoveMovieAndDeleteFiles') } - ]; - - return ( - advancedSettings && -
- { - isFetching && - - } - - { - !isFetching && error && - - {translate('ListOptionsLoadError')} - - } - - { - hasSettings && !isFetching && !error && -
- - {translate('CleanLibraryLevel')} - - - -
- } -
- ); -} - -ImportListOptions.propTypes = { - advancedSettings: PropTypes.bool.isRequired, - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - settings: PropTypes.object.isRequired, - hasSettings: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired -}; - -export default ImportListOptions; diff --git a/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx new file mode 100644 index 000000000..b7acf2504 --- /dev/null +++ b/frontend/src/Settings/ImportLists/Options/ImportListOptions.tsx @@ -0,0 +1,130 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; +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 LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchImportListOptions, + saveImportListOptions, + setImportListOptionsValue, +} from 'Store/Actions/settingsActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import translate from 'Utilities/String/translate'; + +const SECTION = 'importListOptions'; +const cleanLibraryLevelOptions = [ + { key: 'disabled', value: () => translate('Disabled') }, + { key: 'logOnly', value: () => translate('LogOnly') }, + { key: 'keepAndUnmonitor', value: () => translate('KeepAndUnmonitorMovie') }, + { key: 'removeAndKeep', value: () => translate('RemoveMovieAndKeepFiles') }, + { + key: 'removeAndDelete', + value: () => translate('RemoveMovieAndDeleteFiles'), + }, +]; + +function createImportListOptionsSelector() { + return createSelector( + (state: AppState) => state.settings.advancedSettings, + createSettingsSectionSelector(SECTION), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + save: sectionSettings.isSaving, + ...sectionSettings, + }; + } + ); +} + +interface ImportListOptionsPageProps { + setChildSave(saveCallback: () => void): void; + onChildStateChange(payload: unknown): void; +} + +function ImportListOptions(props: ImportListOptionsPageProps) { + const { setChildSave, onChildStateChange } = props; + + const { + isSaving, + hasPendingChanges, + advancedSettings, + isFetching, + error, + settings, + hasSettings, + } = useSelector(createImportListOptionsSelector()); + + const { listSyncLevel } = settings; + + const dispatch = useDispatch(); + + const onInputChange = useCallback( + ({ name, value }: { name: string; value: unknown }) => { + // @ts-expect-error 'setImportListOptionsValue' isn't typed yet + dispatch(setImportListOptionsValue({ name, value })); + }, + [dispatch] + ); + + useEffect(() => { + dispatch(fetchImportListOptions()); + setChildSave(() => dispatch(saveImportListOptions())); + + return () => { + dispatch(clearPendingChanges({ section: SECTION })); + }; + }, [dispatch, setChildSave]); + + useEffect(() => { + onChildStateChange({ + isSaving, + hasPendingChanges, + }); + }, [onChildStateChange, isSaving, hasPendingChanges]); + + const translatedLevelOptions = cleanLibraryLevelOptions.map( + ({ key, value }) => { + return { + key, + value: value(), + }; + } + ); + + return advancedSettings ? ( +
+ {isFetching ? : null} + + {!isFetching && error ? ( + {translate('ListOptionsLoadError')} + ) : null} + + {hasSettings && !isFetching && !error ? ( +
+ + {translate('CleanLibraryLevel')} + + +
+ ) : null} +
+ ) : null; +} + +export default ImportListOptions; diff --git a/frontend/src/Settings/ImportLists/Options/ImportListOptionsConnector.js b/frontend/src/Settings/ImportLists/Options/ImportListOptionsConnector.js deleted file mode 100644 index a5cc3ff66..000000000 --- a/frontend/src/Settings/ImportLists/Options/ImportListOptionsConnector.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { fetchImportListOptions, saveImportListOptions, setImportListOptionsValue } from 'Store/Actions/settingsActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import ImportListOptions from './ImportListOptions'; - -const SECTION = 'importListOptions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - createSettingsSectionSelector(SECTION), - (advancedSettings, sectionSettings) => { - return { - advancedSettings, - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchImportListOptions: fetchImportListOptions, - dispatchSetImportListOptionsValue: setImportListOptionsValue, - dispatchSaveImportListOptions: saveImportListOptions, - dispatchClearPendingChanges: clearPendingChanges -}; - -class ImportListOptionsConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - dispatchFetchImportListOptions, - dispatchSaveImportListOptions, - onChildMounted - } = this.props; - - dispatchFetchImportListOptions(); - onChildMounted(dispatchSaveImportListOptions); - } - - componentDidUpdate(prevProps) { - const { - hasPendingChanges, - isSaving, - onChildStateChange - } = this.props; - - if ( - prevProps.isSaving !== isSaving || - prevProps.hasPendingChanges !== hasPendingChanges - ) { - onChildStateChange({ - isSaving, - hasPendingChanges - }); - } - } - - componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: SECTION }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetImportListOptionsValue({ name, value }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -ImportListOptionsConnector.propTypes = { - isSaving: PropTypes.bool.isRequired, - hasPendingChanges: PropTypes.bool.isRequired, - dispatchFetchImportListOptions: PropTypes.func.isRequired, - dispatchSetImportListOptionsValue: PropTypes.func.isRequired, - dispatchSaveImportListOptions: PropTypes.func.isRequired, - dispatchClearPendingChanges: PropTypes.func.isRequired, - onChildMounted: PropTypes.func.isRequired, - onChildStateChange: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ImportListOptionsConnector); diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.js b/frontend/src/Store/Selectors/createSettingsSectionSelector.js deleted file mode 100644 index a9f6cbff6..000000000 --- a/frontend/src/Store/Selectors/createSettingsSectionSelector.js +++ /dev/null @@ -1,32 +0,0 @@ -import { createSelector } from 'reselect'; -import selectSettings from 'Store/Selectors/selectSettings'; - -function createSettingsSectionSelector(section) { - return createSelector( - (state) => state.settings[section], - (sectionSettings) => { - const { - isFetching, - isPopulated, - error, - item, - pendingChanges, - isSaving, - saveError - } = sectionSettings; - - const settings = selectSettings(item, pendingChanges, saveError); - - return { - isFetching, - isPopulated, - error, - isSaving, - saveError, - ...settings - }; - } - ); -} - -export default createSettingsSectionSelector; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.ts b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts new file mode 100644 index 000000000..f43e4e59b --- /dev/null +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts @@ -0,0 +1,49 @@ +import { createSelector } from 'reselect'; +import AppSectionState, { + AppSectionItemState, +} from 'App/State/AppSectionState'; +import AppState from 'App/State/AppState'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { PendingSection } from 'typings/pending'; + +type SettingNames = keyof Omit; +type GetSectionState = AppState['settings'][Name]; +type GetSettingsSectionItemType = + GetSectionState extends AppSectionItemState + ? R + : GetSectionState extends AppSectionState + ? R + : never; + +type AppStateWithPending = { + item?: GetSettingsSectionItemType; + pendingChanges?: Partial>; + saveError?: Error; +} & GetSectionState; + +function createSettingsSectionSelector( + section: Name +) { + return createSelector( + (state: AppState) => state.settings[section], + (sectionSettings) => { + const { item, pendingChanges, saveError, ...other } = + sectionSettings as AppStateWithPending; + + const { settings, ...rest } = selectSettings( + item, + pendingChanges, + saveError + ); + + return { + ...other, + saveError, + settings: settings as PendingSection>, + ...rest, + }; + } + ); +} + +export default createSettingsSectionSelector; diff --git a/frontend/src/typings/ImportListOptionsSettings.ts b/frontend/src/typings/ImportListOptionsSettings.ts new file mode 100644 index 000000000..ece527417 --- /dev/null +++ b/frontend/src/typings/ImportListOptionsSettings.ts @@ -0,0 +1,10 @@ +export type ListSyncLevel = + | 'disabled' + | 'logOnly' + | 'keepAndUnmonitor' + | 'removeAndKeep' + | 'removeAndDelete'; + +export default interface ImportListOptionsSettings { + listSyncLevel: ListSyncLevel; +} diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts new file mode 100644 index 000000000..5cdcbc003 --- /dev/null +++ b/frontend/src/typings/pending.ts @@ -0,0 +1,23 @@ +export interface ValidationFailure { + propertyName: string; + errorMessage: string; + severity: 'error' | 'warning'; +} + +export interface ValidationError extends ValidationFailure { + isWarning: false; +} + +export interface ValidationWarning extends ValidationFailure { + isWarning: true; +} + +export interface Pending { + value: T; + errors: ValidationError[]; + warnings: ValidationWarning[]; +} + +export type PendingSection = { + [K in keyof T]: Pending; +};