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

Use react-query for Quality Profiles

This commit is contained in:
Mark McDowall
2025-12-29 16:54:05 -08:00
parent f4b9b30978
commit 21ca65a015
30 changed files with 318 additions and 403 deletions
@@ -7,6 +7,7 @@ import EditQualityProfileModalContent from './EditQualityProfileModalContent';
interface EditQualityProfileModalProps {
id?: number;
cloneId?: number;
isOpen: boolean;
onDeleteQualityProfilePress?: () => void;
onModalClose: () => void;
@@ -14,6 +15,7 @@ interface EditQualityProfileModalProps {
function EditQualityProfileModal({
id,
cloneId,
isOpen,
onDeleteQualityProfilePress,
onModalClose,
@@ -44,6 +46,7 @@ function EditQualityProfileModal({
>
<EditQualityProfileModalContent
id={id}
cloneId={cloneId}
onContentHeightChange={handleContentHeightChange}
onDeleteQualityProfilePress={onDeleteQualityProfilePress}
onModalClose={handleOnModalClose}
@@ -1,6 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { QualityProfilesAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -17,18 +15,8 @@ import useMeasure from 'Helpers/Hooks/useMeasure';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import useQualityProfileInUse from 'Settings/Profiles/Quality/useQualityProfileInUse';
import {
fetchQualityProfileSchema,
saveQualityProfile,
setQualityProfileValue,
} from 'Store/Actions/settingsActions';
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
import dimensions from 'Styles/Variables/dimensions';
import { InputChanged } from 'typings/inputs';
import QualityProfile, {
QualityProfileGroup,
QualityProfileQualityItem,
} from 'typings/QualityProfile';
import translate from 'Utilities/String/translate';
import QualityProfileFormatItems from './QualityProfileFormatItems';
import { DragMoveState } from './QualityProfileItemDragSource';
@@ -36,6 +24,11 @@ import QualityProfileItems, {
EditQualityProfileMode,
} from './QualityProfileItems';
import { SizeChanged } from './QualityProfileItemSize';
import {
QualityProfileGroup,
QualityProfileQualityItem,
useManageQualityProfile,
} from './useQualityProfiles';
import styles from './EditQualityProfileModalContent.css';
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
@@ -52,6 +45,7 @@ function parseIndex(index: string): [number | null, number] {
interface EditQualityProfileModalContentProps {
id?: number;
cloneId?: number;
onContentHeightChange: (height: number) => void;
onDeleteQualityProfilePress?: () => void;
onModalClose: () => void;
@@ -59,19 +53,21 @@ interface EditQualityProfileModalContentProps {
function EditQualityProfileModalContent({
id,
cloneId,
onContentHeightChange,
onDeleteQualityProfilePress,
onModalClose,
}: EditQualityProfileModalContentProps) {
const dispatch = useDispatch();
const { error, isFetching, isPopulated, isSaving, saveError, item } =
useSelector(
createProviderSettingsSelectorHook<
QualityProfile,
QualityProfilesAppState
>('qualityProfiles', id)
);
const {
item,
isSaving,
saveError,
isSchemaFetching,
isSchemaFetched,
schemaError,
updateValue,
saveProvider,
} = useManageQualityProfile(id, cloneId);
const isInUse = useQualityProfileInUse(id);
@@ -132,15 +128,15 @@ function EditQualityProfileModalContent({
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name, value }));
// @ts-expect-error - change is not yet typed
updateValue(name, value);
},
[dispatch]
[updateValue]
);
const handleSavePress = useCallback(() => {
dispatch(saveQualityProfile({ id }));
}, [id, dispatch]);
saveProvider();
}, [saveProvider]);
const handleCutoffChange = useCallback(
({ name, value }: InputChanged<number>) => {
@@ -153,10 +149,10 @@ function EditQualityProfileModalContent({
'id' in cutoffItem ? cutoffItem.id : cutoffItem.quality.id;
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name, value: cutoffId }));
updateValue(name, cutoffId);
}
},
[items, dispatch]
[items, updateValue]
);
const handleItemAllowedChange = useCallback(
@@ -172,15 +168,9 @@ function EditQualityProfileModalContent({
return item;
});
dispatch(
// @ts-expect-error - actions are not typed
setQualityProfileValue({
name: 'items',
value: newItems,
})
);
updateValue('items', newItems);
},
[items, dispatch]
[items, updateValue]
);
const handleGroupAllowedChange = useCallback(
@@ -196,15 +186,9 @@ function EditQualityProfileModalContent({
return item;
});
dispatch(
// @ts-expect-error - actions are not typed
setQualityProfileValue({
name: 'items',
value: newItems,
})
);
updateValue('items', newItems);
},
[items, dispatch]
[items, updateValue]
);
const handleGroupNameChange = useCallback(
@@ -220,10 +204,9 @@ function EditQualityProfileModalContent({
return item;
});
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
updateValue('items', newItems);
},
[items, dispatch]
[items, updateValue]
);
const handleSizeChange = useCallback(
@@ -253,15 +236,9 @@ function EditQualityProfileModalContent({
};
});
dispatch(
// @ts-expect-error - actions are not typed
setQualityProfileValue({
name: 'items',
value: newItems,
})
);
updateValue('items', newItems);
},
[items, dispatch]
[items, updateValue]
);
const handleCreateGroupPress = useCallback(
@@ -288,10 +265,9 @@ function EditQualityProfileModalContent({
return item;
});
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
updateValue('items', newItems);
},
[items, dispatch]
[items, updateValue]
);
const handleDeleteGroupPress = useCallback(
@@ -308,10 +284,9 @@ function EditQualityProfileModalContent({
[]
);
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
updateValue('items', newItems);
},
[items, dispatch]
[items, updateValue]
);
const handleDragMove = useCallback((options: DragMoveState) => {
@@ -443,13 +418,7 @@ function EditQualityProfileModalContent({
dropGroup.items.splice(dropItemIndex, 0, item);
}
dispatch(
// @ts-expect-error - actions are not typed
setQualityProfileValue({
name: 'items',
value: newItems,
})
);
updateValue('items', newItems);
}
setDndState({
@@ -458,7 +427,7 @@ function EditQualityProfileModalContent({
dropPosition: null,
});
},
[dragQualityIndex, dropQualityIndex, items, dispatch]
[dragQualityIndex, dropQualityIndex, items, updateValue]
);
const handleChangeMode = useCallback((newMode: EditQualityProfileMode) => {
@@ -478,15 +447,9 @@ function EditQualityProfileModalContent({
return formatItem;
});
dispatch(
// @ts-expect-error - actions are not typed
setQualityProfileValue({
name: 'formatItems',
value: newFormatItems,
})
);
updateValue('formatItems', newFormatItems);
},
[formatItems, dispatch]
[formatItems, updateValue]
);
useEffect(() => {
@@ -523,12 +486,6 @@ function EditQualityProfileModalContent({
}
}, [bodyHeight, mode]);
useEffect(() => {
if (!id && !isPopulated) {
dispatch(fetchQualityProfileSchema());
}
}, [id, isPopulated, dispatch]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
onModalClose();
@@ -554,11 +511,10 @@ function EditQualityProfileModalContent({
cutoffId =
'id' in firstAllowed ? firstAllowed.id : firstAllowed.quality.id;
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name: 'cutoff', value: cutoffId }));
updateValue('cutoff', cutoffId);
}
}
}, [cutoff, items, dispatch]);
}, [cutoff, items, updateValue]);
return (
<ModalContent onModalClose={onModalClose}>
@@ -568,15 +524,15 @@ function EditQualityProfileModalContent({
<ModalBody>
<div ref={measureBodyRef}>
{isPopulated ? null : <LoadingIndicator />}
{isSchemaFetched ? null : <LoadingIndicator />}
{!isFetching && error ? (
{!isSchemaFetching && schemaError ? (
<Alert kind={kinds.DANGER}>
{translate('AddQualityProfileError')}
</Alert>
) : null}
{isPopulated && !error ? (
{isSchemaFetched && !schemaError ? (
<Form>
<div className={styles.formGroupsContainer}>
<div className={styles.formGroupWrapper}>
@@ -1,15 +1,16 @@
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 Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { deleteQualityProfile } from 'Store/Actions/settingsActions';
import { QualityProfileItems } from 'typings/QualityProfile';
import translate from 'Utilities/String/translate';
import EditQualityProfileModal from './EditQualityProfileModal';
import {
QualityProfileItems,
useDeleteQualityProfile,
} from './useQualityProfiles';
import styles from './QualityProfile.css';
interface QualityProfileProps {
@@ -18,7 +19,6 @@ interface QualityProfileProps {
upgradeAllowed: boolean;
cutoff: number;
items: QualityProfileItems;
isDeleting: boolean;
onCloneQualityProfilePress: (id: number) => void;
}
@@ -32,7 +32,7 @@ function QualityProfile({
isDeleting,
onCloneQualityProfilePress,
}: QualityProfileProps) {
const dispatch = useDispatch();
const { deleteQualityProfile } = useDeleteQualityProfile(id);
const [isEditQualityProfileModalOpen, setIsEditQualityProfileModalOpen] =
useState(false);
@@ -57,8 +57,8 @@ function QualityProfile({
}, []);
const handleConfirmDeleteQualityProfile = useCallback(() => {
dispatch(deleteQualityProfile({ id }));
}, [id, dispatch]);
deleteQualityProfile();
}, [deleteQualityProfile]);
const handleCloneQualityProfilePress = useCallback(() => {
onCloneQualityProfilePress(id);
@@ -4,10 +4,10 @@ import { DragSourceMonitor, useDrag, useDrop, XYCoord } from 'react-dnd';
import DragType from 'Helpers/DragType';
import useMeasure from 'Helpers/Hooks/useMeasure';
import { qualityProfileItemHeight } from 'Styles/Variables/dimensions';
import { QualityProfileQualityItem } from 'typings/QualityProfile';
import QualityProfileItem from './QualityProfileItem';
import QualityProfileItemGroup from './QualityProfileItemGroup';
import { SizeChanged } from './QualityProfileItemSize';
import { QualityProfileQualityItem } from './useQualityProfiles';
import styles from './QualityProfileItemDragSource.css';
export interface DragMoveState {
@@ -8,12 +8,12 @@ import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import { QualityProfileQualityItem } from 'typings/QualityProfile';
import translate from 'Utilities/String/translate';
import QualityProfileItemDragSource, {
DragMoveState,
} from './QualityProfileItemDragSource';
import { SizeChanged } from './QualityProfileItemSize';
import { QualityProfileQualityItem } from './useQualityProfiles';
import styles from './QualityProfileItemGroup.css';
interface QualityProfileItemGroupProps {
@@ -7,11 +7,11 @@ import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props';
import { Failure } from 'typings/pending';
import { QualityProfileItems as Items } from 'typings/QualityProfile';
import translate from 'Utilities/String/translate';
import QualityProfileItemDragSource, {
QualityProfileItemDragSourceActionProps,
} from './QualityProfileItemDragSource';
import { QualityProfileItems as Items } from './useQualityProfiles';
import styles from './QualityProfileItems.css';
export type EditQualityProfileMode = 'default' | 'editGroups' | 'editSizes';
@@ -1,16 +1,13 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
import translate from 'Utilities/String/translate';
import { useQualityProfile } from './useQualityProfiles';
interface QualityProfileNameProps {
qualityProfileId: number;
}
function QualityProfileName({ qualityProfileId }: QualityProfileNameProps) {
const qualityProfile = useSelector(
createQualityProfileSelectorForHook(qualityProfileId)
);
const qualityProfile = useQualityProfile(qualityProfileId);
return <span>{qualityProfile?.name ?? translate('Unknown')}</span>;
}
@@ -1,55 +1,40 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { QualityProfilesAppState } 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 {
cloneQualityProfile,
fetchQualityProfiles,
} from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import QualityProfileModel from 'typings/QualityProfile';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import EditQualityProfileModal from './EditQualityProfileModal';
import QualityProfile from './QualityProfile';
import { useQualityProfiles } from './useQualityProfiles';
import styles from './QualityProfiles.css';
function QualityProfiles() {
const dispatch = useDispatch();
const { data, error, isFetching, isFetched } = useQualityProfiles();
const { error, isFetching, isPopulated, isDeleting, items } = useSelector(
createSortedSectionSelector<QualityProfileModel, QualityProfilesAppState>(
'settings.qualityProfiles',
sortByProp('name')
)
) as QualityProfilesAppState;
// Sort the data by name
const sortedItems = data ? data.sort(sortByProp('name')) : [];
const [isQualityProfileModalOpen, setIsQualityProfileModalOpen] =
useState(false);
const [cloneProfileId, setCloneProfileId] = useState<number | null>(null);
const handleEditQualityProfilePress = useCallback(() => {
const handleAddQualityProfilePress = useCallback(() => {
setCloneProfileId(null);
setIsQualityProfileModalOpen(true);
}, []);
const handleEditQualityProfileClosePress = useCallback(() => {
const handleAddQualityProfileClosePress = useCallback(() => {
setCloneProfileId(null);
setIsQualityProfileModalOpen(false);
}, []);
const handleCloneQualityProfilePress = useCallback(
(id: number) => {
dispatch(cloneQualityProfile({ id }));
setIsQualityProfileModalOpen(true);
},
[dispatch]
);
useEffect(() => {
dispatch(fetchQualityProfiles());
}, [dispatch]);
const handleCloneQualityProfilePress = useCallback((id: number) => {
setCloneProfileId(id);
setIsQualityProfileModalOpen(true);
}, []);
return (
<FieldSet legend={translate('QualityProfiles')}>
@@ -57,15 +42,15 @@ function QualityProfiles() {
errorMessage={translate('QualityProfilesLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
isPopulated={isFetched}
>
<div className={styles.qualityProfiles}>
{items.map((item) => {
{sortedItems.map((item) => {
return (
<QualityProfile
key={item.id}
{...item}
isDeleting={isDeleting}
isDeleting={false}
onCloneQualityProfilePress={handleCloneQualityProfilePress}
/>
);
@@ -73,7 +58,7 @@ function QualityProfiles() {
<Card
className={styles.addQualityProfile}
onPress={handleEditQualityProfilePress}
onPress={handleAddQualityProfilePress}
>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
@@ -83,7 +68,8 @@ function QualityProfiles() {
<EditQualityProfileModal
isOpen={isQualityProfileModalOpen}
onModalClose={handleEditQualityProfileClosePress}
cloneId={cloneProfileId ?? undefined}
onModalClose={handleAddQualityProfileClosePress}
/>
</PageSectionContent>
</FieldSet>
@@ -0,0 +1,130 @@
import ModelBase from 'App/ModelBase';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import Quality from 'Quality/Quality';
import {
useDeleteProvider,
useManageProviderSettings,
useProviderSettings,
} from 'Settings/useProviderSettings';
import { QualityProfileFormatItem } from 'typings/CustomFormat';
import translate from 'Utilities/String/translate';
export interface QualityProfileQualityItem {
quality: Quality;
allowed: boolean;
minSize: number | null;
maxSize: number | null;
preferredSize: number | null;
}
export interface QualityProfileGroup {
id: number;
items: QualityProfileQualityItem[];
allowed: boolean;
name: string;
}
export type QualityProfileItems = (
| QualityProfileQualityItem
| QualityProfileGroup
)[];
export interface QualityProfileModel extends ModelBase {
name: string;
upgradeAllowed: boolean;
cutoff: number;
items: QualityProfileItems;
minFormatScore: number;
cutoffFormatScore: number;
minUpgradeFormatScore: number;
formatItems: QualityProfileFormatItem[];
}
const PATH = '/qualityprofile';
export const useQualityProfile = (id: number | undefined) => {
const { data } = useQualityProfiles();
if (id === undefined) {
return undefined;
}
return data.find((profile) => profile.id === id);
};
export const useQualityProfilesData = () => {
const { data } = useQualityProfiles();
return data;
};
export const useQualityProfiles = () => {
return useProviderSettings<QualityProfileModel>({
path: PATH,
queryOptions: {
gcTime: Infinity,
staleTime: 5 * 60 * 1000,
},
});
};
export const useManageQualityProfile = (
id: number | undefined,
cloneId: number | undefined
) => {
const { schema, isSchemaFetching, isSchemaFetched, schemaError } =
useQualityProfileSchema(cloneId == null);
const profile = useQualityProfile(cloneId);
if (cloneId && !profile) {
throw new Error(`Quality Profile with ID ${cloneId} not found`);
}
const manage = useManageProviderSettings<QualityProfileModel>(
id,
cloneId && profile
? {
...profile,
id: 0,
name: translate('DefaultNameCopiedProfile', {
name: profile.name,
}),
}
: schema,
PATH
);
return {
...manage,
isSchemaFetching: cloneId ? false : isSchemaFetching,
isSchemaFetched: cloneId ? true : isSchemaFetched,
schemaError: cloneId ? undefined : schemaError,
};
};
export const useDeleteQualityProfile = (id: number) => {
const result = useDeleteProvider<QualityProfileModel>(id, PATH);
return {
...result,
deleteQualityProfile: result.deleteProvider,
};
};
export const useQualityProfileSchema = (enabled: boolean) => {
const { isFetching, isFetched, error, data } =
useApiQuery<QualityProfileModel>({
path: `${PATH}/schema`,
queryOptions: {
enabled,
},
});
return {
isSchemaFetching: isFetching,
isSchemaFetched: isFetched,
schemaError: error,
schema: data ?? ({} as QualityProfileModel),
};
};
@@ -37,7 +37,7 @@ export const useReleaseProfilesWithIds = (ids: number[]) => {
};
export const useReleaseProfiles = () => {
return useProviderSettings<ReleaseProfileModel>(PATH);
return useProviderSettings<ReleaseProfileModel>({ path: PATH });
};
export const useManageReleaseProfile = (id: number) => {