diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index 058330897..eba2d60f1 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -14,7 +14,7 @@ import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSetting import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; -import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector'; +import IndexerSettings from 'Settings/Indexers/IndexerSettings'; import MediaManagement from 'Settings/MediaManagement/MediaManagement'; import MetadataSettings from 'Settings/Metadata/MetadataSettings'; import MetadataSourceSettings from 'Settings/MetadataSource/MetadataSourceSettings'; @@ -109,7 +109,7 @@ function AppRoutes() { component={CustomFormatSettingsPage} /> - + = T & { + presets: T[]; +}; + export interface AutoTaggingAppState extends AppSectionState, AppSectionDeleteState, @@ -70,10 +75,15 @@ export interface ImportListAppState AppSectionDeleteState, AppSectionSaveState {} +export interface IndexerOptionsAppState + extends AppSectionItemState, + AppSectionSaveState {} + export interface IndexerAppState extends AppSectionState, AppSectionDeleteState, - AppSectionSaveState { + AppSectionSaveState, + AppSectionSchemaState> { isTestingAll: boolean; } @@ -134,6 +144,7 @@ interface SettingsAppState { importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; + indexerOptions: IndexerOptionsAppState; indexers: IndexerAppState; languages: LanguageSettingsAppState; mediaManagement: MediaManagementAppState; diff --git a/frontend/src/Components/Form/FormInputGroup.tsx b/frontend/src/Components/Form/FormInputGroup.tsx index dca3ef2db..24d338448 100644 --- a/frontend/src/Components/Form/FormInputGroup.tsx +++ b/frontend/src/Components/Form/FormInputGroup.tsx @@ -1,5 +1,6 @@ import React, { FocusEvent, ReactNode } from 'react'; import Link from 'Components/Link/Link'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import { inputTypes } from 'Helpers/Props'; import { InputType } from 'Helpers/Props/inputTypes'; import { Kind } from 'Helpers/Props/kinds'; @@ -158,6 +159,7 @@ interface FormInputGroupProps { selectOptionsProviderAction?: string; indexerFlags?: number; pending?: boolean; + protocol?: DownloadProtocol; canEdit?: boolean; includeAny?: boolean; delimiters?: string[]; diff --git a/frontend/src/Settings/Indexers/IndexerSettings.js b/frontend/src/Settings/Indexers/IndexerSettings.js deleted file mode 100644 index 94e324c8e..000000000 --- a/frontend/src/Settings/Indexers/IndexerSettings.js +++ /dev/null @@ -1,120 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; -import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; -import { icons } from 'Helpers/Props'; -import SettingsToolbar from 'Settings/SettingsToolbar'; -import translate from 'Utilities/String/translate'; -import IndexersConnector from './Indexers/IndexersConnector'; -import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal'; -import IndexerOptionsConnector from './Options/IndexerOptionsConnector'; - -class IndexerSettings extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._saveCallback = null; - - this.state = { - isSaving: false, - hasPendingChanges: false, - isManageIndexersOpen: false - }; - } - - // - // Listeners - - onChildMounted = (saveCallback) => { - this._saveCallback = saveCallback; - }; - - onChildStateChange = (payload) => { - this.setState(payload); - }; - - onManageIndexersPress = () => { - this.setState({ isManageIndexersOpen: true }); - }; - - onManageIndexersModalClose = () => { - this.setState({ isManageIndexersOpen: false }); - }; - - onSavePress = () => { - if (this._saveCallback) { - this._saveCallback(); - } - }; - - // Render - // - - render() { - const { - isTestingAll, - dispatchTestAllIndexers - } = this.props; - - const { - isSaving, - hasPendingChanges, - isManageIndexersOpen - } = this.state; - - return ( - - - - - - - - - } - onSavePress={this.onSavePress} - /> - - - - - - - - - - ); - } -} - -IndexerSettings.propTypes = { - isTestingAll: PropTypes.bool.isRequired, - dispatchTestAllIndexers: PropTypes.func.isRequired -}; - -export default IndexerSettings; diff --git a/frontend/src/Settings/Indexers/IndexerSettings.tsx b/frontend/src/Settings/Indexers/IndexerSettings.tsx new file mode 100644 index 000000000..c4878a8c0 --- /dev/null +++ b/frontend/src/Settings/Indexers/IndexerSettings.tsx @@ -0,0 +1,104 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import { icons } from 'Helpers/Props'; +import SettingsToolbar from 'Settings/SettingsToolbar'; +import { testAllIndexers } from 'Store/Actions/settingsActions'; +import { + SaveCallback, + SettingsStateChange, +} from 'typings/Settings/SettingsState'; +import translate from 'Utilities/String/translate'; +import Indexers from './Indexers/Indexers'; +import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal'; +import IndexerOptions from './Options/IndexerOptions'; + +function IndexerSettings() { + const dispatch = useDispatch(); + const isTestingAll = useSelector( + (state: AppState) => state.settings.indexers.isTestingAll + ); + + const saveOptions = useRef<() => void>(); + + const [isSaving, setIsSaving] = useState(false); + const [hasPendingChanges, setHasPendingChanges] = useState(false); + const [isManageIndexersModalOpen, setIsManageIndexersModalOpen] = + useState(false); + + const handleSetChildSave = useCallback((saveCallback: SaveCallback) => { + saveOptions.current = saveCallback; + }, []); + + const handleChildStateChange = useCallback( + ({ isSaving, hasPendingChanges }: SettingsStateChange) => { + setIsSaving(isSaving); + setHasPendingChanges(hasPendingChanges); + }, + [] + ); + + const handleManageIndexersPress = useCallback(() => { + setIsManageIndexersModalOpen(true); + }, []); + + const handleManageIndexersModalClose = useCallback(() => { + setIsManageIndexersModalOpen(false); + }, []); + + const handleSavePress = useCallback(() => { + saveOptions.current?.(); + }, []); + + const handleTestAllIndexersPress = useCallback(() => { + dispatch(testAllIndexers()); + }, [dispatch]); + + return ( + + + + + + + + > + } + onSavePress={handleSavePress} + /> + + + + + + + + + + ); +} + +export default IndexerSettings; diff --git a/frontend/src/Settings/Indexers/IndexerSettingsConnector.js b/frontend/src/Settings/Indexers/IndexerSettingsConnector.js deleted file mode 100644 index 1eaf098d7..000000000 --- a/frontend/src/Settings/Indexers/IndexerSettingsConnector.js +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { testAllIndexers } from 'Store/Actions/settingsActions'; -import IndexerSettings from './IndexerSettings'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.indexers.isTestingAll, - (isTestingAll) => { - return { - isTestingAll - }; - } - ); -} - -const mapDispatchToProps = { - dispatchTestAllIndexers: testAllIndexers -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSettings); diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js deleted file mode 100644 index 5874b4fac..000000000 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js +++ /dev/null @@ -1,113 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Button from 'Components/Link/Button'; -import Link from 'Components/Link/Link'; -import Menu from 'Components/Menu/Menu'; -import MenuContent from 'Components/Menu/MenuContent'; -import { sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem'; -import styles from './AddIndexerItem.css'; - -class AddIndexerItem extends Component { - - // - // Listeners - - onIndexerSelect = () => { - const { - implementation, - implementationName - } = this.props; - - this.props.onIndexerSelect({ implementation, implementationName }); - }; - - // - // Render - - render() { - const { - implementation, - implementationName, - infoLink, - presets, - onIndexerSelect - } = this.props; - - const hasPresets = !!presets && !!presets.length; - - return ( - - - - - - {implementationName} - - - - { - hasPresets && - - - {translate('Custom')} - - - - - {translate('Presets')} - - - - { - presets.map((preset) => { - return ( - - ); - }) - } - - - - } - - - {translate('MoreInfo')} - - - - - ); - } -} - -AddIndexerItem.propTypes = { - implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, - infoLink: PropTypes.string.isRequired, - presets: PropTypes.arrayOf(PropTypes.object), - onIndexerSelect: PropTypes.func.isRequired -}; - -export default AddIndexerItem; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx new file mode 100644 index 000000000..f75623539 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx @@ -0,0 +1,88 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import { sizes } from 'Helpers/Props'; +import { selectIndexerSchema } from 'Store/Actions/settingsActions'; +import Indexer from 'typings/Indexer'; +import translate from 'Utilities/String/translate'; +import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem'; +import styles from './AddIndexerItem.css'; + +interface AddIndexerItemProps { + implementation: string; + implementationName: string; + infoLink: string; + presets?: Indexer[]; + onIndexerSelect: () => void; +} + +function AddIndexerItem({ + implementation, + implementationName, + infoLink, + presets, + onIndexerSelect, +}: AddIndexerItemProps) { + const dispatch = useDispatch(); + const hasPresets = !!presets && !!presets.length; + + const handleIndexerSelect = useCallback(() => { + dispatch( + selectIndexerSchema({ + implementation, + implementationName, + }) + ); + + onIndexerSelect(); + }, [implementation, implementationName, dispatch, onIndexerSelect]); + + return ( + + + + + {implementationName} + + + {hasPresets && ( + + + {translate('Custom')} + + + + + {translate('Presets')} + + + + {presets.map((preset) => { + return ( + + ); + })} + + + + )} + + + {translate('MoreInfo')} + + + + + ); +} + +export default AddIndexerItem; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js deleted file mode 100644 index d05e8eb9a..000000000 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import AddIndexerModalContentConnector from './AddIndexerModalContentConnector'; - -function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -AddIndexerModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AddIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx new file mode 100644 index 000000000..f834c30cb --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddIndexerModalContent from './AddIndexerModalContent'; + +interface AddIndexerModalProps { + isOpen: boolean; + onIndexerSelect: () => void; + onModalClose: () => void; +} + +function AddIndexerModal({ + isOpen, + onIndexerSelect, + onModalClose, +}: AddIndexerModalProps) { + return ( + + + + ); +} + +export default AddIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js deleted file mode 100644 index 00ebbdc55..000000000 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js +++ /dev/null @@ -1,122 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -import Button from 'Components/Link/Button'; -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 { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import AddIndexerItem from './AddIndexerItem'; -import styles from './AddIndexerModalContent.css'; - -class AddIndexerModalContent extends Component { - - // - // Render - - render() { - const { - isSchemaFetching, - isSchemaPopulated, - schemaError, - usenetIndexers, - torrentIndexers, - onIndexerSelect, - onModalClose - } = this.props; - - return ( - - - {translate('AddIndexer')} - - - - { - isSchemaFetching && - - } - - { - !isSchemaFetching && !!schemaError && - - {translate('AddIndexerError')} - - } - - { - isSchemaPopulated && !schemaError && - - - - - {translate('SupportedIndexers')} - - - {translate('SupportedIndexersMoreInfo')} - - - - - - { - usenetIndexers.map((indexer) => { - return ( - - ); - }) - } - - - - - - { - torrentIndexers.map((indexer) => { - return ( - - ); - }) - } - - - - } - - - - {translate('Close')} - - - - ); - } -} - -AddIndexerModalContent.propTypes = { - isSchemaFetching: PropTypes.bool.isRequired, - isSchemaPopulated: PropTypes.bool.isRequired, - schemaError: PropTypes.object, - usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, - torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, - onIndexerSelect: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AddIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx new file mode 100644 index 000000000..78c4d7ceb --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useMemo } 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 Button from 'Components/Link/Button'; +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 { kinds } from 'Helpers/Props'; +import { fetchIndexerSchema } from 'Store/Actions/settingsActions'; +import Indexer from 'typings/Indexer'; +import translate from 'Utilities/String/translate'; +import AddIndexerItem from './AddIndexerItem'; +import styles from './AddIndexerModalContent.css'; + +interface AddIndexerModalContentProps { + onIndexerSelect: () => void; + onModalClose: () => void; +} + +function AddIndexerModalContent({ + onIndexerSelect, + onModalClose, +}: AddIndexerModalContentProps) { + const dispatch = useDispatch(); + + const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = + useSelector((state: AppState) => state.settings.indexers); + + const { usenetIndexers, torrentIndexers } = useMemo(() => { + return schema.reduce<{ + usenetIndexers: Indexer[]; + torrentIndexers: Indexer[]; + }>( + (acc, item) => { + if (item.protocol === 'usenet') { + acc.usenetIndexers.push(item); + } else if (item.protocol === 'torrent') { + acc.torrentIndexers.push(item); + } + + return acc; + }, + { + usenetIndexers: [], + torrentIndexers: [], + } + ); + }, [schema]); + + useEffect(() => { + dispatch(fetchIndexerSchema()); + }, [dispatch]); + + return ( + + {translate('AddIndexer')} + + + {isSchemaFetching ? : null} + + {!isSchemaFetching && !!schemaError ? ( + {translate('AddIndexerError')} + ) : null} + + {isSchemaPopulated && !schemaError ? ( + + + {translate('SupportedIndexers')} + {translate('SupportedIndexersMoreInfo')} + + + + + {usenetIndexers.map((indexer) => { + return ( + + ); + })} + + + + + + {torrentIndexers.map((indexer) => { + return ( + + ); + })} + + + + ) : null} + + + + {translate('Close')} + + + ); +} + +export default AddIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js deleted file mode 100644 index a9c313479..000000000 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js +++ /dev/null @@ -1,75 +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 { fetchIndexerSchema, selectIndexerSchema } from 'Store/Actions/settingsActions'; -import AddIndexerModalContent from './AddIndexerModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.indexers, - (indexers) => { - const { - isSchemaFetching, - isSchemaPopulated, - schemaError, - schema - } = indexers; - - const usenetIndexers = _.filter(schema, { protocol: 'usenet' }); - const torrentIndexers = _.filter(schema, { protocol: 'torrent' }); - - return { - isSchemaFetching, - isSchemaPopulated, - schemaError, - usenetIndexers, - torrentIndexers - }; - } - ); -} - -const mapDispatchToProps = { - fetchIndexerSchema, - selectIndexerSchema -}; - -class AddIndexerModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchIndexerSchema(); - } - - // - // Listeners - - onIndexerSelect = ({ implementation, implementationName, name }) => { - this.props.selectIndexerSchema({ implementation, implementationName, presetName: name }); - this.props.onModalClose({ indexerSelected: true }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -AddIndexerModalContentConnector.propTypes = { - fetchIndexerSchema: PropTypes.func.isRequired, - selectIndexerSchema: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js deleted file mode 100644 index 8f98d0e12..000000000 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import MenuItem from 'Components/Menu/MenuItem'; - -class AddIndexerPresetMenuItem extends Component { - - // - // Listeners - - onPress = () => { - const { - name, - implementation, - implementationName - } = this.props; - - this.props.onPress({ - name, - implementation, - implementationName - }); - }; - - // - // Render - - render() { - const { - name, - implementation, - implementationName, - ...otherProps - } = this.props; - - return ( - - {name} - - ); - } -} - -AddIndexerPresetMenuItem.propTypes = { - name: PropTypes.string.isRequired, - implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired -}; - -export default AddIndexerPresetMenuItem; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx new file mode 100644 index 000000000..70502a615 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx @@ -0,0 +1,42 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem'; +import { selectIndexerSchema } from 'Store/Actions/settingsActions'; + +interface AddIndexerPresetMenuItemProps + extends Omit { + name: string; + implementation: string; + implementationName: string; + onPress: () => void; +} + +function AddIndexerPresetMenuItem({ + name, + implementation, + implementationName, + onPress, + ...otherProps +}: AddIndexerPresetMenuItemProps) { + const dispatch = useDispatch(); + + const handlePress = useCallback(() => { + dispatch( + selectIndexerSchema({ + implementation, + implementationName, + presetName: name, + }) + ); + + onPress(); + }, [name, implementation, implementationName, dispatch, onPress]); + + return ( + + {name} + + ); +} + +export default AddIndexerPresetMenuItem; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js deleted file mode 100644 index 0f57c3bdf..000000000 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import EditIndexerModalContentConnector from './EditIndexerModalContentConnector'; - -function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditIndexerModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx new file mode 100644 index 000000000..87175e197 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx @@ -0,0 +1,45 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + cancelSaveIndexer, + cancelTestIndexer, +} from 'Store/Actions/settingsActions'; +import EditIndexerModalContent, { + EditIndexerModalContentProps, +} from './EditIndexerModalContent'; + +const section = 'settings.indexers'; + +interface EditIndexerModalProps extends EditIndexerModalContentProps { + isOpen: boolean; +} + +function EditIndexerModal({ + isOpen, + onModalClose, + ...otherProps +}: EditIndexerModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch(clearPendingChanges({ section })); + dispatch(cancelTestIndexer({ section })); + dispatch(cancelSaveIndexer({ section })); + + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js deleted file mode 100644 index bc4300a7e..000000000 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { cancelSaveIndexer, cancelTestIndexer } from 'Store/Actions/settingsActions'; -import EditIndexerModal from './EditIndexerModal'; - -function createMapDispatchToProps(dispatch, props) { - const section = 'settings.indexers'; - - return { - dispatchClearPendingChanges() { - dispatch(clearPendingChanges({ section })); - }, - - dispatchCancelTestIndexer() { - dispatch(cancelTestIndexer({ section })); - }, - - dispatchCancelSaveIndexer() { - dispatch(cancelSaveIndexer({ section })); - } - }; -} - -class EditIndexerModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.dispatchClearPendingChanges(); - this.props.dispatchCancelTestIndexer(); - this.props.dispatchCancelSaveIndexer(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - const { - dispatchClearPendingChanges, - dispatchCancelTestIndexer, - dispatchCancelSaveIndexer, - ...otherProps - } = this.props; - - return ( - - ); - } -} - -EditIndexerModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - dispatchClearPendingChanges: PropTypes.func.isRequired, - dispatchCancelTestIndexer: PropTypes.func.isRequired, - dispatchCancelSaveIndexer: PropTypes.func.isRequired -}; - -export default connect(null, createMapDispatchToProps)(EditIndexerModalConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js deleted file mode 100644 index fc99e5191..000000000 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ /dev/null @@ -1,269 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; -import translate from 'Utilities/String/translate'; -import styles from './EditIndexerModalContent.css'; - -function EditIndexerModalContent(props) { - const { - advancedSettings, - isFetching, - error, - isSaving, - isTesting, - saveError, - item, - onInputChange, - onFieldChange, - onModalClose, - onSavePress, - onTestPress, - onDeleteIndexerPress, - ...otherProps - } = props; - - const { - id, - implementationName, - name, - enableRss, - enableAutomaticSearch, - enableInteractiveSearch, - supportsRss, - supportsSearch, - tags, - fields, - priority, - seasonSearchMaximumSingleEpisodeAge, - protocol, - downloadClientId - } = item; - - return ( - - - {id ? translate('EditIndexerImplementation', { implementationName }) : translate('AddIndexerImplementation', { implementationName })} - - - - { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('AddIndexerError')} - - } - - { - !isFetching && !error && - - - {translate('Name')} - - - - - - {translate('EnableRss')} - - - - - - {translate('EnableAutomaticSearch')} - - - - - - {translate('EnableInteractiveSearch')} - - - - - { - fields.map((field) => { - return ( - - ); - }) - } - - - {translate('IndexerPriority')} - - - - - - {translate('MaximumSingleEpisodeAge')} - - - - - - {translate('DownloadClient')} - - - - - - {translate('Tags')} - - - - - } - - - { - id && - - {translate('Delete')} - - } - - - - - {translate('Test')} - - - - {translate('Cancel')} - - - - {translate('Save')} - - - - ); -} - -EditIndexerModalContent.propTypes = { - advancedSettings: PropTypes.bool.isRequired, - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - isTesting: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onFieldChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onTestPress: PropTypes.func.isRequired, - onDeleteIndexerPress: PropTypes.func -}; - -export default EditIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx new file mode 100644 index 000000000..0a0a896d1 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx @@ -0,0 +1,317 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { IndexerAppState } from 'App/State/SettingsAppState'; +import Alert from 'Components/Alert'; +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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +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 useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings'; +import { inputTypes, kinds } from 'Helpers/Props'; +import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; +import { + saveIndexer, + setIndexerFieldValue, + setIndexerValue, + testIndexer, +} from 'Store/Actions/settingsActions'; +import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector'; +import Indexer from 'typings/Indexer'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './EditIndexerModalContent.css'; + +export interface EditIndexerModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteIndexerPress?: () => void; +} + +function EditIndexerModalContent({ + id, + onModalClose, + onDeleteIndexerPress, +}: EditIndexerModalContentProps) { + const dispatch = useDispatch(); + const showAdvancedSettings = useShowAdvancedSettings(); + + const { + isFetching, + error, + isSaving, + isTesting = false, + saveError, + item, + validationErrors, + validationWarnings, + } = useSelector( + createProviderSettingsSelectorHook('indexers', id) + ); + + const wasSaving = usePrevious(isSaving); + + const { + implementationName = '', + name, + enableRss, + enableAutomaticSearch, + enableInteractiveSearch, + supportsRss, + supportsSearch, + tags, + fields, + priority, + seasonSearchMaximumSingleEpisodeAge, + protocol, + downloadClientId, + } = item; + + const handleInputChange = useCallback( + (change: InputChanged) => { + // @ts-expect-error - actions are not typed + dispatch(setIndexerValue(change)); + }, + [dispatch] + ); + + const handleFieldChange = useCallback( + (change: InputChanged) => { + // @ts-expect-error - actions are not typed + dispatch(setIndexerFieldValue(change)); + }, + [dispatch] + ); + + const handleSavePress = useCallback(() => { + dispatch(saveIndexer({ id })); + }, [id, dispatch]); + + const handleTestPress = useCallback(() => { + dispatch(testIndexer({ id })); + }, [id, dispatch]); + + useEffect(() => { + if (!isSaving && wasSaving && !saveError) { + onModalClose(); + } + }, [isSaving, wasSaving, saveError, onModalClose]); + + return ( + + + {id + ? translate('EditIndexerImplementation', { implementationName }) + : translate('AddIndexerImplementation', { implementationName })} + + + + {isFetching ? : null} + + {!isFetching && error ? ( + {translate('AddIndexerError')} + ) : null} + + {!isFetching && !error ? ( + + + {translate('Name')} + + + + + + {translate('EnableRss')} + + + + + + {translate('EnableAutomaticSearch')} + + + + + + {translate('EnableInteractiveSearch')} + + + + + {fields?.map((field) => { + return ( + + ); + })} + + + {translate('IndexerPriority')} + + + + + + {translate('MaximumSingleEpisodeAge')} + + + + + + {translate('DownloadClient')} + + + + + + {translate('Tags')} + + + + + ) : null} + + + + {id ? ( + + {translate('Delete')} + + ) : null} + + + + + {translate('Test')} + + + {translate('Cancel')} + + + {translate('Save')} + + + + ); +} + +export default EditIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js deleted file mode 100644 index 5be01849d..000000000 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js +++ /dev/null @@ -1,88 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/settingsActions'; -import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; -import EditIndexerModalContent from './EditIndexerModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - createProviderSettingsSelector('indexers'), - (advancedSettings, indexer) => { - return { - advancedSettings, - ...indexer - }; - } - ); -} - -const mapDispatchToProps = { - setIndexerValue, - setIndexerFieldValue, - saveIndexer, - testIndexer -}; - -class EditIndexerModalContentConnector extends Component { - - // - // Lifecycle - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setIndexerValue({ name, value }); - }; - - onFieldChange = ({ name, value }) => { - this.props.setIndexerFieldValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveIndexer({ id: this.props.id }); - }; - - onTestPress = () => { - this.props.testIndexer({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditIndexerModalContentConnector.propTypes = { - id: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setIndexerValue: PropTypes.func.isRequired, - setIndexerFieldValue: PropTypes.func.isRequired, - saveIndexer: PropTypes.func.isRequired, - testIndexer: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.js b/frontend/src/Settings/Indexers/Indexers/Indexer.js deleted file mode 100644 index e6c24cee8..000000000 --- a/frontend/src/Settings/Indexers/Indexers/Indexer.js +++ /dev/null @@ -1,181 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TagList from 'Components/TagList'; -import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditIndexerModalConnector from './EditIndexerModalConnector'; -import styles from './Indexer.css'; - -class Indexer extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditIndexerModalOpen: false, - isDeleteIndexerModalOpen: false - }; - } - - // - // Listeners - - onEditIndexerPress = () => { - this.setState({ isEditIndexerModalOpen: true }); - }; - - onEditIndexerModalClose = () => { - this.setState({ isEditIndexerModalOpen: false }); - }; - - onDeleteIndexerPress = () => { - this.setState({ - isEditIndexerModalOpen: false, - isDeleteIndexerModalOpen: true - }); - }; - - onDeleteIndexerModalClose = () => { - this.setState({ isDeleteIndexerModalOpen: false }); - }; - - onConfirmDeleteIndexer = () => { - this.props.onConfirmDeleteIndexer(this.props.id); - }; - - onCloneIndexerPress = () => { - const { - id, - onCloneIndexerPress - } = this.props; - - onCloneIndexerPress(id); - }; - - // - // Render - - render() { - const { - id, - name, - enableRss, - enableAutomaticSearch, - enableInteractiveSearch, - tags, - tagList, - supportsRss, - supportsSearch, - priority, - showPriority - } = this.props; - - return ( - - - - {name} - - - - - - - - { - supportsRss && enableRss && - - {translate('Rss')} - - } - - { - supportsSearch && enableAutomaticSearch && - - {translate('AutomaticSearch')} - - } - - { - supportsSearch && enableInteractiveSearch && - - {translate('InteractiveSearch')} - - } - - { - showPriority && - - {translate('Priority')}: {priority} - - } - { - !enableRss && !enableAutomaticSearch && !enableInteractiveSearch && - - {translate('Disabled')} - - } - - - - - - - - - ); - } -} - -Indexer.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - priority: PropTypes.number.isRequired, - enableRss: PropTypes.bool.isRequired, - enableAutomaticSearch: PropTypes.bool.isRequired, - enableInteractiveSearch: PropTypes.bool.isRequired, - tags: PropTypes.arrayOf(PropTypes.number).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - supportsRss: PropTypes.bool.isRequired, - supportsSearch: PropTypes.bool.isRequired, - showPriority: PropTypes.bool.isRequired, - onCloneIndexerPress: PropTypes.func.isRequired, - onConfirmDeleteIndexer: PropTypes.func.isRequired -}; - -export default Indexer; diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.tsx b/frontend/src/Settings/Indexers/Indexers/Indexer.tsx new file mode 100644 index 000000000..5804a2cc1 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.tsx @@ -0,0 +1,131 @@ +import React, { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagList from 'Components/TagList'; +import { icons, kinds } from 'Helpers/Props'; +import { deleteIndexer } from 'Store/Actions/settingsActions'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import IndexerModel from 'typings/Indexer'; +import translate from 'Utilities/String/translate'; +import EditIndexerModal from './EditIndexerModal'; +import styles from './Indexer.css'; + +interface IndexerProps extends IndexerModel { + showPriority: boolean; + onCloneIndexerPress: (id: number) => void; +} + +function Indexer({ + id, + name, + enableRss, + enableAutomaticSearch, + enableInteractiveSearch, + tags, + supportsRss, + supportsSearch, + priority, + showPriority, + onCloneIndexerPress, +}: IndexerProps) { + const dispatch = useDispatch(); + const tagList = useSelector(createTagsSelector()); + + const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); + const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = + useState(false); + + const handleEditIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(true); + }, []); + + const handleEditIndexerModalClose = useCallback(() => { + setIsEditIndexerModalOpen(false); + }, []); + + const handleDeleteIndexerPress = useCallback(() => { + setIsEditIndexerModalOpen(false); + setIsDeleteIndexerModalOpen(true); + }, []); + + const handleDeleteIndexerModalClose = useCallback(() => { + setIsDeleteIndexerModalOpen(false); + }, []); + + const handleConfirmDeleteIndexer = useCallback(() => { + dispatch(deleteIndexer({ id })); + }, [id, dispatch]); + + const handleCloneIndexerPress = useCallback(() => { + onCloneIndexerPress(id); + }, [id, onCloneIndexerPress]); + + return ( + + + {name} + + + + + + {supportsRss && enableRss ? ( + {translate('Rss')} + ) : null} + + {supportsSearch && enableAutomaticSearch ? ( + {translate('AutomaticSearch')} + ) : null} + + {supportsSearch && enableInteractiveSearch ? ( + {translate('InteractiveSearch')} + ) : null} + + {showPriority ? ( + + {translate('Priority')}: {priority} + + ) : null} + + {!enableRss && !enableAutomaticSearch && !enableInteractiveSearch ? ( + + {translate('Disabled')} + + ) : null} + + + + + + + + + ); +} + +export default Indexer; diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.js b/frontend/src/Settings/Indexers/Indexers/Indexers.js deleted file mode 100644 index 2cb01953a..000000000 --- a/frontend/src/Settings/Indexers/Indexers/Indexers.js +++ /dev/null @@ -1,129 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import AddIndexerModal from './AddIndexerModal'; -import EditIndexerModalConnector from './EditIndexerModalConnector'; -import Indexer from './Indexer'; -import styles from './Indexers.css'; - -class Indexers extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddIndexerModalOpen: false, - isEditIndexerModalOpen: false - }; - } - - // - // Listeners - - onAddIndexerPress = () => { - this.setState({ isAddIndexerModalOpen: true }); - }; - - onCloneIndexerPress = (id) => { - this.props.dispatchCloneIndexer({ id }); - this.setState({ isEditIndexerModalOpen: true }); - }; - - onAddIndexerModalClose = ({ indexerSelected = false } = {}) => { - this.setState({ - isAddIndexerModalOpen: false, - isEditIndexerModalOpen: indexerSelected - }); - }; - - onEditIndexerModalClose = () => { - this.setState({ isEditIndexerModalOpen: false }); - }; - - // - // Render - - render() { - const { - items, - tagList, - dispatchCloneIndexer, - onConfirmDeleteIndexer, - ...otherProps - } = this.props; - - const { - isAddIndexerModalOpen, - isEditIndexerModalOpen - } = this.state; - - const showPriority = items.some((index) => index.priority !== 25); - - return ( - - - - { - items.map((item) => { - return ( - - ); - }) - } - - - - - - - - - - - - - - ); - } -} - -Indexers.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - dispatchCloneIndexer: PropTypes.func.isRequired, - onConfirmDeleteIndexer: PropTypes.func.isRequired -}; - -export default Indexers; diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.tsx b/frontend/src/Settings/Indexers/Indexers/Indexers.tsx new file mode 100644 index 000000000..9483e2ff8 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.tsx @@ -0,0 +1,105 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { IndexerAppState } from 'App/State/SettingsAppState'; +import Card from 'Components/Card'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import { icons } from 'Helpers/Props'; +import { cloneIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import IndexerModel from 'typings/Indexer'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import AddIndexerModal from './AddIndexerModal'; +import EditIndexerModal from './EditIndexerModal'; +import Indexer from './Indexer'; +import styles from './Indexers.css'; + +function Indexers() { + const dispatch = useDispatch(); + + const { isFetching, isPopulated, items, error } = useSelector( + createSortedSectionSelector( + 'settings.indexers', + sortByProp('name') + ) + ); + + const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false); + const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); + + const showPriority = items.some((index) => index.priority !== 25); + + const handleAddIndexerPress = useCallback(() => { + setIsAddIndexerModalOpen(true); + }, []); + + const handleCloneIndexerPress = useCallback( + (id: number) => { + dispatch(cloneIndexer({ id })); + setIsEditIndexerModalOpen(true); + }, + [dispatch] + ); + + const handleIndexerSelect = useCallback(() => { + setIsAddIndexerModalOpen(false); + setIsEditIndexerModalOpen(true); + }, []); + + const handleAddIndexerModalClose = useCallback(() => { + setIsAddIndexerModalOpen(false); + }, []); + + const handleEditIndexerModalClose = useCallback(() => { + setIsEditIndexerModalOpen(false); + }, []); + + useEffect(() => { + dispatch(fetchIndexers()); + }, [dispatch]); + + return ( + + + + {items.map((item) => { + return ( + + ); + })} + + + + + + + + + + + + + + ); +} + +export default Indexers; diff --git a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js deleted file mode 100644 index 88c571a60..000000000 --- a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import Indexers from './Indexers'; - -function createMapStateToProps() { - return createSelector( - createSortedSectionSelector('settings.indexers', sortByProp('name')), - createTagsSelector(), - (indexers, tagList) => { - return { - ...indexers, - tagList - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchIndexers: fetchIndexers, - dispatchDeleteIndexer: deleteIndexer, - dispatchCloneIndexer: cloneIndexer -}; - -class IndexersConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchIndexers(); - } - - // - // Listeners - - onConfirmDeleteIndexer = (id) => { - this.props.dispatchDeleteIndexer({ id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -IndexersConnector.propTypes = { - dispatchFetchIndexers: PropTypes.func.isRequired, - dispatchDeleteIndexer: PropTypes.func.isRequired, - dispatchCloneIndexer: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector); diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptions.js b/frontend/src/Settings/Indexers/Options/IndexerOptions.js deleted file mode 100644 index c1211b075..000000000 --- a/frontend/src/Settings/Indexers/Options/IndexerOptions.js +++ /dev/null @@ -1,116 +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 IndexerOptions(props) { - const { - advancedSettings, - isFetching, - error, - settings, - hasSettings, - onInputChange - } = props; - - return ( - - { - isFetching && - - } - - { - !isFetching && error && - - {translate('IndexerOptionsLoadError')} - - } - - { - hasSettings && !isFetching && !error && - - - {translate('MinimumAge')} - - - - - - {translate('Retention')} - - - - - - {translate('MaximumSize')} - - - - - - {translate('RssSyncInterval')} - - - - - } - - ); -} - -IndexerOptions.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 IndexerOptions; diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx b/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx new file mode 100644 index 000000000..63f05a236 --- /dev/null +++ b/frontend/src/Settings/Indexers/Options/IndexerOptions.tsx @@ -0,0 +1,152 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +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 useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchIndexerOptions, + saveIndexerOptions, + setIndexerOptionsValue, +} from 'Store/Actions/settingsActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { InputChanged } from 'typings/inputs'; +import { + OnChildStateChange, + SetChildSave, +} from 'typings/Settings/SettingsState'; +import translate from 'Utilities/String/translate'; + +const SECTION = 'indexerOptions'; + +interface IndexerOptionsProps { + setChildSave: SetChildSave; + onChildStateChange: OnChildStateChange; +} + +function IndexerOptions({ + setChildSave, + onChildStateChange, +}: IndexerOptionsProps) { + const dispatch = useDispatch(); + const { + isFetching, + isPopulated, + isSaving, + error, + settings, + hasSettings, + hasPendingChanges, + } = useSelector(createSettingsSectionSelector(SECTION)); + + const showAdvancedSettings = useShowAdvancedSettings(); + + const handleInputChange = useCallback( + (change: InputChanged) => { + // @ts-expect-error - actions aren't typed + dispatch(setIndexerOptionsValue(change)); + }, + [dispatch] + ); + + useEffect(() => { + dispatch(fetchIndexerOptions()); + setChildSave(() => dispatch(saveIndexerOptions())); + }, [dispatch, setChildSave]); + + useEffect(() => { + onChildStateChange({ + isSaving, + hasPendingChanges, + }); + }, [hasPendingChanges, isSaving, onChildStateChange]); + + useEffect(() => { + return () => { + dispatch(clearPendingChanges({ section: `settings.${SECTION}` })); + }; + }, [dispatch]); + + return ( + + {isFetching ? : null} + + {!isFetching && error ? ( + + {translate('IndexerOptionsLoadError')} + + ) : null} + + {hasSettings && isPopulated && !error ? ( + + + {translate('MinimumAge')} + + + + + + {translate('Retention')} + + + + + + {translate('MaximumSize')} + + + + + + {translate('RssSyncInterval')} + + + + + ) : null} + + ); +} + +export default IndexerOptions; diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js deleted file mode 100644 index f067a9ea6..000000000 --- a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.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 { fetchIndexerOptions, saveIndexerOptions, setIndexerOptionsValue } from 'Store/Actions/settingsActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import IndexerOptions from './IndexerOptions'; - -const SECTION = 'indexerOptions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - createSettingsSectionSelector(SECTION), - (advancedSettings, sectionSettings) => { - return { - advancedSettings, - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchIndexerOptions: fetchIndexerOptions, - dispatchSetIndexerOptionsValue: setIndexerOptionsValue, - dispatchSaveIndexerOptions: saveIndexerOptions, - dispatchClearPendingChanges: clearPendingChanges -}; - -class IndexerOptionsConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - dispatchFetchIndexerOptions, - dispatchSaveIndexerOptions, - onChildMounted - } = this.props; - - dispatchFetchIndexerOptions(); - onChildMounted(dispatchSaveIndexerOptions); - } - - componentDidUpdate(prevProps) { - const { - hasPendingChanges, - isSaving, - onChildStateChange - } = this.props; - - if ( - prevProps.isSaving !== isSaving || - prevProps.hasPendingChanges !== hasPendingChanges - ) { - onChildStateChange({ - isSaving, - hasPendingChanges - }); - } - } - - componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.dispatchSetIndexerOptionsValue({ name, value }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -IndexerOptionsConnector.propTypes = { - isSaving: PropTypes.bool.isRequired, - hasPendingChanges: PropTypes.bool.isRequired, - dispatchFetchIndexerOptions: PropTypes.func.isRequired, - dispatchSetIndexerOptionsValue: PropTypes.func.isRequired, - dispatchSaveIndexerOptions: PropTypes.func.isRequired, - dispatchClearPendingChanges: PropTypes.func.isRequired, - onChildMounted: PropTypes.func.isRequired, - onChildStateChange: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector); diff --git a/frontend/src/Store/Selectors/createProviderSettingsSelector.ts b/frontend/src/Store/Selectors/createProviderSettingsSelector.ts index 90d6fe0fe..5ddbd1f61 100644 --- a/frontend/src/Store/Selectors/createProviderSettingsSelector.ts +++ b/frontend/src/Store/Selectors/createProviderSettingsSelector.ts @@ -9,6 +9,7 @@ import AppState from 'App/State/AppState'; import selectSettings, { ModelBaseSetting, } from 'Store/Selectors/selectSettings'; +import { PendingSection } from 'typings/pending'; import getSectionState from 'Utilities/State/getSectionState'; type SchemaState = AppSectionSchemaState | AppSectionItemSchemaState; @@ -73,7 +74,7 @@ function selector< isTesting, ...settings, pendingChanges, - item: settings.settings, + item: settings.settings as PendingSection, }; } diff --git a/frontend/src/typings/Indexer.ts b/frontend/src/typings/Indexer.ts index ea38651f4..db0be8531 100644 --- a/frontend/src/typings/Indexer.ts +++ b/frontend/src/typings/Indexer.ts @@ -1,11 +1,16 @@ +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import Provider from './Provider'; interface Indexer extends Provider { enableRss: boolean; enableAutomaticSearch: boolean; enableInteractiveSearch: boolean; - protocol: string; + supportsRss: boolean; + supportsSearch: boolean; + seasonSearchMaximumSingleEpisodeAge: number; + protocol: DownloadProtocol; priority: number; + downloadClientId: number; tags: number[]; } diff --git a/frontend/src/typings/Settings/IndexerOptions.ts b/frontend/src/typings/Settings/IndexerOptions.ts new file mode 100644 index 000000000..1eb21de6e --- /dev/null +++ b/frontend/src/typings/Settings/IndexerOptions.ts @@ -0,0 +1,6 @@ +export default interface IndexerOptions { + minimumAge: number; + retention: number; + maximumSize: number; + rssSyncInterval: number; +}