1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Use react-query for Indexers

This commit is contained in:
Mark McDowall
2026-02-16 15:48:47 -08:00
parent bcceb22512
commit c4c0ec25ac
32 changed files with 689 additions and 729 deletions

View File

@@ -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<IndexerOptions>,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<Presets<Indexer>> {
isTestingAll: boolean;
}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
@@ -109,7 +100,6 @@ interface SettingsAppState {
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexerOptions: IndexerOptionsAppState;
indexers: IndexerAppState;
}
export default SettingsAppState;

View File

@@ -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<T> = Omit<
function IndexerFilterBuilderRowValue<T>(
props: IndexerFilterBuilderRowValueProps<T>
) {
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 <FilterBuilderRowValue {...props} tagList={tagList} />;
}

View File

@@ -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 (
<EnhancedSelectInput

View File

@@ -14,6 +14,7 @@ import Episode from 'Episode/Episode';
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
import Series from 'Series/Series';
import { IndexerModel } from 'Settings/Indexers/useIndexers';
import { removeItem, updateItem } from 'Store/Actions/baseActions';
import { repopulatePage } from 'Utilities/pagePopulator';
import SignalRLogger from 'Utilities/SignalRLogger';
@@ -256,12 +257,12 @@ function SignalRListener() {
}
if (name === 'indexer') {
const section = 'settings.indexers';
const updatedItem = body.resource as IndexerModel;
if (body.action === 'created' || body.action === 'updated') {
dispatch(updateItem({ section, ...body.resource }));
updateQueryClientItem(queryClient, ['/indexer'], updatedItem, true);
} else if (body.action === 'deleted') {
dispatch(removeItem({ section, id: body.resource.id }));
removeQueryClientItem(queryClient, ['/indexer'], body.resource.id);
}
return;
@@ -521,3 +522,50 @@ const updatePagedItem = <T extends ModelBase>(
}
);
};
const updateQueryClientItem = <T extends ModelBase>(
queryClient: ReturnType<typeof useQueryClient>,
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 = <T extends ModelBase>(
queryClient: ReturnType<typeof useQueryClient>,
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);
});
};

View File

@@ -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 (
<PageContent title={translate('IndexerSettings')}>
@@ -70,7 +65,7 @@ function IndexerSettings() {
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
isSpinning={isTestingAllIndexers}
onPress={handleTestAllIndexersPress}
/>

View File

@@ -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 (
<div className={styles.indexer}>

View File

@@ -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({

View File

@@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
@@ -66,7 +58,7 @@ function AddIndexerModalContent({
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
{isSchemaPopulated && !schemaError ? (
{isSchemaFetched && !schemaError ? (
<div>
<Alert kind={kinds.INFO}>
<div>{translate('SupportedIndexers')}</div>

View File

@@ -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<MenuItemProps, 'children'> {
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 (
<MenuItem {...otherProps} onPress={handlePress}>

View File

@@ -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 (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditIndexerModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}>
<EditIndexerModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}

View File

@@ -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<Indexer, IndexerAppState>('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<unknown>) => {
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({
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
/>
</FormGroup>
{!isFetching && !error ? (
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={
supportsRss.value ? translate('EnableRssHelpText') : undefined
}
helpTextWarning={
supportsRss.value
? undefined
: translate('RssIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={
supportsSearch.value
? translate('EnableAutomaticSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={
supportsSearch.value
? translate('EnableInteractiveSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={handleInputChange}
/>
</FormGroup>
{fields?.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={showAdvancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={handleFieldChange}
/>
</FormGroup>
);
})}
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={
supportsRss.value ? translate('EnableRssHelpText') : undefined
}
helpTextWarning={
supportsRss.value
? undefined
: translate('RssIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={handleInputChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('MaximumSingleEpisodeAge')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={
supportsSearch.value
? translate('EnableAutomaticSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={handleInputChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.NUMBER}
name="seasonSearchMaximumSingleEpisodeAge"
helpText={translate('MaximumSingleEpisodeAgeHelpText')}
min={0}
unit="days"
{...seasonSearchMaximumSingleEpisodeAge}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={
supportsSearch.value
? translate('EnableInteractiveSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={handleInputChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={handleInputChange}
/>
</FormGroup>
{fields?.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={showAdvancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={handleFieldChange}
/>
);
})}
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('MaximumSingleEpisodeAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="seasonSearchMaximumSingleEpisodeAge"
helpText={translate('MaximumSingleEpisodeAgeHelpText')}
min={0}
unit="days"
{...seasonSearchMaximumSingleEpisodeAge}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagSeriesHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
) : null}
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagSeriesHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>

View File

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

View File

@@ -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<IndexerModel, IndexerAppState>(
'settings.indexers',
sortByProp('name')
)
);
const { isFetching, isFetched, data, error } = useSortedIndexers();
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [cloneIndexerId, setCloneIndexerId] = useState<number | null>(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 (
<FieldSet legend={translate('Indexers')}>
<PageSectionContent
errorMessage={translate('IndexersLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
isPopulated={isFetched}
>
<div className={styles.indexers}>
{items.map((item) => {
{data.map((item) => {
return (
<Indexer
key={item.id}
@@ -95,6 +85,8 @@ function Indexers() {
<EditIndexerModal
isOpen={isEditIndexerModalOpen}
cloneId={cloneIndexerId ?? undefined}
selectedSchema={selectedSchema}
onModalClose={handleEditIndexerModalClose}
/>
</PageSectionContent>

View File

@@ -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<Indexer>();
} = useSelect<IndexerModel>();
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 ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length ? (
{isFetched && !error && !data.length ? (
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
) : null}
{isPopulated && !!items.length && !isFetching && !isFetching ? (
{isFetched && !!data.length && !isFetching && !isFetching ? (
<Table
columns={COLUMNS}
horizontalScroll={true}
@@ -228,7 +215,7 @@ function ManageIndexersModalContentInner(
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {
{data.map((item) => {
return (
<ManageIndexersModalRow
key={item.id}
@@ -303,9 +290,7 @@ function ManageIndexersModalContentInner(
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const { items }: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers', 'manageIndexers')
);
const items = useIndexersData();
return (
<SelectProvider items={items}>

View File

@@ -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<Indexer>();
const { toggleSelected, useIsSelected } = useSelect<IndexerModel>();
const isSelected = useIsSelected(id);
const onSelectedChangeWrapper = useCallback(

View File

@@ -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<string>[] = [
{
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<number[]>([]);
@@ -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<string>[] = [
{
key: 'add',
get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('Tags')}</ModalHeader>

View File

@@ -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<IndexerModel>({
path: PATH,
});
};
export const useManageIndexer = (
id: number | undefined,
cloneId: number | undefined,
selectedSchema?: SelectedSchema
) => {
const schema = useSelectedSchema<IndexerModel>(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<IndexerModel>(
id,
defaultProvider,
PATH
);
return manage;
};
export const useDeleteIndexer = (id: number) => {
const result = useDeleteProvider<IndexerModel>(id, PATH);
return {
...result,
deleteIndexer: result.deleteProvider,
};
};
export const useIndexerSchema = (enabled: boolean = true) => {
return useProviderSchema<IndexerModel>(PATH, enabled);
};
export const useTestIndexer = (
onSuccess?: () => void,
onError?: (error: ApiError) => void
) => {
const { mutate, isPending, error } = useApiMutation<void, IndexerModel>({
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<void, void>({
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<IndexerModel[]>([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<IndexerModel[]>([PATH], (oldIndexers) => {
if (!oldIndexers) {
return oldIndexers;
}
return oldIndexers.filter((indexer) => !deletedIds.has(indexer.id));
});
onSuccess?.();
},
onError,
},
});
return {
bulkDeleteIndexers: mutate,
isDeleting: isPending,
bulkDeleteError: error,
};
};

View File

@@ -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<ManageIndexersOptions>(
'manage_indexers_options',
() => {
return {
sortKey: 'name',
sortDirection: 'ascending',
};
}
);
export const useManageIndexersOptions = useOptions;
export const setManageIndexersSort = setSort;

View File

@@ -15,7 +15,7 @@ interface AddNotificationItemProps {
implementationName: string;
infoLink: string;
presets?: NotificationModel[];
onNotificationSelect: (selectedScehema: SelectedSchema) => void;
onNotificationSelect: (selectedSchema: SelectedSchema) => void;
}
function AddNotificationItem({

View File

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

View File

@@ -6,7 +6,7 @@ interface AddNotificationPresetMenuItemProps {
name: string;
implementation: string;
implementationName: string;
onPress: (selectedScehema: SelectedSchema) => void;
onPress: (selectedSchema: SelectedSchema) => void;
}
function AddNotificationPresetMenuItem({

View File

@@ -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 (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}>
<EditNotificationModalContent
{...otherProps}
onModalClose={handleModalClose}
onModalClose={onModalClose}
/>
</Modal>
);

View File

@@ -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<string, unknown>) => void;
};
} = useManageConnection(id, selectedSchema);
const wasSaving = usePrevious(isSaving);

View File

@@ -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) {

View File

@@ -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 (
<FieldSet legend={translate('ReleaseProfiles')}>
<PageSectionContent

View File

@@ -12,6 +12,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import useSeries from 'Series/useSeries';
import { useIndexersWithIds } from 'Settings/Indexers/useIndexers';
import { useConnectionsWithIds } from 'Settings/Notifications/useConnections';
import { useReleaseProfilesWithIds } from 'Settings/Profiles/Release/useReleaseProfiles';
import translate from 'Utilities/String/translate';
@@ -99,13 +100,7 @@ function TagDetailsModalContent({
const releaseProfiles = useReleaseProfilesWithIds(releaseProfileIds);
const notifications = useConnectionsWithIds(notificationIds);
const indexers = useSelector(
createMatchingItemSelector(
indexerIds,
(state: AppState) => state.settings.indexers.items
)
);
const indexers = useIndexersWithIds(indexerIds);
const downloadClients = useSelector(
createMatchingItemSelector(

View File

@@ -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'] });

View File

@@ -10,7 +10,7 @@ import { PendingSection } from 'typings/pending';
import Provider from 'typings/Provider';
import { ApiError } from 'Utilities/Fetch/fetchJson';
interface ManageProviderSettings<T extends ModelBase>
interface BaseManageProviderSettings<T extends ModelBase>
extends Omit<ReturnType<typeof selectSettings<T>>, 'settings'> {
item: PendingSection<T>;
updateValue: <K extends keyof T>(key: K, value: T[K]) => void;
@@ -19,9 +19,17 @@ interface ManageProviderSettings<T extends ModelBase>
saveError: ApiError | null;
testProvider: () => void;
isTesting: boolean;
updateFieldValue?: (fieldProperties: Record<string, unknown>) => void;
}
interface ManageProviderSettingsWithFields<T extends ModelBase>
extends BaseManageProviderSettings<T> {
updateFieldValue: (fieldProperties: Record<string, unknown>) => void;
}
type ManageProviderSettings<T extends ModelBase> = T extends Provider
? ManageProviderSettingsWithFields<T>
: BaseManageProviderSettings<T>;
const isProviderWithFields = (provider: unknown): provider is Provider => {
return (
typeof provider === 'object' &&
@@ -296,10 +304,10 @@ export const useManageProviderSettings = <T extends ModelBase>(
return {
...baseReturn,
updateFieldValue,
};
} as ManageProviderSettings<T>;
}
return baseReturn;
return baseReturn as ManageProviderSettings<T>;
};
export const useDeleteProvider = <T extends ModelBase>(

View File

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

View File

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

View File

@@ -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 (
<FieldSet

View File

@@ -1,17 +0,0 @@
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import Provider from './Provider';
interface Indexer extends Provider {
enableRss: boolean;
enableAutomaticSearch: boolean;
enableInteractiveSearch: boolean;
supportsRss: boolean;
supportsSearch: boolean;
seasonSearchMaximumSingleEpisodeAge: number;
protocol: DownloadProtocol;
priority: number;
downloadClientId: number;
tags: number[];
}
export default Indexer;