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:
@@ -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;
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
274
frontend/src/Settings/Indexers/useIndexers.ts
Normal file
274
frontend/src/Settings/Indexers/useIndexers.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -15,7 +15,7 @@ interface AddNotificationItemProps {
|
||||
implementationName: string;
|
||||
infoLink: string;
|
||||
presets?: NotificationModel[];
|
||||
onNotificationSelect: (selectedScehema: SelectedSchema) => void;
|
||||
onNotificationSelect: (selectedSchema: SelectedSchema) => void;
|
||||
}
|
||||
|
||||
function AddNotificationItem({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ interface AddNotificationPresetMenuItemProps {
|
||||
name: string;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
onPress: (selectedScehema: SelectedSchema) => void;
|
||||
onPress: (selectedSchema: SelectedSchema) => void;
|
||||
}
|
||||
|
||||
function AddNotificationPresetMenuItem({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'] });
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user