1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-11-19 17:32:38 +01:00

Convert Import List Options to TypeScript

Co-authored-by: The Dark <12370876+CheAle14@users.noreply.github.com>
This commit is contained in:
Bogdan 2024-08-11 12:33:01 +03:00 committed by Qstick
parent d25bcdb043
commit 1f5a84d202
9 changed files with 224 additions and 219 deletions

View File

@ -7,6 +7,7 @@ import AppSectionState, {
import Language from 'Language/Language'; import Language from 'Language/Language';
import DownloadClient from 'typings/DownloadClient'; import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList'; import ImportList from 'typings/ImportList';
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import Indexer from 'typings/Indexer'; import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag'; import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
@ -36,12 +37,18 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>, extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {} AppSectionSchemaState<QualityProfile> {}
export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>; export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type LanguageSettingsAppState = AppSectionState<Language>; export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>; export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState { interface SettingsAppState {
advancedSettings: boolean;
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState; importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState; indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState; indexers: IndexerAppState;

View File

@ -10,7 +10,7 @@ import translate from 'Utilities/String/translate';
import ImportListExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector'; import ImportListExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
import ImportListsConnector from './ImportLists/ImportListsConnector'; import ImportListsConnector from './ImportLists/ImportListsConnector';
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal'; import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
import ImportListOptionsConnector from './Options/ImportListOptionsConnector'; import ImportListOptions from './Options/ImportListOptions';
class ImportListSettings extends Component { class ImportListSettings extends Component {
@ -32,7 +32,7 @@ class ImportListSettings extends Component {
// //
// Listeners // Listeners
onChildMounted = (saveCallback) => { setChildSave = (saveCallback) => {
this._saveCallback = saveCallback; this._saveCallback = saveCallback;
}; };
@ -54,8 +54,8 @@ class ImportListSettings extends Component {
} }
}; };
// Render
// //
// Render
render() { render() {
const { const {
@ -98,8 +98,8 @@ class ImportListSettings extends Component {
<PageContentBody> <PageContentBody>
<ImportListsConnector /> <ImportListsConnector />
<ImportListOptionsConnector <ImportListOptions
onChildMounted={this.onChildMounted} setChildSave={this.setChildSave}
onChildStateChange={this.onChildStateChange} onChildStateChange={this.onChildStateChange}
/> />
@ -109,7 +109,6 @@ class ImportListSettings extends Component {
isOpen={isManageImportListsOpen} isOpen={isManageImportListsOpen}
onModalClose={this.onManageImportListsModalClose} onModalClose={this.onManageImportListsModalClose}
/> />
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );

View File

@ -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 &&
<FieldSet legend={translate('Options')}>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('ListOptionsLoadError')}
</Alert>
}
{
hasSettings && !isFetching && !error &&
<Form>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('CleanLibraryLevel')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="listSyncLevel"
values={cleanLibraryLevelOptions}
helpText={translate('ListSyncLevelHelpText')}
helpTextWarning={settings.listSyncLevel.value === 'removeAndDelete' ? translate('ListSyncLevelHelpTextWarning') : undefined}
onChange={onInputChange}
{...settings.listSyncLevel}
/>
</FormGroup>
</Form>
}
</FieldSet>
);
}
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;

View File

@ -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 ? (
<FieldSet legend={translate('Options')}>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('ListOptionsLoadError')}</Alert>
) : null}
{hasSettings && !isFetching && !error ? (
<Form>
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
<FormLabel>{translate('CleanLibraryLevel')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="listSyncLevel"
values={translatedLevelOptions}
helpText={translate('ListSyncLevelHelpText')}
onChange={onInputChange}
{...listSyncLevel}
/>
</FormGroup>
</Form>
) : null}
</FieldSet>
) : null;
}
export default ImportListOptions;

View File

@ -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 (
<ImportListOptions
onInputChange={this.onInputChange}
{...this.props}
/>
);
}
}
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);

View File

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

View File

@ -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<AppState['settings'], 'advancedSettings'>;
type GetSectionState<Name extends SettingNames> = AppState['settings'][Name];
type GetSettingsSectionItemType<Name extends SettingNames> =
GetSectionState<Name> extends AppSectionItemState<infer R>
? R
: GetSectionState<Name> extends AppSectionState<infer R>
? R
: never;
type AppStateWithPending<Name extends SettingNames> = {
item?: GetSettingsSectionItemType<Name>;
pendingChanges?: Partial<GetSettingsSectionItemType<Name>>;
saveError?: Error;
} & GetSectionState<Name>;
function createSettingsSectionSelector<Name extends SettingNames>(
section: Name
) {
return createSelector(
(state: AppState) => state.settings[section],
(sectionSettings) => {
const { item, pendingChanges, saveError, ...other } =
sectionSettings as AppStateWithPending<Name>;
const { settings, ...rest } = selectSettings(
item,
pendingChanges,
saveError
);
return {
...other,
saveError,
settings: settings as PendingSection<GetSettingsSectionItemType<Name>>,
...rest,
};
}
);
}
export default createSettingsSectionSelector;

View File

@ -0,0 +1,10 @@
export type ListSyncLevel =
| 'disabled'
| 'logOnly'
| 'keepAndUnmonitor'
| 'removeAndKeep'
| 'removeAndDelete';
export default interface ImportListOptionsSettings {
listSyncLevel: ListSyncLevel;
}

View File

@ -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<T> {
value: T;
errors: ValidationError[];
warnings: ValidationWarning[];
}
export type PendingSection<T> = {
[K in keyof T]: Pending<T[K]>;
};