diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 8f51882c6..9c6045938 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -15,13 +15,18 @@ import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; -import { UiSettings } from 'typings/UiSettings'; +import General from 'typings/Settings/General'; +import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, AppSectionSaveState {} +export interface GeneralAppState + extends AppSectionItemState, + AppSectionSaveState {} + export interface ImportListAppState extends AppSectionState, AppSectionDeleteState, @@ -65,6 +70,7 @@ interface SettingsAppState { advancedSettings: boolean; customFormats: CustomFormatAppState; downloadClients: DownloadClientAppState; + general: GeneralAppState; importListExclusions: ImportListExclusionsSettingsAppState; importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.js b/frontend/src/FirstRun/AuthenticationRequiredModal.js deleted file mode 100644 index caa855cb7..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModal.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModal(props) { - const { - isOpen - } = props; - - return ( - - - - ); -} - -AuthenticationRequiredModal.propTypes = { - isOpen: PropTypes.bool.isRequired -}; - -export default AuthenticationRequiredModal; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModal.tsx b/frontend/src/FirstRun/AuthenticationRequiredModal.tsx new file mode 100644 index 000000000..1b4b519ce --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; + +function onModalClose() { + // No-op +} + +interface AuthenticationRequiredModalProps { + isOpen: boolean; +} + +export default function AuthenticationRequiredModal({ + isOpen, +}: AuthenticationRequiredModalProps) { + return ( + + + + ); +} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js deleted file mode 100644 index aa8e51a1e..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js +++ /dev/null @@ -1,170 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useEffect, useRef } from 'react'; -import Alert from 'Components/Alert'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -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 { inputTypes, kinds } from 'Helpers/Props'; -import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings'; -import translate from 'Utilities/String/translate'; -import styles from './AuthenticationRequiredModalContent.css'; - -function onModalClose() { - // No-op -} - -function AuthenticationRequiredModalContent(props) { - const { - isPopulated, - error, - isSaving, - settings, - onInputChange, - onSavePress, - dispatchFetchStatus - } = props; - - const { - authenticationMethod, - authenticationRequired, - username, - password, - passwordConfirmation - } = settings; - - const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; - - const didMount = useRef(false); - - useEffect(() => { - if (!isSaving && didMount.current) { - dispatchFetchStatus(); - } - - didMount.current = true; - }, [isSaving, dispatchFetchStatus]); - - return ( - - - {translate('AuthenticationRequired')} - - - - - {translate('AuthenticationRequiredWarning')} - - - { - isPopulated && !error ? -
- - {translate('AuthenticationMethod')} - - - - - - {translate('AuthenticationRequired')} - - - - - - {translate('Username')} - - - - - - {translate('Password')} - - - - - - {translate('PasswordConfirmation')} - - - -
: - null - } - - { - !isPopulated && !error ? : null - } -
- - - - {translate('Save')} - - -
- ); -} - -AuthenticationRequiredModalContent.propTypes = { - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - settings: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default AuthenticationRequiredModalContent; diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx new file mode 100644 index 000000000..09f66d381 --- /dev/null +++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.tsx @@ -0,0 +1,194 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +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 usePrevious from 'Helpers/Hooks/usePrevious'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { + authenticationMethodOptions, + authenticationRequiredOptions, +} from 'Settings/General/SecuritySettings'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchGeneralSettings, + saveGeneralSettings, + setGeneralSettingsValue, +} from 'Store/Actions/settingsActions'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './AuthenticationRequiredModalContent.css'; + +const SECTION = 'general'; + +const selector = createSettingsSectionSelector(SECTION); + +function onModalClose() { + // No-op +} + +export default function AuthenticationRequiredModalContent() { + const { isPopulated, error, isSaving, settings } = useSelector(selector); + const dispatch = useDispatch(); + + const { + authenticationMethod, + authenticationRequired, + username, + password, + passwordConfirmation, + } = settings; + + const wasSaving = usePrevious(isSaving); + + useEffect(() => { + dispatch(fetchGeneralSettings()); + + return () => { + dispatch(clearPendingChanges()); + }; + }, [dispatch]); + + const onInputChange = useCallback( + (args: InputChanged) => { + // @ts-expect-error Actions aren't typed + dispatch(setGeneralSettingsValue(args)); + }, + [dispatch] + ); + + const authenticationEnabled = + authenticationMethod && authenticationMethod.value !== 'none'; + + useEffect(() => { + if (isSaving || !wasSaving) { + return; + } + + dispatch(fetchStatus()); + }, [isSaving, wasSaving, dispatch]); + + const onPress = useCallback(() => { + dispatch(saveGeneralSettings()); + }, [dispatch]); + + return ( + + {translate('AuthenticationRequired')} + + + + {translate('AuthenticationRequiredWarning')} + + + {isPopulated && !error ? ( +
+ + {translate('AuthenticationMethod')} + + + + + + {translate('AuthenticationRequired')} + + + + + + {translate('Username')} + + + + + + {translate('Password')} + + + + + + {translate('PasswordConfirmation')} + + + +
+ ) : null} + + {!isPopulated && !error ? : null} +
+ + + + {translate('Save')} + + +
+ ); +} diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js b/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js deleted file mode 100644 index 6653a9d34..000000000 --- a/frontend/src/FirstRun/AuthenticationRequiredModalContentConnector.js +++ /dev/null @@ -1,86 +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 { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions'; -import { fetchStatus } from 'Store/Actions/systemActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent'; - -const SECTION = 'general'; - -function createMapStateToProps() { - return createSelector( - createSettingsSectionSelector(SECTION), - (sectionSettings) => { - return { - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchClearPendingChanges: clearPendingChanges, - dispatchSetGeneralSettingsValue: setGeneralSettingsValue, - dispatchSaveGeneralSettings: saveGeneralSettings, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchFetchStatus: fetchStatus -}; - -class AuthenticationRequiredModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchGeneralSettings(); - } - - componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetGeneralSettingsValue({ name, value }); - }; - - onSavePress = () => { - this.props.dispatchSaveGeneralSettings(); - }; - - // - // Render - - render() { - const { - dispatchClearPendingChanges, - dispatchFetchGeneralSettings, - dispatchSetGeneralSettingsValue, - dispatchSaveGeneralSettings, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -AuthenticationRequiredModalContentConnector.propTypes = { - dispatchClearPendingChanges: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchSetGeneralSettingsValue: PropTypes.func.isRequired, - dispatchSaveGeneralSettings: PropTypes.func.isRequired, - dispatchFetchStatus: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector); diff --git a/frontend/src/Movie/Index/Overview/MovieIndexOverviewInfo.tsx b/frontend/src/Movie/Index/Overview/MovieIndexOverviewInfo.tsx index b60825b4c..5d5f06ae6 100644 --- a/frontend/src/Movie/Index/Overview/MovieIndexOverviewInfo.tsx +++ b/frontend/src/Movie/Index/Overview/MovieIndexOverviewInfo.tsx @@ -5,7 +5,7 @@ import { icons } from 'Helpers/Props'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import dimensions from 'Styles/Variables/dimensions'; import QualityProfile from 'typings/QualityProfile'; -import { UiSettings } from 'typings/UiSettings'; +import UiSettings from 'typings/Settings/UiSettings'; import formatDateTime from 'Utilities/Date/formatDateTime'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import formatBytes from 'Utilities/Number/formatBytes'; diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts new file mode 100644 index 000000000..c867bed74 --- /dev/null +++ b/frontend/src/typings/Settings/General.ts @@ -0,0 +1,45 @@ +export type UpdateMechanism = + | 'builtIn' + | 'script' + | 'external' + | 'apt' + | 'docker'; + +export default interface General { + bindAddress: string; + port: number; + sslPort: number; + enableSsl: boolean; + launchBrowser: boolean; + authenticationMethod: string; + authenticationRequired: string; + analyticsEnabled: boolean; + username: string; + password: string; + passwordConfirmation: string; + logLevel: string; + consoleLogLevel: string; + branch: string; + apiKey: string; + sslCertPath: string; + sslCertPassword: string; + urlBase: string; + instanceName: string; + applicationUrl: string; + updateAutomatically: boolean; + updateMechanism: UpdateMechanism; + updateScriptPath: string; + proxyEnabled: boolean; + proxyType: string; + proxyHostname: string; + proxyPort: number; + proxyUsername: string; + proxyPassword: string; + proxyBypassFilter: string; + proxyBypassLocalAddresses: boolean; + certificateValidation: string; + backupFolder: string; + backupInterval: number; + backupRetention: number; + id: number; +} diff --git a/frontend/src/typings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts similarity index 85% rename from frontend/src/typings/UiSettings.ts rename to frontend/src/typings/Settings/UiSettings.ts index 97f79f849..8e5765a4d 100644 --- a/frontend/src/typings/UiSettings.ts +++ b/frontend/src/typings/Settings/UiSettings.ts @@ -1,4 +1,4 @@ -export interface UiSettings { +export default interface UiSettings { theme: 'auto' | 'dark' | 'light'; showRelativeDates: boolean; shortDateFormat: string; diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts index c0fda305c..cf91149b6 100644 --- a/frontend/src/typings/inputs.ts +++ b/frontend/src/typings/inputs.ts @@ -1,4 +1,6 @@ -export type CheckInputChanged = { +export type InputChanged = { name: string; - value: boolean; + value: T; }; + +export type CheckInputChanged = InputChanged;