From c4c0ec25acef7855cc25f38648bfc1649c322e87 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 16 Feb 2026 15:48:47 -0800 Subject: [PATCH] Use react-query for Indexers --- frontend/src/App/State/SettingsAppState.ts | 10 - .../Builder/IndexerFilterBuilderRowValue.tsx | 22 +- .../Form/Select/IndexerSelectInput.tsx | 61 +-- frontend/src/Components/SignalRListener.tsx | 54 ++- .../src/Settings/Indexers/IndexerSettings.tsx | 15 +- .../Indexers/Indexers/AddIndexerItem.tsx | 21 +- .../Indexers/Indexers/AddIndexerModal.tsx | 8 +- .../Indexers/AddIndexerModalContent.tsx | 28 +- .../Indexers/AddIndexerPresetMenuItem.tsx | 24 +- .../Indexers/Indexers/EditIndexerModal.tsx | 27 +- .../Indexers/EditIndexerModalContent.tsx | 347 ++++++++---------- .../Settings/Indexers/Indexers/Indexer.tsx | 10 +- .../Settings/Indexers/Indexers/Indexers.tsx | 52 ++- .../Manage/ManageIndexersModalContent.tsx | 85 ++--- .../Manage/ManageIndexersModalRow.tsx | 4 +- .../Indexers/Manage/Tags/TagsModalContent.tsx | 53 ++- frontend/src/Settings/Indexers/useIndexers.ts | 274 ++++++++++++++ .../Indexers/useManageIndexersOptionsStore.ts | 20 + .../Notifications/AddNotificationItem.tsx | 2 +- .../AddNotificationModalContent.tsx | 2 +- .../AddNotificationPresetMenuItem.tsx | 2 +- .../Notifications/EditNotificationModal.tsx | 17 +- .../EditNotificationModalContent.tsx | 9 +- .../Profiles/Release/ReleaseProfileItem.tsx | 4 +- .../Profiles/Release/ReleaseProfiles.tsx | 15 +- .../Tags/Details/TagDetailsModalContent.tsx | 9 +- frontend/src/Settings/Tags/Tags.tsx | 4 +- frontend/src/Settings/useProviderSettings.ts | 16 +- .../src/Store/Actions/Settings/indexers.js | 180 --------- frontend/src/Store/Actions/settingsActions.js | 11 +- frontend/src/System/Status/Health/Health.tsx | 15 +- frontend/src/typings/Indexer.ts | 17 - 32 files changed, 689 insertions(+), 729 deletions(-) create mode 100644 frontend/src/Settings/Indexers/useIndexers.ts create mode 100644 frontend/src/Settings/Indexers/useManageIndexersOptionsStore.ts delete mode 100644 frontend/src/Store/Actions/Settings/indexers.js delete mode 100644 frontend/src/typings/Indexer.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index fc313b0bb..52b940692 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -14,7 +14,6 @@ import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; import ImportListExclusion from 'typings/ImportListExclusion'; import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; -import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; import DownloadClientOptions from 'typings/Settings/DownloadClientOptions'; import IndexerOptions from 'typings/Settings/IndexerOptions'; @@ -63,14 +62,6 @@ export interface IndexerOptionsAppState extends AppSectionItemState, AppSectionSaveState {} -export interface IndexerAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState, - AppSectionSchemaState> { - isTestingAll: boolean; -} - export interface CustomFormatAppState extends AppSectionState, AppSectionDeleteState, @@ -109,7 +100,6 @@ interface SettingsAppState { importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; indexerOptions: IndexerOptionsAppState; - indexers: IndexerAppState; } export default SettingsAppState; diff --git a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx index 3526081b0..3ff6f8bcf 100644 --- a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValue.tsx @@ -1,7 +1,5 @@ -import React, { useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import { fetchIndexers } from 'Store/Actions/settingsActions'; +import React, { useMemo } from 'react'; +import { useSortedIndexers } from 'Settings/Indexers/useIndexers'; import FilterBuilderRowValue, { FilterBuilderRowValueProps, } from './FilterBuilderRowValue'; @@ -14,26 +12,16 @@ type IndexerFilterBuilderRowValueProps = Omit< function IndexerFilterBuilderRowValue( props: IndexerFilterBuilderRowValueProps ) { - const dispatch = useDispatch(); - - const { isPopulated, items } = useSelector( - (state: AppState) => state.settings.indexers - ); + const { data } = useSortedIndexers(); const tagList = useMemo(() => { - return items.map((item) => { + return data.map((item) => { return { id: item.id, name: item.name, }; }); - }, [items]); - - useEffect(() => { - if (!isPopulated) { - dispatch(fetchIndexers()); - } - }, [isPopulated, dispatch]); + }, [data]); return ; } diff --git a/frontend/src/Components/Form/Select/IndexerSelectInput.tsx b/frontend/src/Components/Form/Select/IndexerSelectInput.tsx index 14a6df8ec..a66b423db 100644 --- a/frontend/src/Components/Form/Select/IndexerSelectInput.tsx +++ b/frontend/src/Components/Form/Select/IndexerSelectInput.tsx @@ -1,43 +1,9 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; -import { fetchIndexers } from 'Store/Actions/settingsActions'; +import React, { useMemo } from 'react'; +import { useSortedIndexers } from 'Settings/Indexers/useIndexers'; import { EnhancedSelectInputChanged } from 'typings/inputs'; -import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; -function createIndexersSelector(includeAny: boolean) { - return createSelector( - (state: AppState) => state.settings.indexers, - (indexers) => { - const { isFetching, isPopulated, error, items } = indexers; - - const values = items.sort(sortByProp('name')).map((indexer) => { - return { - key: indexer.id, - value: indexer.name, - }; - }); - - if (includeAny) { - values.unshift({ - key: 0, - value: `(${translate('Any')})`, - }); - } - - return { - isFetching, - isPopulated, - error, - values, - }; - } - ); -} - export interface IndexerSelectInputProps { name: string; value: number | number[]; @@ -51,16 +17,23 @@ function IndexerSelectInput({ includeAny = false, onChange, }: IndexerSelectInputProps) { - const dispatch = useDispatch(); - const { isFetching, isPopulated, values } = useSelector( - createIndexersSelector(includeAny) - ); + const { isFetching, data } = useSortedIndexers(); - useEffect(() => { - if (!isPopulated) { - dispatch(fetchIndexers()); + const values = useMemo(() => { + const indexerOptions = data.map((indexer) => ({ + key: indexer.id, + value: indexer.name, + })); + + if (includeAny) { + indexerOptions.unshift({ + key: 0, + value: `(${translate('Any')})`, + }); } - }, [isPopulated, dispatch]); + + return indexerOptions; + }, [data, includeAny]); return ( ( } ); }; + +const updateQueryClientItem = ( + queryClient: ReturnType, + queryKey: QueryKey, + updatedItem: T, + addMissing: boolean +) => { + queryClient.setQueriesData({ queryKey }, (oldData: T[] | undefined) => { + if (!oldData) { + return oldData; + } + + const itemIndex = oldData.findIndex((item) => item.id === updatedItem.id); + + if (itemIndex === -1 && addMissing) { + return [...oldData, updatedItem]; + } + + return oldData.map((item) => { + if (item.id === updatedItem.id) { + return updatedItem; + } + + return item; + }); + }); +}; + +const removeQueryClientItem = ( + queryClient: ReturnType, + queryKey: QueryKey, + id: T['id'] +) => { + queryClient.setQueriesData({ queryKey }, (oldData: T[] | undefined) => { + if (!oldData) { + return oldData; + } + + const itemIndex = oldData.findIndex((item) => item.id === updatedItem.id); + + if (itemIndex === -1) { + return oldData; + } + + return oldData.filter((item) => item.id !== id); + }); +}; diff --git a/frontend/src/Settings/Indexers/IndexerSettings.tsx b/frontend/src/Settings/Indexers/IndexerSettings.tsx index c4878a8c0..908bbca95 100644 --- a/frontend/src/Settings/Indexers/IndexerSettings.tsx +++ b/frontend/src/Settings/Indexers/IndexerSettings.tsx @@ -1,13 +1,10 @@ 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, @@ -16,12 +13,10 @@ import translate from 'Utilities/String/translate'; import Indexers from './Indexers/Indexers'; import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal'; import IndexerOptions from './Options/IndexerOptions'; +import { useTestAllIndexers } from './useIndexers'; function IndexerSettings() { - const dispatch = useDispatch(); - const isTestingAll = useSelector( - (state: AppState) => state.settings.indexers.isTestingAll - ); + const { isTestingAllIndexers, testAllIndexers } = useTestAllIndexers(); const saveOptions = useRef<() => void>(); @@ -55,8 +50,8 @@ function IndexerSettings() { }, []); const handleTestAllIndexersPress = useCallback(() => { - dispatch(testAllIndexers()); - }, [dispatch]); + testAllIndexers(); + }, [testAllIndexers]); return ( @@ -70,7 +65,7 @@ function IndexerSettings() { diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx index f75623539..c415bc3d1 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx @@ -1,13 +1,12 @@ 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 { SelectedSchema } from 'Settings/useProviderSchema'; import translate from 'Utilities/String/translate'; +import { IndexerModel } from '../useIndexers'; import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem'; import styles from './AddIndexerItem.css'; @@ -15,8 +14,8 @@ interface AddIndexerItemProps { implementation: string; implementationName: string; infoLink: string; - presets?: Indexer[]; - onIndexerSelect: () => void; + presets?: IndexerModel[]; + onIndexerSelect: (selectedSchema: SelectedSchema) => void; } function AddIndexerItem({ @@ -26,19 +25,11 @@ function AddIndexerItem({ presets, onIndexerSelect, }: AddIndexerItemProps) { - const dispatch = useDispatch(); const hasPresets = !!presets && !!presets.length; const handleIndexerSelect = useCallback(() => { - dispatch( - selectIndexerSchema({ - implementation, - implementationName, - }) - ); - - onIndexerSelect(); - }, [implementation, implementationName, dispatch, onIndexerSelect]); + onIndexerSelect({ implementation, implementationName }); + }, [implementation, implementationName, onIndexerSelect]); return (
diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx index f834c30cb..f18857d4a 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx @@ -1,11 +1,11 @@ import React from 'react'; import Modal from 'Components/Modal/Modal'; -import AddIndexerModalContent from './AddIndexerModalContent'; +import AddIndexerModalContent, { + AddIndexerModalContentProps, +} from './AddIndexerModalContent'; -interface AddIndexerModalProps { +interface AddIndexerModalProps extends AddIndexerModalContentProps { isOpen: boolean; - onIndexerSelect: () => void; - onModalClose: () => void; } function AddIndexerModal({ diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx index 78c4d7ceb..686023b93 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.tsx @@ -1,6 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import React, { useMemo } from 'react'; import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import Button from 'Components/Link/Button'; @@ -10,14 +8,14 @@ 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 { SelectedSchema } from 'Settings/useProviderSchema'; import translate from 'Utilities/String/translate'; +import { IndexerModel, useIndexerSchema } from '../useIndexers'; import AddIndexerItem from './AddIndexerItem'; import styles from './AddIndexerModalContent.css'; -interface AddIndexerModalContentProps { - onIndexerSelect: () => void; +export interface AddIndexerModalContentProps { + onIndexerSelect: (selectedSchema: SelectedSchema) => void; onModalClose: () => void; } @@ -25,15 +23,13 @@ function AddIndexerModalContent({ onIndexerSelect, onModalClose, }: AddIndexerModalContentProps) { - const dispatch = useDispatch(); - - const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = - useSelector((state: AppState) => state.settings.indexers); + const { isSchemaFetching, isSchemaFetched, schemaError, schema } = + useIndexerSchema(); const { usenetIndexers, torrentIndexers } = useMemo(() => { return schema.reduce<{ - usenetIndexers: Indexer[]; - torrentIndexers: Indexer[]; + usenetIndexers: IndexerModel[]; + torrentIndexers: IndexerModel[]; }>( (acc, item) => { if (item.protocol === 'usenet') { @@ -51,10 +47,6 @@ function AddIndexerModalContent({ ); }, [schema]); - useEffect(() => { - dispatch(fetchIndexerSchema()); - }, [dispatch]); - return ( {translate('AddIndexer')} @@ -66,7 +58,7 @@ function AddIndexerModalContent({ {translate('AddIndexerError')} ) : null} - {isSchemaPopulated && !schemaError ? ( + {isSchemaFetched && !schemaError ? (
{translate('SupportedIndexers')}
diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx index 70502a615..0c52ce69c 100644 --- a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.tsx @@ -1,14 +1,12 @@ import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem'; -import { selectIndexerSchema } from 'Store/Actions/settingsActions'; +import MenuItem from 'Components/Menu/MenuItem'; +import { SelectedSchema } from 'Settings/useProviderSchema'; -interface AddIndexerPresetMenuItemProps - extends Omit { +interface AddIndexerPresetMenuItemProps { name: string; implementation: string; implementationName: string; - onPress: () => void; + onPress: (selectedSchema: SelectedSchema) => void; } function AddIndexerPresetMenuItem({ @@ -18,19 +16,9 @@ function AddIndexerPresetMenuItem({ onPress, ...otherProps }: AddIndexerPresetMenuItemProps) { - const dispatch = useDispatch(); - const handlePress = useCallback(() => { - dispatch( - selectIndexerSchema({ - implementation, - implementationName, - presetName: name, - }) - ); - - onPress(); - }, [name, implementation, implementationName, dispatch, onPress]); + onPress({ implementation, implementationName, presetName: name }); + }, [name, implementation, implementationName, onPress]); return ( diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx index 87175e197..04357de18 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx @@ -1,18 +1,10 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; 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; } @@ -22,22 +14,9 @@ function EditIndexerModal({ onModalClose, ...otherProps }: EditIndexerModalProps) { - const dispatch = useDispatch(); - - const handleModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section })); - dispatch(cancelTestIndexer({ section })); - dispatch(cancelSaveIndexer({ section })); - - onModalClose(); - }, [dispatch, onModalClose]); - return ( - - + + ); } diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx index 460be11d5..854035556 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.tsx @@ -1,7 +1,4 @@ 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'; @@ -9,7 +6,6 @@ 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'; @@ -18,44 +14,41 @@ import usePrevious from 'Helpers/Hooks/usePrevious'; import { inputTypes, kinds } from 'Helpers/Props'; import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import { useShowAdvancedSettings } from 'Settings/advancedSettingsStore'; -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 { SelectedSchema } from 'Settings/useProviderSchema'; +import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs'; import translate from 'Utilities/String/translate'; +import { useManageIndexer } from '../useIndexers'; import styles from './EditIndexerModalContent.css'; export interface EditIndexerModalContentProps { id?: number; + cloneId?: number; + selectedSchema?: SelectedSchema; onModalClose: () => void; onDeleteIndexerPress?: () => void; } function EditIndexerModalContent({ id, + cloneId, + selectedSchema, onModalClose, onDeleteIndexerPress, }: EditIndexerModalContentProps) { - const dispatch = useDispatch(); const showAdvancedSettings = useShowAdvancedSettings(); const { - isFetching, - error, - isSaving, - isTesting = false, - saveError, item, + updateFieldValue, + updateValue, + saveProvider, + isSaving, + saveError, + testProvider, + isTesting, validationErrors, validationWarnings, - } = useSelector( - createProviderSettingsSelectorHook('indexers', id) - ); + } = useManageIndexer(id, cloneId, selectedSchema); const wasSaving = usePrevious(isSaving); @@ -77,27 +70,30 @@ function EditIndexerModalContent({ const handleInputChange = useCallback( (change: InputChanged) => { - // @ts-expect-error - actions are not typed - dispatch(setIndexerValue(change)); + // @ts-expect-error - InputChanged is not typed correctly + updateValue(change.name, change.value); }, - [dispatch] + [updateValue] ); const handleFieldChange = useCallback( - (change: InputChanged) => { - // @ts-expect-error - actions are not typed - dispatch(setIndexerFieldValue(change)); + ({ + name, + value, + additionalProperties, + }: EnhancedSelectInputChanged) => { + updateFieldValue({ [name]: value, ...additionalProperties }); }, - [dispatch] + [updateFieldValue] ); const handleSavePress = useCallback(() => { - dispatch(saveIndexer({ id })); - }, [id, dispatch]); + saveProvider(); + }, [saveProvider]); const handleTestPress = useCallback(() => { - dispatch(testIndexer({ id })); - }, [id, dispatch]); + testProvider(); + }, [testProvider]); useEffect(() => { if (!isSaving && wasSaving && !saveError) { @@ -114,169 +110,152 @@ function EditIndexerModalContent({ - {isFetching ? : null} +
+ + {translate('Name')} - {!isFetching && error ? ( - {translate('AddIndexerError')} - ) : null} + + - {!isFetching && !error ? ( - - - {translate('Name')} + + {translate('EnableRss')} - + + + + {translate('EnableAutomaticSearch')} + + + + + + {translate('EnableInteractiveSearch')} + + + + + {fields?.map((field) => { + return ( + - + ); + })} - - {translate('EnableRss')} + + {translate('IndexerPriority')} - - + + - - {translate('EnableAutomaticSearch')} + + {translate('MaximumSingleEpisodeAge')} - - + + - - {translate('EnableInteractiveSearch')} + + {translate('DownloadClient')} - - + + - {fields?.map((field) => { - return ( - - ); - })} + + {translate('Tags')} - - {translate('IndexerPriority')} - - - - - - {translate('MaximumSingleEpisodeAge')} - - - - - - {translate('DownloadClient')} - - - - - - {translate('Tags')} - - - - - ) : null} + + +
diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.tsx b/frontend/src/Settings/Indexers/Indexers/Indexer.tsx index a9f068888..0a4b7f62b 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexer.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.tsx @@ -1,15 +1,13 @@ import React, { useCallback, useState } from 'react'; -import { useDispatch } 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 { useTagList } from 'Tags/useTags'; -import IndexerModel from 'typings/Indexer'; import translate from 'Utilities/String/translate'; +import { IndexerModel, useDeleteIndexer } from '../useIndexers'; import EditIndexerModal from './EditIndexerModal'; import styles from './Indexer.css'; @@ -31,8 +29,8 @@ function Indexer({ showPriority, onCloneIndexerPress, }: IndexerProps) { - const dispatch = useDispatch(); const tagList = useTagList(); + const { deleteIndexer } = useDeleteIndexer(id); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = @@ -56,8 +54,8 @@ function Indexer({ }, []); const handleConfirmDeleteIndexer = useCallback(() => { - dispatch(deleteIndexer({ id })); - }, [id, dispatch]); + deleteIndexer(); + }, [deleteIndexer]); const handleCloneIndexerPress = useCallback(() => { onCloneIndexerPress(id); diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.tsx b/frontend/src/Settings/Indexers/Indexers/Indexers.tsx index 9483e2ff8..8c9cb4286 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexers.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.tsx @@ -1,49 +1,42 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { IndexerAppState } from 'App/State/SettingsAppState'; +import React, { useCallback, useState } 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 { cloneIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import IndexerModel from 'typings/Indexer'; -import sortByProp from 'Utilities/Array/sortByProp'; +import { SelectedSchema } from 'Settings/useProviderSchema'; import translate from 'Utilities/String/translate'; +import { useSortedIndexers } from '../useIndexers'; 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 { isFetching, isFetched, data, error } = useSortedIndexers(); const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); + const [cloneIndexerId, setCloneIndexerId] = useState(null); - const showPriority = items.some((index) => index.priority !== 25); + const showPriority = data.some((index) => index.priority !== 25); + + const [selectedSchema, setSelectedSchema] = useState< + SelectedSchema | undefined + >(undefined); const handleAddIndexerPress = useCallback(() => { + setCloneIndexerId(null); setIsAddIndexerModalOpen(true); }, []); - const handleCloneIndexerPress = useCallback( - (id: number) => { - dispatch(cloneIndexer({ id })); - setIsEditIndexerModalOpen(true); - }, - [dispatch] - ); + const handleCloneIndexerPress = useCallback((id: number) => { + setCloneIndexerId(id); + setIsEditIndexerModalOpen(true); + }, []); - const handleIndexerSelect = useCallback(() => { + const handleIndexerSelect = useCallback((selected: SelectedSchema) => { + setSelectedSchema(selected); setIsAddIndexerModalOpen(false); setIsEditIndexerModalOpen(true); }, []); @@ -53,23 +46,20 @@ function Indexers() { }, []); const handleEditIndexerModalClose = useCallback(() => { + setCloneIndexerId(null); setIsEditIndexerModalOpen(false); }, []); - useEffect(() => { - dispatch(fetchIndexers()); - }, [dispatch]); - return (
- {items.map((item) => { + {data.map((item) => { return ( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 24ea41a28..31a0b6785 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { SelectProvider, useSelect } from 'App/Select/SelectContext'; -import { IndexerAppState } from 'App/State/SettingsAppState'; import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; import SpinnerButton from 'Components/Link/SpinnerButton'; @@ -16,12 +14,16 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { kinds } from 'Helpers/Props'; import { - bulkDeleteIndexers, - bulkEditIndexers, + IndexerModel, + useBulkDeleteIndexers, + useBulkEditIndexers, + useIndexersData, + useSortedIndexers, +} from 'Settings/Indexers/useIndexers'; +import { setManageIndexersSort, -} from 'Store/Actions/settingsActions'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import Indexer from 'typings/Indexer'; + useManageIndexersOptions, +} from 'Settings/Indexers/useManageIndexersOptionsStore'; import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; @@ -94,19 +96,11 @@ function ManageIndexersModalContentInner( ) { const { onModalClose } = props; - const { - isFetching, - isPopulated, - isDeleting, - isSaving, - error, - items, - sortKey, - sortDirection, - }: IndexerAppState = useSelector( - createClientSideCollectionSelector('settings.indexers') - ); - const dispatch = useDispatch(); + const { sortKey, sortDirection } = useManageIndexersOptions(); + const { data, isFetching, isFetched, error } = useSortedIndexers(); + + const { isDeleting, bulkDeleteIndexers } = useBulkDeleteIndexers(); + const { isSaving, bulkEditIndexers } = useBulkEditIndexers(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); @@ -121,14 +115,11 @@ function ManageIndexersModalContentInner( selectAll, unselectAll, useSelectedIds, - } = useSelect(); + } = useSelect(); - const onSortPress = useCallback( - (value: string) => { - dispatch(setManageIndexersSort({ sortKey: value })); - }, - [dispatch] - ); + const onSortPress = useCallback((value: string) => { + setManageIndexersSort({ sortKey: value }); + }, []); const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); @@ -147,22 +138,20 @@ function ManageIndexersModalContentInner( }, [setIsEditModalOpen]); const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteIndexers({ ids: getSelectedIds() })); + bulkDeleteIndexers({ ids: getSelectedIds() }); setIsDeleteModalOpen(false); - }, [getSelectedIds, dispatch]); + }, [bulkDeleteIndexers, getSelectedIds]); const onSavePress = useCallback( (payload: object) => { setIsEditModalOpen(false); - dispatch( - bulkEditIndexers({ - ids: getSelectedIds(), - ...payload, - }) - ); + bulkEditIndexers({ + ids: getSelectedIds(), + ...payload, + }); }, - [getSelectedIds, dispatch] + [getSelectedIds, bulkEditIndexers] ); const onTagsPress = useCallback(() => { @@ -178,15 +167,13 @@ function ManageIndexersModalContentInner( setIsSavingTags(true); setIsTagsModalOpen(false); - dispatch( - bulkEditIndexers({ - ids: getSelectedIds(), - tags, - applyTags, - }) - ); + bulkEditIndexers({ + ids: getSelectedIds(), + tags, + applyTags, + }); }, - [getSelectedIds, dispatch] + [getSelectedIds, bulkEditIndexers] ); const onSelectAllChange = useCallback( @@ -211,11 +198,11 @@ function ManageIndexersModalContentInner( {error ?
{errorMessage}
: null} - {isPopulated && !error && !items.length ? ( + {isFetched && !error && !data.length ? ( {translate('NoIndexersFound')} ) : null} - {isPopulated && !!items.length && !isFetching && !isFetching ? ( + {isFetched && !!data.length && !isFetching && !isFetching ? ( - {items.map((item) => { + {data.map((item) => { return ( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx index 730b510cd..f87467044 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx @@ -7,7 +7,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; import { kinds } from 'Helpers/Props'; -import Indexer from 'typings/Indexer'; +import { IndexerModel } from 'Settings/Indexers/useIndexers'; import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; import styles from './ManageIndexersModalRow.css'; @@ -38,7 +38,7 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) { tags, } = props; - const { toggleSelected, useIsSelected } = useSelect(); + const { toggleSelected, useIsSelected } = useSelect(); const isSelected = useIsSelected(id); const onSelectedChangeWrapper = useCallback( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx index a3657b524..96a7097ff 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Tags/TagsModalContent.tsx @@ -1,8 +1,5 @@ import { uniq } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import { IndexerAppState } from 'App/State/SettingsAppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -15,8 +12,8 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import { useIndexersData } from 'Settings/Indexers/useIndexers'; import { Tag, useTagList } from 'Tags/useTags'; -import Indexer from 'typings/Indexer'; import translate from 'Utilities/String/translate'; import styles from './TagsModalContent.css'; @@ -26,12 +23,31 @@ interface TagsModalContentProps { onModalClose: () => void; } +const applyTagsOptions: EnhancedSelectInputValue[] = [ + { + key: 'add', + get value() { + return translate('Add'); + }, + }, + { + key: 'remove', + get value() { + return translate('Remove'); + }, + }, + { + key: 'replace', + get value() { + return translate('Replace'); + }, + }, +]; + function TagsModalContent(props: TagsModalContentProps) { const { ids, onModalClose, onApplyTagsPress } = props; - const allIndexers: IndexerAppState = useSelector( - (state: AppState) => state.settings.indexers - ); + const allIndexers = useIndexersData(); const tagList: Tag[] = useTagList(); const [tags, setTags] = useState([]); @@ -39,7 +55,7 @@ function TagsModalContent(props: TagsModalContentProps) { const indexersTags = useMemo(() => { const tags = ids.reduce((acc: number[], id) => { - const s = allIndexers.items.find((s: Indexer) => s.id === id); + const s = allIndexers.find((s) => s.id === id); if (s) { acc.push(...s.tags); @@ -69,27 +85,6 @@ function TagsModalContent(props: TagsModalContentProps) { onApplyTagsPress(tags, applyTags); }, [tags, applyTags, onApplyTagsPress]); - const applyTagsOptions: EnhancedSelectInputValue[] = [ - { - key: 'add', - get value() { - return translate('Add'); - }, - }, - { - key: 'remove', - get value() { - return translate('Remove'); - }, - }, - { - key: 'replace', - get value() { - return translate('Replace'); - }, - }, - ]; - return ( {translate('Tags')} diff --git a/frontend/src/Settings/Indexers/useIndexers.ts b/frontend/src/Settings/Indexers/useIndexers.ts new file mode 100644 index 000000000..b0bf26064 --- /dev/null +++ b/frontend/src/Settings/Indexers/useIndexers.ts @@ -0,0 +1,274 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import DownloadProtocol from 'DownloadClient/DownloadProtocol'; +import useApiMutation from 'Helpers/Hooks/useApiMutation'; +import { + SelectedSchema, + useProviderSchema, + useSelectedSchema, +} from 'Settings/useProviderSchema'; +import { + useDeleteProvider, + useManageProviderSettings, + useProviderSettings, +} from 'Settings/useProviderSettings'; +import Provider from 'typings/Provider'; +import { sortByProp } from 'Utilities/Array/sortByProp'; +import { ApiError } from 'Utilities/Fetch/fetchJson'; +import translate from 'Utilities/String/translate'; + +export interface IndexerModel extends Provider { + enableRss: boolean; + enableAutomaticSearch: boolean; + enableInteractiveSearch: boolean; + supportsRss: boolean; + supportsSearch: boolean; + seasonSearchMaximumSingleEpisodeAge: number; + protocol: DownloadProtocol; + priority: number; + downloadClientId: number; + tags: number[]; +} + +interface BulkEditIndexersPayload { + ids: number[]; + [key: string]: unknown; +} + +interface BulkDeleteIndexersPayload { + ids: number[]; +} + +const PATH = '/indexer'; + +export const useIndexersWithIds = (ids: number[]) => { + const allIndexers = useIndexersData(); + + return allIndexers.filter((indexer) => ids.includes(indexer.id)); +}; + +export const useIndexer = (id: number | undefined) => { + const { data } = useIndexers(); + + if (id === undefined) { + return undefined; + } + + return data.find((indexer) => indexer.id === id); +}; + +export const useIndexersData = () => { + const { data } = useIndexers(); + + return data; +}; + +export const useSortedIndexers = () => { + const result = useIndexers(); + + const sortedData = useMemo( + () => result.data.sort(sortByProp('name')), + [result.data] + ); + + return { + ...result, + data: sortedData, + }; +}; + +export const useIndexers = () => { + return useProviderSettings({ + path: PATH, + }); +}; + +export const useManageIndexer = ( + id: number | undefined, + cloneId: number | undefined, + selectedSchema?: SelectedSchema +) => { + const schema = useSelectedSchema(PATH, selectedSchema); + const cloneIndexer = useIndexer(cloneId); + + if (cloneId && !cloneIndexer) { + throw new Error(`Indexer with ID ${cloneId} not found`); + } + + if (selectedSchema && !schema) { + throw new Error('A selected schema is required to manage metadata'); + } + + const defaultProvider = useMemo(() => { + if (cloneId && cloneIndexer) { + const clonedIndexer = { + ...cloneIndexer, + id: 0, + name: translate('DefaultNameCopiedProfile', { + name: cloneIndexer.name, + }), + }; + + clonedIndexer.fields = clonedIndexer.fields.map((field) => { + const newField = { ...field }; + + if (newField.privacy === 'apiKey' || newField.privacy === 'password') { + newField.value = ''; + } + + return newField; + }); + + return clonedIndexer; + } + + if (selectedSchema && schema) { + return { + ...schema, + name: schema.implementationName, + enableRss: schema.supportsRss, + enableAutomaticSearch: schema.supportsSearch, + enableInteractiveSearch: schema.supportsSearch, + }; + } + + return {} as IndexerModel; + }, [cloneId, cloneIndexer, schema, selectedSchema]); + + const manage = useManageProviderSettings( + id, + defaultProvider, + PATH + ); + + return manage; +}; + +export const useDeleteIndexer = (id: number) => { + const result = useDeleteProvider(id, PATH); + + return { + ...result, + deleteIndexer: result.deleteProvider, + }; +}; + +export const useIndexerSchema = (enabled: boolean = true) => { + return useProviderSchema(PATH, enabled); +}; + +export const useTestIndexer = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const { mutate, isPending, error } = useApiMutation({ + path: `${PATH}/test`, + method: 'POST', + mutationOptions: { + onSuccess, + onError, + }, + }); + + return { + testIndexer: mutate, + isTesting: isPending, + testError: error, + }; +}; + +export const useTestAllIndexers = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const { mutate, isPending, error } = useApiMutation({ + path: `${PATH}/testall`, + method: 'POST', + mutationOptions: { + onSuccess, + onError, + }, + }); + + return { + testAllIndexers: mutate, + isTestingAllIndexers: isPending, + testAllError: error, + }; +}; + +export const useBulkEditIndexers = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const queryClient = useQueryClient(); + + const { mutate, isPending, error } = useApiMutation< + IndexerModel[], + BulkEditIndexersPayload + >({ + path: `${PATH}/bulk`, + method: 'PUT', + mutationOptions: { + onSuccess: (updatedIndexers) => { + queryClient.setQueryData([PATH], (oldIndexers) => { + if (!oldIndexers) { + return oldIndexers; + } + + return oldIndexers.map((indexer) => { + const updatedIndexer = updatedIndexers.find( + (updated) => updated.id === indexer.id + ); + + return updatedIndexer ? { ...indexer, ...updatedIndexer } : indexer; + }); + }); + onSuccess?.(); + }, + onError, + }, + }); + + return { + bulkEditIndexers: mutate, + isSaving: isPending, + bulkError: error, + }; +}; + +export const useBulkDeleteIndexers = ( + onSuccess?: () => void, + onError?: (error: ApiError) => void +) => { + const queryClient = useQueryClient(); + + const { mutate, isPending, error } = useApiMutation< + void, + BulkDeleteIndexersPayload + >({ + path: `${PATH}/bulk`, + method: 'DELETE', + mutationOptions: { + onSuccess: (_, variables) => { + const deletedIds = new Set(variables.ids); + + queryClient.setQueryData([PATH], (oldIndexers) => { + if (!oldIndexers) { + return oldIndexers; + } + + return oldIndexers.filter((indexer) => !deletedIds.has(indexer.id)); + }); + onSuccess?.(); + }, + onError, + }, + }); + + return { + bulkDeleteIndexers: mutate, + isDeleting: isPending, + bulkDeleteError: error, + }; +}; diff --git a/frontend/src/Settings/Indexers/useManageIndexersOptionsStore.ts b/frontend/src/Settings/Indexers/useManageIndexersOptionsStore.ts new file mode 100644 index 000000000..6f95426d1 --- /dev/null +++ b/frontend/src/Settings/Indexers/useManageIndexersOptionsStore.ts @@ -0,0 +1,20 @@ +import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore'; +import { SortDirection } from 'Helpers/Props/sortDirections'; + +export interface ManageIndexersOptions { + sortKey: string; + sortDirection: SortDirection; +} + +const { useOptions, setSort } = createOptionsStore( + 'manage_indexers_options', + () => { + return { + sortKey: 'name', + sortDirection: 'ascending', + }; + } +); + +export const useManageIndexersOptions = useOptions; +export const setManageIndexersSort = setSort; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx index e126a285e..ede25bbaf 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.tsx @@ -15,7 +15,7 @@ interface AddNotificationItemProps { implementationName: string; infoLink: string; presets?: NotificationModel[]; - onNotificationSelect: (selectedScehema: SelectedSchema) => void; + onNotificationSelect: (selectedSchema: SelectedSchema) => void; } function AddNotificationItem({ diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx index 55e4dc6e8..7dca3e2c1 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.tsx @@ -14,7 +14,7 @@ import AddNotificationItem from './AddNotificationItem'; import styles from './AddNotificationModalContent.css'; export interface AddNotificationModalContentProps { - onNotificationSelect: (selectedScehema: SelectedSchema) => void; + onNotificationSelect: (selectedSchema: SelectedSchema) => void; onModalClose: () => void; } diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx index a94b24247..2c5dc00d8 100644 --- a/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.tsx @@ -6,7 +6,7 @@ interface AddNotificationPresetMenuItemProps { name: string; implementation: string; implementationName: string; - onPress: (selectedScehema: SelectedSchema) => void; + onPress: (selectedSchema: SelectedSchema) => void; } function AddNotificationPresetMenuItem({ diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx index 00752acad..4cf2d711f 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.tsx @@ -1,14 +1,10 @@ -import React, { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React from 'react'; import Modal from 'Components/Modal/Modal'; import { sizes } from 'Helpers/Props'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; import EditNotificationModalContent, { EditNotificationModalContentProps, } from './EditNotificationModalContent'; -const section = 'settings.notifications'; - interface EditNotificationModalProps extends EditNotificationModalContentProps { isOpen: boolean; } @@ -18,18 +14,11 @@ function EditNotificationModal({ onModalClose, ...otherProps }: EditNotificationModalProps) { - const dispatch = useDispatch(); - - const handleModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section })); - onModalClose(); - }, [dispatch, onModalClose]); - return ( - + ); diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx index 73bf7b756..c2c5eb85c 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.tsx @@ -37,9 +37,9 @@ function EditNotificationModalContent({ }: EditNotificationModalContentProps) { const showAdvancedSettings = useShowAdvancedSettings(); - const result = useManageConnection(id, selectedSchema); const { item, + updateFieldValue, updateValue, saveProvider, isSaving, @@ -48,12 +48,7 @@ function EditNotificationModalContent({ isTesting, validationErrors, validationWarnings, - } = result; - - // updateFieldValue is guaranteed to exist for NotificationModel since it extends Provider - const { updateFieldValue } = result as typeof result & { - updateFieldValue: (fieldProperties: Record) => void; - }; + } = useManageConnection(id, selectedSchema); const wasSaving = usePrevious(isSaving); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx index ae718b272..efb14c546 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfileItem.tsx @@ -6,8 +6,8 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import TagList from 'Components/TagList'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import { kinds } from 'Helpers/Props'; +import { IndexerModel } from 'Settings/Indexers/useIndexers'; import { Tag } from 'Tags/useTags'; -import Indexer from 'typings/Indexer'; import translate from 'Utilities/String/translate'; import EditReleaseProfileModal from './EditReleaseProfileModal'; import { @@ -18,7 +18,7 @@ import styles from './ReleaseProfileItem.css'; interface ReleaseProfileProps extends ReleaseProfileModel { tagList: Tag[]; - indexerList: Indexer[]; + indexerList: IndexerModel[]; } function ReleaseProfileItem(props: ReleaseProfileProps) { diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx index 877b09d3f..759394361 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx @@ -1,13 +1,11 @@ -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import React from 'react'; import Card from 'Components/Card'; import FieldSet from 'Components/FieldSet'; import Icon from 'Components/Icon'; import PageSectionContent from 'Components/Page/PageSectionContent'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import { icons } from 'Helpers/Props'; -import { fetchIndexers } from 'Store/Actions/settingsActions'; +import { useIndexersData } from 'Settings/Indexers/useIndexers'; import { useTagList } from 'Tags/useTags'; import translate from 'Utilities/String/translate'; import EditReleaseProfileModal from './EditReleaseProfileModal'; @@ -16,13 +14,10 @@ import { useReleaseProfiles } from './useReleaseProfiles'; import styles from './ReleaseProfiles.css'; function ReleaseProfiles() { - const dispatch = useDispatch(); const { data, isFetching, isFetched, error } = useReleaseProfiles(); const tagList = useTagList(); - const indexerList = useSelector( - (state: AppState) => state.settings.indexers.items - ); + const indexerList = useIndexersData(); const [ isAddReleaseProfileModalOpen, @@ -30,10 +25,6 @@ function ReleaseProfiles() { setAddReleaseProfileModalClosed, ] = useModalOpenState(false); - useEffect(() => { - dispatch(fetchIndexers()); - }, [dispatch]); - return (
state.settings.indexers.items - ) - ); + const indexers = useIndexersWithIds(indexerIds); const downloadClients = useSelector( createMatchingItemSelector( diff --git a/frontend/src/Settings/Tags/Tags.tsx b/frontend/src/Settings/Tags/Tags.tsx index 73c89bcc0..5cdc51faa 100644 --- a/frontend/src/Settings/Tags/Tags.tsx +++ b/frontend/src/Settings/Tags/Tags.tsx @@ -5,13 +5,13 @@ import Alert from 'Components/Alert'; import FieldSet from 'Components/FieldSet'; import PageSectionContent from 'Components/Page/PageSectionContent'; import { kinds } from 'Helpers/Props'; +import { useIndexers } from 'Settings/Indexers/useIndexers'; import { useConnections } from 'Settings/Notifications/useConnections'; import { useReleaseProfiles } from 'Settings/Profiles/Release/useReleaseProfiles'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, - fetchIndexers, } from 'Store/Actions/settingsActions'; import useTagDetails from 'Tags/useTagDetails'; import useTags, { useSortedTagList } from 'Tags/useTags'; @@ -33,11 +33,11 @@ function Tags() { useReleaseProfiles(); useConnections(); + useIndexers(); useEffect(() => { dispatch(fetchDelayProfiles()); dispatch(fetchImportLists()); - dispatch(fetchIndexers()); dispatch(fetchDownloadClients()); queryClient.invalidateQueries({ queryKey: ['releaseprofile'] }); diff --git a/frontend/src/Settings/useProviderSettings.ts b/frontend/src/Settings/useProviderSettings.ts index 0756f9300..06ef539eb 100644 --- a/frontend/src/Settings/useProviderSettings.ts +++ b/frontend/src/Settings/useProviderSettings.ts @@ -10,7 +10,7 @@ import { PendingSection } from 'typings/pending'; import Provider from 'typings/Provider'; import { ApiError } from 'Utilities/Fetch/fetchJson'; -interface ManageProviderSettings +interface BaseManageProviderSettings extends Omit>, 'settings'> { item: PendingSection; updateValue: (key: K, value: T[K]) => void; @@ -19,9 +19,17 @@ interface ManageProviderSettings saveError: ApiError | null; testProvider: () => void; isTesting: boolean; - updateFieldValue?: (fieldProperties: Record) => void; } +interface ManageProviderSettingsWithFields + extends BaseManageProviderSettings { + updateFieldValue: (fieldProperties: Record) => void; +} + +type ManageProviderSettings = T extends Provider + ? ManageProviderSettingsWithFields + : BaseManageProviderSettings; + const isProviderWithFields = (provider: unknown): provider is Provider => { return ( typeof provider === 'object' && @@ -296,10 +304,10 @@ export const useManageProviderSettings = ( return { ...baseReturn, updateFieldValue, - }; + } as ManageProviderSettings; } - return baseReturn; + return baseReturn as ManageProviderSettings; }; export const useDeleteProvider = ( diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js deleted file mode 100644 index a277e013f..000000000 --- a/frontend/src/Store/Actions/Settings/indexers.js +++ /dev/null @@ -1,180 +0,0 @@ -import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; -import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; -import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; -import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; -import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler'; -import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; -import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; -import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; -import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; -import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer'; -import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; -import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; -import { createThunk } from 'Store/thunks'; -import getSectionState from 'Utilities/State/getSectionState'; -import selectProviderSchema from 'Utilities/State/selectProviderSchema'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import translate from 'Utilities/String/translate'; - -// -// Variables - -const section = 'settings.indexers'; - -// -// Actions Types - -export const FETCH_INDEXERS = 'settings/indexers/fetchIndexers'; -export const FETCH_INDEXER_SCHEMA = 'settings/indexers/fetchIndexerSchema'; -export const SELECT_INDEXER_SCHEMA = 'settings/indexers/selectIndexerSchema'; -export const CLONE_INDEXER = 'settings/indexers/cloneIndexer'; -export const SET_INDEXER_VALUE = 'settings/indexers/setIndexerValue'; -export const SET_INDEXER_FIELD_VALUE = 'settings/indexers/setIndexerFieldValue'; -export const SAVE_INDEXER = 'settings/indexers/saveIndexer'; -export const CANCEL_SAVE_INDEXER = 'settings/indexers/cancelSaveIndexer'; -export const DELETE_INDEXER = 'settings/indexers/deleteIndexer'; -export const TEST_INDEXER = 'settings/indexers/testIndexer'; -export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer'; -export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers'; -export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers'; -export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers'; -export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort'; - -// -// Action Creators - -export const fetchIndexers = createThunk(FETCH_INDEXERS); -export const fetchIndexerSchema = createThunk(FETCH_INDEXER_SCHEMA); -export const selectIndexerSchema = createAction(SELECT_INDEXER_SCHEMA); -export const cloneIndexer = createAction(CLONE_INDEXER); - -export const saveIndexer = createThunk(SAVE_INDEXER); -export const cancelSaveIndexer = createThunk(CANCEL_SAVE_INDEXER); -export const deleteIndexer = createThunk(DELETE_INDEXER); -export const testIndexer = createThunk(TEST_INDEXER); -export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER); -export const testAllIndexers = createThunk(TEST_ALL_INDEXERS); -export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS); -export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS); -export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT); - -export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => { - return { - section, - ...payload - }; -}); - -export const setIndexerFieldValue = createAction(SET_INDEXER_FIELD_VALUE, (payload) => { - return { - section, - ...payload - }; -}); - -// -// Details - -export default { - - // - // State - - defaultState: { - isFetching: false, - isPopulated: false, - error: null, - isSchemaFetching: false, - isSchemaPopulated: false, - schemaError: null, - schema: [], - selectedSchema: {}, - isSaving: false, - saveError: null, - isDeleting: false, - deleteError: null, - isTesting: false, - isTestingAll: false, - items: [], - pendingChanges: {}, - sortKey: 'name', - sortDirection: sortDirections.ASCENDING, - sortPredicates: { - name: ({ name }) => { - return name.toLocaleLowerCase(); - } - } - }, - - // - // Action Handlers - - actionHandlers: { - [FETCH_INDEXERS]: createFetchHandler(section, '/indexer'), - [FETCH_INDEXER_SCHEMA]: createFetchSchemaHandler(section, '/indexer/schema'), - - [SAVE_INDEXER]: createSaveProviderHandler(section, '/indexer'), - [CANCEL_SAVE_INDEXER]: createCancelSaveProviderHandler(section), - [DELETE_INDEXER]: createRemoveItemHandler(section, '/indexer'), - [TEST_INDEXER]: createTestProviderHandler(section, '/indexer'), - [CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section), - [TEST_ALL_INDEXERS]: createTestAllProvidersHandler(section, '/indexer'), - [BULK_EDIT_INDEXERS]: createBulkEditItemHandler(section, '/indexer/bulk'), - [BULK_DELETE_INDEXERS]: createBulkRemoveItemHandler(section, '/indexer/bulk') - }, - - // - // Reducers - - reducers: { - [SET_INDEXER_VALUE]: createSetSettingValueReducer(section), - [SET_INDEXER_FIELD_VALUE]: createSetProviderFieldValueReducer(section), - - [SELECT_INDEXER_SCHEMA]: (state, { payload }) => { - return selectProviderSchema(state, section, payload, (selectedSchema) => { - selectedSchema.name = payload.presetName ?? payload.implementationName; - selectedSchema.implementationName = payload.implementationName; - selectedSchema.enableRss = selectedSchema.supportsRss; - selectedSchema.enableAutomaticSearch = selectedSchema.supportsSearch; - selectedSchema.enableInteractiveSearch = selectedSchema.supportsSearch; - - return selectedSchema; - }); - }, - - [CLONE_INDEXER]: function(state, { payload }) { - const id = payload.id; - const newState = getSectionState(state, section); - const item = newState.items.find((i) => i.id === id); - - // Use selectedSchema so `createProviderSettingsSelector` works properly - const selectedSchema = { ...item }; - delete selectedSchema.id; - delete selectedSchema.name; - - selectedSchema.fields = selectedSchema.fields.map((field) => { - const newField = { ...field }; - - if (newField.privacy === 'apiKey' || newField.privacy === 'password') { - newField.value = ''; - } - - return newField; - }); - - newState.selectedSchema = selectedSchema; - - // Set the name in pendingChanges - newState.pendingChanges = { - name: translate('DefaultNameCopiedProfile', { name: item.name }) - }; - - return updateSectionState(state, section, newState); - }, - - [SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section) - - } - -}; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 7bd3794f8..35d04611e 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -12,7 +12,6 @@ import importListOptions from './Settings/importListOptions'; import importLists from './Settings/importLists'; import indexerFlags from './Settings/indexerFlags'; import indexerOptions from './Settings/indexerOptions'; -import indexers from './Settings/indexers'; export * from './Settings/autoTaggingSpecifications'; export * from './Settings/autoTaggings'; @@ -26,7 +25,6 @@ export * from './Settings/importLists'; export * from './Settings/importListExclusions'; export * from './Settings/indexerFlags'; export * from './Settings/indexerOptions'; -export * from './Settings/indexers'; // // Variables @@ -49,8 +47,7 @@ export const defaultState = { importListExclusions: importListExclusions.defaultState, importListOptions: importListOptions.defaultState, indexerFlags: indexerFlags.defaultState, - indexerOptions: indexerOptions.defaultState, - indexers: indexers.defaultState + indexerOptions: indexerOptions.defaultState }; export const persistState = [ @@ -72,8 +69,7 @@ export const actionHandlers = handleThunks({ ...importListExclusions.actionHandlers, ...importListOptions.actionHandlers, ...indexerFlags.actionHandlers, - ...indexerOptions.actionHandlers, - ...indexers.actionHandlers + ...indexerOptions.actionHandlers }); // @@ -91,7 +87,6 @@ export const reducers = createHandleActions({ ...importListExclusions.reducers, ...importListOptions.reducers, ...indexerFlags.reducers, - ...indexerOptions.reducers, - ...indexers.reducers + ...indexerOptions.reducers }, defaultState, section); diff --git a/frontend/src/System/Status/Health/Health.tsx b/frontend/src/System/Status/Health/Health.tsx index f6743e664..fc334b542 100644 --- a/frontend/src/System/Status/Health/Health.tsx +++ b/frontend/src/System/Status/Health/Health.tsx @@ -14,10 +14,8 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import TableRow from 'Components/Table/TableRow'; import { icons, kinds } from 'Helpers/Props'; -import { - testAllDownloadClients, - testAllIndexers, -} from 'Store/Actions/settingsActions'; +import { useTestAllIndexers } from 'Settings/Indexers/useIndexers'; +import { testAllDownloadClients } from 'Store/Actions/settingsActions'; import titleCase from 'Utilities/String/titleCase'; import translate from 'Utilities/String/translate'; import HealthItemLink from './HealthItemLink'; @@ -49,9 +47,8 @@ function Health() { const isTestingAllDownloadClients = useSelector( (state: AppState) => state.settings.downloadClients.isTestingAll ); - const isTestingAllIndexers = useSelector( - (state: AppState) => state.settings.indexers.isTestingAll - ); + + const { testAllIndexers, isTestingAllIndexers } = useTestAllIndexers(); const healthIssues = !!data.length; @@ -60,8 +57,8 @@ function Health() { }, [dispatch]); const handleTestAllIndexersPress = useCallback(() => { - dispatch(testAllIndexers()); - }, [dispatch]); + testAllIndexers(); + }, [testAllIndexers]); return (