diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx
index 853bd4590..058330897 100644
--- a/frontend/src/App/AppRoutes.tsx
+++ b/frontend/src/App/AppRoutes.tsx
@@ -15,7 +15,7 @@ import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadCl
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
-import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
+import MediaManagement from 'Settings/MediaManagement/MediaManagement';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import MetadataSourceSettings from 'Settings/MetadataSource/MetadataSourceSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
@@ -98,10 +98,7 @@ function AppRoutes() {
-
+
diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts
index ab046fba9..7016182f2 100644
--- a/frontend/src/App/State/SettingsAppState.ts
+++ b/frontend/src/App/State/SettingsAppState.ts
@@ -21,6 +21,7 @@ import Notification from 'typings/Notification';
import QualityDefinition from 'typings/QualityDefinition';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
+import MediaManagement from 'typings/Settings/MediaManagement';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
@@ -54,6 +55,10 @@ export interface GeneralAppState
extends AppSectionItemState,
AppSectionSaveState {}
+export interface MediaManagementAppState
+ extends AppSectionItemState,
+ AppSectionSaveState {}
+
export interface NamingAppState
extends AppSectionItemState,
AppSectionSaveState {}
@@ -131,6 +136,7 @@ interface SettingsAppState {
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
+ mediaManagement: MediaManagementAppState;
metadata: MetadataAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;
diff --git a/frontend/src/Helpers/Hooks/useIsWindows.ts b/frontend/src/Helpers/Hooks/useIsWindows.ts
new file mode 100644
index 000000000..c4729c4c0
--- /dev/null
+++ b/frontend/src/Helpers/Hooks/useIsWindows.ts
@@ -0,0 +1,8 @@
+import { useSelector } from 'react-redux';
+import AppState from 'App/State/AppState';
+
+function useIsWindows() {
+ return useSelector((state: AppState) => state.system.status.item.isWindows);
+}
+
+export default useIsWindows;
diff --git a/frontend/src/Helpers/elementChildren.js b/frontend/src/Helpers/elementChildren.js
deleted file mode 100644
index 1c10b2f0e..000000000
--- a/frontend/src/Helpers/elementChildren.js
+++ /dev/null
@@ -1,149 +0,0 @@
-// https://github.com/react-bootstrap/react-element-children
-
-import React from 'react';
-
-/**
- * Iterates through children that are typically specified as `props.children`,
- * but only maps over children that are "valid components".
- *
- * The mapFunction provided index will be normalised to the components mapped,
- * so an invalid component would not increase the index.
- *
- * @param {?*} children Children tree container.
- * @param {function(*, int)} func.
- * @param {*} context Context for func.
- * @return {object} Object containing the ordered map of results.
- */
-export function map(children, func, context) {
- let index = 0;
-
- return React.Children.map(children, (child) => {
- if (!React.isValidElement(child)) {
- return child;
- }
-
- return func.call(context, child, index++);
- });
-}
-
-/**
- * Iterates through children that are "valid components".
- *
- * The provided forEachFunc(child, index) will be called for each
- * leaf child with the index reflecting the position relative to "valid components".
- *
- * @param {?*} children Children tree container.
- * @param {function(*, int)} func.
- * @param {*} context Context for context.
- */
-export function forEach(children, func, context) {
- let index = 0;
-
- React.Children.forEach(children, (child) => {
- if (!React.isValidElement(child)) {
- return;
- }
-
- func.call(context, child, index++);
- });
-}
-
-/**
- * Count the number of "valid components" in the Children container.
- *
- * @param {?*} children Children tree container.
- * @returns {number}
- */
-export function count(children) {
- let result = 0;
-
- React.Children.forEach(children, (child) => {
- if (!React.isValidElement(child)) {
- return;
- }
-
- ++result;
- });
-
- return result;
-}
-
-/**
- * Finds children that are typically specified as `props.children`,
- * but only iterates over children that are "valid components".
- *
- * The provided forEachFunc(child, index) will be called for each
- * leaf child with the index reflecting the position relative to "valid components".
- *
- * @param {?*} children Children tree container.
- * @param {function(*, int)} func.
- * @param {*} context Context for func.
- * @returns {array} of children that meet the func return statement
- */
-export function filter(children, func, context) {
- const result = [];
-
- forEach(children, (child, index) => {
- if (func.call(context, child, index)) {
- result.push(child);
- }
- });
-
- return result;
-}
-
-export function find(children, func, context) {
- let result = null;
-
- forEach(children, (child, index) => {
- if (result) {
- return;
- }
- if (func.call(context, child, index)) {
- result = child;
- }
- });
-
- return result;
-}
-
-export function every(children, func, context) {
- let result = true;
-
- forEach(children, (child, index) => {
- if (!result) {
- return;
- }
- if (!func.call(context, child, index)) {
- result = false;
- }
- });
-
- return result;
-}
-
-export function some(children, func, context) {
- let result = false;
-
- forEach(children, (child, index) => {
- if (result) {
- return;
- }
-
- if (func.call(context, child, index)) {
- result = true;
- }
- });
-
- return result;
-}
-
-export function toArray(children) {
- const result = [];
-
- forEach(children, (child) => {
- result.push(child);
- });
-
- return result;
-}
diff --git a/frontend/src/Helpers/getDisplayName.js b/frontend/src/Helpers/getDisplayName.js
deleted file mode 100644
index 512702c87..000000000
--- a/frontend/src/Helpers/getDisplayName.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function getDisplayName(Component) {
- return Component.displayName || Component.name || 'Component';
-}
diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js
deleted file mode 100644
index d63a8a7d2..000000000
--- a/frontend/src/Settings/MediaManagement/MediaManagement.js
+++ /dev/null
@@ -1,536 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } 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 PageContent from 'Components/Page/PageContent';
-import PageContentBody from 'Components/Page/PageContentBody';
-import { inputTypes, kinds, sizes } from 'Helpers/Props';
-import RootFolders from 'RootFolder/RootFolders';
-import SettingsToolbar from 'Settings/SettingsToolbar';
-import translate from 'Utilities/String/translate';
-import Naming from './Naming/Naming';
-import AddRootFolder from './RootFolder/AddRootFolder';
-
-const episodeTitleRequiredOptions = [
- {
- key: 'always',
- get value() {
- return translate('Always');
- }
- },
- {
- key: 'bulkSeasonReleases',
- get value() {
- return translate('OnlyForBulkSeasonReleases');
- }
- },
- {
- key: 'never',
- get value() {
- return translate('Never');
- }
- }
-];
-
-const rescanAfterRefreshOptions = [
- {
- key: 'always',
- get value() {
- return translate('Always');
- }
- },
- {
- key: 'afterManual',
- get value() {
- return translate('AfterManualRefresh');
- }
- },
- {
- key: 'never',
- get value() {
- return translate('Never');
- }
- }
-];
-
-const downloadPropersAndRepacksOptions = [
- {
- key: 'preferAndUpgrade',
- get value() {
- return translate('PreferAndUpgrade');
- }
- },
- {
- key: 'doNotUpgrade',
- get value() {
- return translate('DoNotUpgradeAutomatically');
- }
- },
- {
- key: 'doNotPrefer',
- get value() {
- return translate('DoNotPrefer');
- }
- }
-];
-
-const fileDateOptions = [
- {
- key: 'none',
- get value() {
- return translate('None');
- }
- },
- {
- key: 'localAirDate',
- get value() {
- return translate('LocalAirDate');
- }
- },
- {
- key: 'utcAirDate',
- get value() {
- return translate('UtcAirDate');
- }
- }
-];
-
-class MediaManagement extends Component {
-
- //
- // Render
-
- render() {
- const {
- advancedSettings,
- isFetching,
- error,
- settings,
- hasSettings,
- isWindows,
- onInputChange,
- onSavePress,
- ...otherProps
- } = this.props;
-
- return (
-
-
-
-
-
-
- {
- isFetching ?
- : null
- }
-
- {
- !isFetching && error ?
- : null
- }
-
- {
- hasSettings && !isFetching && !error ?
- : null
- }
-
-
-
-
- );
- }
-
-}
-
-MediaManagement.propTypes = {
- advancedSettings: PropTypes.bool.isRequired,
- isFetching: PropTypes.bool.isRequired,
- error: PropTypes.object,
- settings: PropTypes.object.isRequired,
- hasSettings: PropTypes.bool.isRequired,
- isWindows: PropTypes.bool.isRequired,
- onSavePress: PropTypes.func.isRequired,
- onInputChange: PropTypes.func.isRequired
-};
-
-export default MediaManagement;
diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.tsx b/frontend/src/Settings/MediaManagement/MediaManagement.tsx
new file mode 100644
index 000000000..0fce5a2f0
--- /dev/null
+++ b/frontend/src/Settings/MediaManagement/MediaManagement.tsx
@@ -0,0 +1,559 @@
+import React, { useCallback, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+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 PageContent from 'Components/Page/PageContent';
+import PageContentBody from 'Components/Page/PageContentBody';
+import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
+import { inputTypes, kinds, sizes } from 'Helpers/Props';
+import RootFolders from 'RootFolder/RootFolders';
+import SettingsToolbar from 'Settings/SettingsToolbar';
+import { clearPendingChanges } from 'Store/Actions/baseActions';
+import {
+ fetchMediaManagementSettings,
+ saveMediaManagementSettings,
+ saveNamingSettings,
+ setMediaManagementSettingsValue,
+} from 'Store/Actions/settingsActions';
+import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
+import useIsWindows from 'System/useIsWindows';
+import { InputChanged } from 'typings/inputs';
+import isEmpty from 'Utilities/Object/isEmpty';
+import translate from 'Utilities/String/translate';
+import Naming from './Naming/Naming';
+import AddRootFolder from './RootFolder/AddRootFolder';
+
+const SECTION = 'mediaManagement';
+
+const episodeTitleRequiredOptions = [
+ {
+ key: 'always',
+ get value() {
+ return translate('Always');
+ },
+ },
+ {
+ key: 'bulkSeasonReleases',
+ get value() {
+ return translate('OnlyForBulkSeasonReleases');
+ },
+ },
+ {
+ key: 'never',
+ get value() {
+ return translate('Never');
+ },
+ },
+];
+
+const rescanAfterRefreshOptions = [
+ {
+ key: 'always',
+ get value() {
+ return translate('Always');
+ },
+ },
+ {
+ key: 'afterManual',
+ get value() {
+ return translate('AfterManualRefresh');
+ },
+ },
+ {
+ key: 'never',
+ get value() {
+ return translate('Never');
+ },
+ },
+];
+
+const downloadPropersAndRepacksOptions = [
+ {
+ key: 'preferAndUpgrade',
+ get value() {
+ return translate('PreferAndUpgrade');
+ },
+ },
+ {
+ key: 'doNotUpgrade',
+ get value() {
+ return translate('DoNotUpgradeAutomatically');
+ },
+ },
+ {
+ key: 'doNotPrefer',
+ get value() {
+ return translate('DoNotPrefer');
+ },
+ },
+];
+
+const fileDateOptions = [
+ {
+ key: 'none',
+ get value() {
+ return translate('None');
+ },
+ },
+ {
+ key: 'localAirDate',
+ get value() {
+ return translate('LocalAirDate');
+ },
+ },
+ {
+ key: 'utcAirDate',
+ get value() {
+ return translate('UtcAirDate');
+ },
+ },
+];
+
+function MediaManagement() {
+ const dispatch = useDispatch();
+ const showAdvancedSettings = useShowAdvancedSettings();
+ const hasNamingPendingChanges = !isEmpty(
+ useSelector((state: AppState) => state.settings.naming.pendingChanges)
+ );
+ const isWindows = useIsWindows();
+ const {
+ isFetching,
+ isPopulated,
+ isSaving,
+ error,
+ settings,
+ hasSettings,
+ hasPendingChanges,
+ validationErrors,
+ validationWarnings,
+ } = useSelector(createSettingsSectionSelector(SECTION));
+
+ const handleSavePress = useCallback(() => {
+ dispatch(saveMediaManagementSettings());
+ dispatch(saveNamingSettings());
+ }, [dispatch]);
+
+ const handleInputChange = useCallback(
+ (change: InputChanged) => {
+ // @ts-expect-error - actions are not typed
+ dispatch(setMediaManagementSettingsValue(change));
+ },
+ [dispatch]
+ );
+
+ useEffect(() => {
+ dispatch(fetchMediaManagementSettings());
+
+ return () => {
+ dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
+ };
+ }, [dispatch]);
+
+ return (
+
+
+
+
+
+
+ {isFetching ? (
+
+ ) : null}
+
+ {!isFetching && error ? (
+
+ ) : null}
+
+ {hasSettings && isPopulated && !error ? (
+
+ ) : null}
+
+
+
+
+ );
+}
+
+export default MediaManagement;
diff --git a/frontend/src/Settings/MediaManagement/MediaManagementConnector.js b/frontend/src/Settings/MediaManagement/MediaManagementConnector.js
deleted file mode 100644
index 9d6f959b8..000000000
--- a/frontend/src/Settings/MediaManagement/MediaManagementConnector.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import _ from 'lodash';
-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 { fetchMediaManagementSettings, saveMediaManagementSettings, saveNamingSettings, setMediaManagementSettingsValue } from 'Store/Actions/settingsActions';
-import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
-import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
-import MediaManagement from './MediaManagement';
-
-const SECTION = 'mediaManagement';
-
-function createMapStateToProps() {
- return createSelector(
- (state) => state.settings.advancedSettings,
- (state) => state.settings.naming,
- createSettingsSectionSelector(SECTION),
- createSystemStatusSelector(),
- (advancedSettings, namingSettings, sectionSettings, systemStatus) => {
- return {
- advancedSettings,
- ...sectionSettings,
- hasPendingChanges: !_.isEmpty(namingSettings.pendingChanges) || sectionSettings.hasPendingChanges,
- isWindows: systemStatus.isWindows
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- fetchMediaManagementSettings,
- setMediaManagementSettingsValue,
- saveMediaManagementSettings,
- saveNamingSettings,
- clearPendingChanges
-};
-
-class MediaManagementConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- this.props.fetchMediaManagementSettings();
- }
-
- componentWillUnmount() {
- this.props.clearPendingChanges({ section: `settings.${SECTION}` });
- }
-
- //
- // Listeners
-
- onInputChange = ({ name, value }) => {
- this.props.setMediaManagementSettingsValue({ name, value });
- };
-
- onSavePress = () => {
- this.props.saveMediaManagementSettings();
- this.props.saveNamingSettings();
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-MediaManagementConnector.propTypes = {
- fetchMediaManagementSettings: PropTypes.func.isRequired,
- setMediaManagementSettingsValue: PropTypes.func.isRequired,
- saveMediaManagementSettings: PropTypes.func.isRequired,
- saveNamingSettings: PropTypes.func.isRequired,
- clearPendingChanges: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(MediaManagementConnector);
diff --git a/frontend/src/System/useIsWindows.ts b/frontend/src/System/useIsWindows.ts
new file mode 100644
index 000000000..c4729c4c0
--- /dev/null
+++ b/frontend/src/System/useIsWindows.ts
@@ -0,0 +1,8 @@
+import { useSelector } from 'react-redux';
+import AppState from 'App/State/AppState';
+
+function useIsWindows() {
+ return useSelector((state: AppState) => state.system.status.item.isWindows);
+}
+
+export default useIsWindows;
diff --git a/frontend/src/typings/Settings/MediaManagement.ts b/frontend/src/typings/Settings/MediaManagement.ts
new file mode 100644
index 000000000..ac589d3c9
--- /dev/null
+++ b/frontend/src/typings/Settings/MediaManagement.ts
@@ -0,0 +1,22 @@
+export default interface MediaManagement {
+ autoUnmonitorPreviouslyDownloadedEpisodes: boolean;
+ recycleBin: string;
+ recycleBinCleanupDays: number;
+ downloadPropersAndRepacks: string;
+ createEmptySeriesFolders: boolean;
+ deleteEmptyFolders: boolean;
+ fileDate: string;
+ rescanAfterRefresh: string;
+ setPermissionsLinux: boolean;
+ chmodFolder: string;
+ chownGroup: string;
+ episodeTitleRequired: string;
+ skipFreeSpaceCheckWhenImporting: boolean;
+ minimumFreeSpaceWhenImporting: number;
+ copyUsingHardlinks: boolean;
+ useScriptImport: boolean;
+ scriptImportPath: string;
+ importExtraFiles: boolean;
+ extraFileExtensions: string;
+ enableMediaInfo: boolean;
+}