1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-18 21:35:27 -04:00

Use react-query for Release Profiles

This commit is contained in:
Mark McDowall
2025-12-29 15:00:33 -08:00
parent f963a0d972
commit 4713615b17
10 changed files with 111 additions and 207 deletions
@@ -27,7 +27,6 @@ import IndexerOptions from 'typings/Settings/IndexerOptions';
import MediaManagement from 'typings/Settings/MediaManagement';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import MetadataAppState from './MetadataAppState';
type Presets<T> = T & {
@@ -116,12 +115,6 @@ export interface QualityProfilesAppState
AppSectionDeleteState,
AppSectionSaveState {}
export interface ReleaseProfilesAppState
extends AppSectionState<ReleaseProfile>,
AppSectionSaveState {
pendingChanges: Partial<ReleaseProfile>;
}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
@@ -171,7 +164,6 @@ interface SettingsAppState {
notifications: NotificationAppState;
qualityDefinitions: QualityDefinitionsAppState;
qualityProfiles: QualityProfilesAppState;
releaseProfiles: ReleaseProfilesAppState;
}
export default SettingsAppState;
@@ -1,7 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -14,55 +11,13 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { inputTypes, kinds } from 'Helpers/Props';
import {
saveReleaseProfile,
setReleaseProfileValue,
} from 'Store/Actions/Settings/releaseProfiles';
import selectSettings from 'Store/Selectors/selectSettings';
import { InputChanged } from 'typings/inputs';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import translate from 'Utilities/String/translate';
import { useManageReleaseProfile } from './useReleaseProfiles';
import styles from './EditReleaseProfileModalContent.css';
const tagInputDelimiters = ['Tab', 'Enter'];
const newReleaseProfile: ReleaseProfile = {
id: 0,
name: '',
enabled: true,
required: [],
ignored: [],
tags: [],
excludedTags: [],
indexerId: 0,
};
function createReleaseProfileSelector(id?: number) {
return createSelector(
(state: AppState) => state.settings.releaseProfiles,
(releaseProfiles) => {
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
releaseProfiles;
const mapping = id ? items.find((i) => i.id === id)! : newReleaseProfile;
const settings = selectSettings<ReleaseProfile>(
mapping,
pendingChanges,
saveError
);
return {
isFetching,
error,
isSaving,
saveError,
item: settings.settings,
...settings,
};
}
);
}
interface EditReleaseProfileModalContentProps {
id?: number;
onModalClose: () => void;
@@ -74,43 +29,39 @@ function EditReleaseProfileModalContent({
onModalClose,
onDeleteReleaseProfilePress,
}: EditReleaseProfileModalContentProps) {
const { item, isFetching, isSaving, error, saveError, ...otherProps } =
useSelector(createReleaseProfileSelector(id));
const {
item,
isSaving,
saveError,
validationErrors,
validationWarnings,
updateValue,
saveProvider,
} = useManageReleaseProfile(id ?? 0);
const { name, enabled, required, ignored, tags, excludedTags, indexerId } =
item;
const dispatch = useDispatch();
const previousIsSaving = usePrevious(isSaving);
useEffect(() => {
if (!id) {
Object.entries(newReleaseProfile).forEach(([name, value]) => {
// @ts-expect-error 'setReleaseProfileValue' isn't typed yet
dispatch(setReleaseProfileValue({ name, value }));
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (previousIsSaving && !isSaving && !saveError) {
onModalClose();
}
}, [previousIsSaving, isSaving, saveError, onModalClose]);
const handleSavePress = useCallback(() => {
dispatch(saveReleaseProfile({ id }));
}, [dispatch, id]);
const wasSaving = usePrevious(isSaving);
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error 'setReleaseProfileValue' isn't typed yet
dispatch(setReleaseProfileValue(change));
// @ts-expect-error - change is not yet typed
updateValue(change.name, change.value);
},
[dispatch]
[updateValue]
);
const handleSavePress = useCallback(() => {
saveProvider();
}, [saveProvider]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -118,7 +69,10 @@ function EditReleaseProfileModalContent({
</ModalHeader>
<ModalBody>
<Form {...otherProps}>
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
@@ -219,6 +173,7 @@ function EditReleaseProfileModalContent({
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
{id ? (
<Button
@@ -1,5 +1,4 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import MiddleTruncate from 'Components/MiddleTruncate';
@@ -7,15 +6,17 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { kinds } from 'Helpers/Props';
import { deleteReleaseProfile } from 'Store/Actions/Settings/releaseProfiles';
import { Tag } from 'Tags/useTags';
import Indexer from 'typings/Indexer';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import translate from 'Utilities/String/translate';
import EditReleaseProfileModal from './EditReleaseProfileModal';
import {
ReleaseProfileModel,
useDeleteReleaseProfile,
} from './useReleaseProfiles';
import styles from './ReleaseProfileItem.css';
interface ReleaseProfileProps extends ReleaseProfile {
interface ReleaseProfileProps extends ReleaseProfileModel {
tagList: Tag[];
indexerList: Indexer[];
}
@@ -34,7 +35,7 @@ function ReleaseProfileItem(props: ReleaseProfileProps) {
indexerList,
} = props;
const dispatch = useDispatch();
const { deleteReleaseProfile } = useDeleteReleaseProfile(id);
const [
isEditReleaseProfileModalOpen,
@@ -49,8 +50,8 @@ function ReleaseProfileItem(props: ReleaseProfileProps) {
] = useModalOpenState(false);
const handleDeletePress = useCallback(() => {
dispatch(deleteReleaseProfile({ id }));
}, [id, dispatch]);
deleteReleaseProfile();
}, [deleteReleaseProfile]);
const indexer =
indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
@@ -1,50 +1,39 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import React from 'react';
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { ReleaseProfilesAppState } from 'App/State/SettingsAppState';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import { fetchIndexers } from 'Store/Actions/Settings/indexers';
import { fetchReleaseProfiles } from 'Store/Actions/Settings/releaseProfiles';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { useTagList } from 'Tags/useTags';
import translate from 'Utilities/String/translate';
import EditReleaseProfileModal from './EditReleaseProfileModal';
import ReleaseProfileItem from './ReleaseProfileItem';
import { useReleaseProfiles } from './useReleaseProfiles';
import styles from './ReleaseProfiles.css';
function ReleaseProfiles() {
const { items, isFetching, isPopulated, error }: ReleaseProfilesAppState =
useSelector(createClientSideCollectionSelector('settings.releaseProfiles'));
const { data, isFetching, isFetched, error } = useReleaseProfiles();
const tagList = useTagList();
const indexerList = useSelector(
(state: AppState) => state.settings.indexers.items
);
const dispatch = useDispatch();
const [
isAddReleaseProfileModalOpen,
setAddReleaseProfileModalOpen,
setAddReleaseProfileModalClosed,
] = useModalOpenState(false);
useEffect(() => {
dispatch(fetchReleaseProfiles());
dispatch(fetchIndexers());
}, [dispatch]);
return (
<FieldSet legend={translate('ReleaseProfiles')}>
<PageSectionContent
errorMessage={translate('ReleaseProfilesLoadError')}
isFetching={isFetching}
isPopulated={isPopulated}
isPopulated={isFetched}
error={error}
>
<div className={styles.releaseProfiles}>
@@ -57,7 +46,7 @@ function ReleaseProfiles() {
</div>
</Card>
{items.map((item) => {
{data.map((item) => {
return (
<ReleaseProfileItem
key={item.id}
@@ -0,0 +1,58 @@
import ModelBase from 'App/ModelBase';
import {
useDeleteProvider,
useManageProviderSettings,
useProviderSettings,
} from 'Settings/useProviderSettings';
export interface ReleaseProfileModel extends ModelBase {
name: string;
enabled: boolean;
required: string[];
ignored: string[];
indexerId: number;
tags: number[];
excludedTags: number[];
}
const PATH = '/releaseprofile';
const NEW_RELEASE_PROFILE: ReleaseProfileModel = {
id: 0,
name: '',
enabled: true,
required: [],
ignored: [],
indexerId: 0,
tags: [],
excludedTags: [],
};
export const useReleaseProfilesWithIds = (ids: number[]) => {
const allReleaseProfiles = useReleaseProfiles();
return allReleaseProfiles.data.filter((releaseProfiles) =>
ids.includes(releaseProfiles.id)
);
};
export const useReleaseProfiles = () => {
return useProviderSettings<ReleaseProfileModel>(PATH);
};
export const useManageReleaseProfile = (id: number) => {
return useManageProviderSettings<ReleaseProfileModel>(
id,
NEW_RELEASE_PROFILE,
PATH
);
};
export const useDeleteReleaseProfile = (id: number) => {
const result = useDeleteProvider<ReleaseProfileModel>(id, PATH);
return {
...result,
deleteReleaseProfile: result.deleteProvider,
};
};
@@ -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 { useReleaseProfilesWithIds } from 'Settings/Profiles/Release/useReleaseProfiles';
import translate from 'Utilities/String/translate';
import TagDetailsDelayProfile from './TagDetailsDelayProfile';
import styles from './TagDetailsModalContent.css';
@@ -102,12 +103,7 @@ function TagDetailsModalContent({
)
);
const releaseProfiles = useSelector(
createMatchingItemSelector(
releaseProfileIds,
(state: AppState) => state.settings.releaseProfiles.items
)
);
const releaseProfiles = useReleaseProfilesWithIds(releaseProfileIds);
const indexers = useSelector(
createMatchingItemSelector(
+5 -3
View File
@@ -1,3 +1,4 @@
import { useQueryClient } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import Alert from 'Components/Alert';
@@ -10,7 +11,6 @@ import {
fetchImportLists,
fetchIndexers,
fetchNotifications,
fetchReleaseProfiles,
} from 'Store/Actions/settingsActions';
import useTagDetails from 'Tags/useTagDetails';
import useTags, { useSortedTagList } from 'Tags/useTags';
@@ -20,6 +20,7 @@ import styles from './Tags.css';
function Tags() {
const dispatch = useDispatch();
const queryClient = useQueryClient();
const { isFetching, isFetched, error } = useTags();
const items = useSortedTagList();
@@ -33,10 +34,11 @@ function Tags() {
dispatch(fetchDelayProfiles());
dispatch(fetchImportLists());
dispatch(fetchNotifications());
dispatch(fetchReleaseProfiles());
dispatch(fetchIndexers());
dispatch(fetchDownloadClients());
}, [dispatch]);
queryClient.invalidateQueries({ queryKey: ['releaseprofile'] });
}, [dispatch, queryClient]);
if (!items.length) {
return (
@@ -1,71 +0,0 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.releaseProfiles';
//
// Actions Types
export const FETCH_RELEASE_PROFILES = 'settings/releaseProfiles/fetchReleaseProfiles';
export const SAVE_RELEASE_PROFILE = 'settings/releaseProfiles/saveReleaseProfile';
export const DELETE_RELEASE_PROFILE = 'settings/releaseProfiles/deleteReleaseProfile';
export const SET_RELEASE_PROFILE_VALUE = 'settings/releaseProfiles/setReleaseProfileValue';
//
// Action Creators
export const fetchReleaseProfiles = createThunk(FETCH_RELEASE_PROFILES);
export const saveReleaseProfile = createThunk(SAVE_RELEASE_PROFILE);
export const deleteReleaseProfile = createThunk(DELETE_RELEASE_PROFILE);
export const setReleaseProfileValue = createAction(SET_RELEASE_PROFILE_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_RELEASE_PROFILES]: createFetchHandler(section, '/releaseprofile'),
[SAVE_RELEASE_PROFILE]: createSaveProviderHandler(section, '/releaseprofile'),
[DELETE_RELEASE_PROFILE]: createRemoveItemHandler(section, '/releaseprofile')
},
//
// Reducers
reducers: {
[SET_RELEASE_PROFILE_VALUE]: createSetSettingValueReducer(section)
}
};
@@ -22,7 +22,6 @@ import namingExamples from './Settings/namingExamples';
import notifications from './Settings/notifications';
import qualityDefinitions from './Settings/qualityDefinitions';
import qualityProfiles from './Settings/qualityProfiles';
import releaseProfiles from './Settings/releaseProfiles';
export * from './Settings/autoTaggingSpecifications';
export * from './Settings/autoTaggings';
@@ -46,7 +45,6 @@ export * from './Settings/namingExamples';
export * from './Settings/notifications';
export * from './Settings/qualityDefinitions';
export * from './Settings/qualityProfiles';
export * from './Settings/releaseProfiles';
//
// Variables
@@ -79,8 +77,7 @@ export const defaultState = {
namingExamples: namingExamples.defaultState,
notifications: notifications.defaultState,
qualityDefinitions: qualityDefinitions.defaultState,
qualityProfiles: qualityProfiles.defaultState,
releaseProfiles: releaseProfiles.defaultState
qualityProfiles: qualityProfiles.defaultState
};
export const persistState = [
@@ -112,8 +109,7 @@ export const actionHandlers = handleThunks({
...namingExamples.actionHandlers,
...notifications.actionHandlers,
...qualityDefinitions.actionHandlers,
...qualityProfiles.actionHandlers,
...releaseProfiles.actionHandlers
...qualityProfiles.actionHandlers
});
//
@@ -141,7 +137,6 @@ export const reducers = createHandleActions({
...namingExamples.reducers,
...notifications.reducers,
...qualityDefinitions.reducers,
...qualityProfiles.reducers,
...releaseProfiles.reducers
...qualityProfiles.reducers
}, defaultState, section);
@@ -1,13 +0,0 @@
import ModelBase from 'App/ModelBase';
interface ReleaseProfile extends ModelBase {
name: string;
enabled: boolean;
required: string[];
ignored: string[];
indexerId: number;
tags: number[];
excludedTags: number[];
}
export default ReleaseProfile;