1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-25 22:46:31 -04:00

Use react-query for Quality Definitions

This commit is contained in:
Mark McDowall
2025-12-29 23:38:30 -08:00
parent 243a3057ae
commit cf593b1f5d
17 changed files with 320 additions and 370 deletions
@@ -1,31 +1,29 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import TextInput from 'Components/Form/TextInput';
import Quality from 'Quality/Quality';
import { setQualityDefinitionValue } from 'Store/Actions/settingsActions';
import { useManageQualityDefinitions } from './useQualityDefinitions';
import styles from './QualityDefinition.css';
interface QualityDefinitionProps {
id: number;
quality: Quality;
title: string;
updateDefinition: ReturnType<
typeof useManageQualityDefinitions
>['updateDefinition'];
}
function QualityDefinition(props: QualityDefinitionProps) {
const { id, quality, title } = props;
const dispatch = useDispatch();
function QualityDefinition({
id,
quality,
title,
updateDefinition,
}: QualityDefinitionProps) {
const handleTitleChange = useCallback(
({ value }: { value: string }) => {
dispatch(
setQualityDefinitionValue({
id,
name: 'title',
value,
})
);
updateDefinition(id, 'title', value);
},
[id, dispatch]
[id, updateDefinition]
);
return (
@@ -1,42 +1,17 @@
import { isEmpty } from 'lodash';
import { useQueryClient } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FieldSet from 'Components/FieldSet';
import PageSectionContent from 'Components/Page/PageSectionContent';
import usePrevious from 'Helpers/Hooks/usePrevious';
import {
fetchQualityDefinitions,
saveQualityDefinitions,
} from 'Store/Actions/settingsActions';
import {
OnChildStateChange,
SetChildSave,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
import QualityDefinition from './QualityDefinition';
import { useManageQualityDefinitions } from './useQualityDefinitions';
import styles from './QualityDefinitions.css';
function createQualityDefinitionsSelector() {
return createSelector(
(state: AppState) => state.settings.qualityDefinitions,
(qualityDefinitions) => {
const items = qualityDefinitions.items.map((item) => {
const pendingChanges = qualityDefinitions.pendingChanges[item.id] || {};
return Object.assign({}, item, pendingChanges);
});
return {
...qualityDefinitions,
items,
hasPendingChanges: !isEmpty(qualityDefinitions.pendingChanges),
};
}
);
}
interface QualityDefinitionsProps {
isResettingQualityDefinitions: boolean;
setChildSave: SetChildSave;
@@ -48,21 +23,27 @@ function QualityDefinitions({
setChildSave,
onChildStateChange,
}: QualityDefinitionsProps) {
const dispatch = useDispatch();
const { items, isFetching, isPopulated, isSaving, error, hasPendingChanges } =
useSelector(createQualityDefinitionsSelector());
const queryClient = useQueryClient();
const {
items,
isFetching,
isFetched,
isSaving,
error,
hasPendingChanges,
updateDefinition,
saveQualityDefinitions,
} = useManageQualityDefinitions();
const wasResettingQualityDefinitions = usePrevious(
isResettingQualityDefinitions
);
useEffect(() => {
dispatch(fetchQualityDefinitions());
setChildSave(() => {
dispatch(saveQualityDefinitions());
saveQualityDefinitions();
});
}, [dispatch, setChildSave]);
}, [saveQualityDefinitions, setChildSave]);
useEffect(() => {
onChildStateChange({
@@ -73,16 +54,20 @@ function QualityDefinitions({
useEffect(() => {
if (wasResettingQualityDefinitions && !isResettingQualityDefinitions) {
dispatch(fetchQualityDefinitions());
queryClient.invalidateQueries({ queryKey: ['/qualitydefinition'] });
}
}, [isResettingQualityDefinitions, wasResettingQualityDefinitions, dispatch]);
}, [
isResettingQualityDefinitions,
wasResettingQualityDefinitions,
queryClient,
]);
return (
<FieldSet legend={translate('QualityDefinitions')}>
<PageSectionContent
errorMessage={translate('QualityDefinitionsLoadError')}
isFetching={isFetching}
isPopulated={isPopulated}
isPopulated={isFetched}
error={error}
>
<div className={styles.header}>
@@ -92,7 +77,13 @@ function QualityDefinitions({
<div className={styles.definitions}>
{items.map((item) => {
return <QualityDefinition key={item.id} {...item} />;
return (
<QualityDefinition
key={item.id}
{...item}
updateDefinition={updateDefinition}
/>
);
})}
</div>
</PageSectionContent>
@@ -0,0 +1,99 @@
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { usePendingItemsStore } from 'Helpers/Hooks/usePendingItemsStore';
import QualityDefinitionModel from 'Quality/QualityDefinitionModel';
import { useSaveSettings } from 'Settings/useSettings';
const PATH = '/qualitydefinition';
const DEFAULT_QUALITY_DEFINITIONS: QualityDefinitionModel[] = [];
export const useQualityDefinitions = () => {
const result = useApiQuery<QualityDefinitionModel[]>({
path: PATH,
});
return {
...result,
data: result.data ?? DEFAULT_QUALITY_DEFINITIONS,
};
};
export const useSaveQualityDefinitions = (onSuccess?: () => void) => {
const queryClient = useQueryClient();
const { mutate, isPending, error } = useApiMutation<
QualityDefinitionModel[],
QualityDefinitionModel[]
>({
path: PATH,
method: 'PUT',
mutationOptions: {
onSuccess: (updatedSettings: QualityDefinitionModel[]) => {
queryClient.setQueryData<QualityDefinitionModel[]>(
[PATH],
updatedSettings
);
onSuccess?.();
},
},
});
return {
save: mutate,
isSaving: isPending,
saveError: error,
};
};
export const useManageQualityDefinitions = () => {
const { data, isFetching, isFetched, error } = useQualityDefinitions();
const {
setPendingItem,
clearPendingItems,
getItemsWithPendingChanges,
getPendingChangesForSave,
hasPendingChanges,
} = usePendingItemsStore<QualityDefinitionModel>();
const { save, isSaving, saveError } = useSaveSettings(
PATH,
clearPendingItems
);
const settings = useMemo(() => {
return {
items: getItemsWithPendingChanges(data),
hasPendingChanges,
};
}, [data, getItemsWithPendingChanges, hasPendingChanges]);
const saveQualityDefinitions = useCallback(() => {
const updatedSettings = getPendingChangesForSave(data);
save(updatedSettings);
}, [data, getPendingChangesForSave, save]);
const updateDefinition = useCallback(
<K extends keyof QualityDefinitionModel>(
id: number,
key: keyof QualityDefinitionModel,
value: QualityDefinitionModel[K]
) => {
const originalItem = data.find((def) => def.id === id);
setPendingItem(id, key, value, originalItem);
},
[data, setPendingItem]
);
return {
...settings,
updateDefinition,
saveQualityDefinitions,
isFetching,
isFetched,
isSaving,
error,
saveError,
};
};
+20 -5
View File
@@ -1,6 +1,7 @@
import React, { useCallback, useRef, useState } from 'react';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting } from 'Commands/useCommands';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -13,9 +14,9 @@ import {
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
import QualityDefinitions from './Definition/QualityDefinitions';
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
function Quality() {
const executeCommand = useExecuteCommand();
const isResettingQualityDefinitions = useCommandExecuting(
CommandNames.ResetQualityDefinitions
);
@@ -51,6 +52,15 @@ function Quality() {
setIsConfirmQualityDefinitionResetModalOpen(false);
}, []);
const handleResetQualityDefinitionsConfirmed = useCallback(() => {
executeCommand({
name: CommandNames.ResetQualityDefinitions,
resetTitles: true,
});
setIsConfirmQualityDefinitionResetModalOpen(false);
}, [executeCommand]);
const handleSavePress = useCallback(() => {
saveDefinitions.current?.();
}, []);
@@ -68,13 +78,13 @@ function Quality() {
label={translate('ResetDefinitions')}
iconName={icons.REFRESH}
isSpinning={isResettingQualityDefinitions}
isDisabled={isResettingQualityDefinitions}
onPress={handleResetQualityDefinitionsPress}
/>
</>
}
onSavePress={handleSavePress}
/>
<PageContentBody>
<QualityDefinitions
isResettingQualityDefinitions={isResettingQualityDefinitions}
@@ -83,9 +93,14 @@ function Quality() {
/>
</PageContentBody>
<ResetQualityDefinitionsModal
<ConfirmModal
isOpen={isConfirmQualityDefinitionResetModalOpen}
onModalClose={handleCloseResetQualityDefinitionsModal}
kind="danger"
title={translate('ResetQualityDefinitions')}
message={translate('ResetQualityDefinitionsMessageText')}
confirmLabel={translate('Reset')}
onConfirm={handleResetQualityDefinitionsConfirmed}
onCancel={handleCloseResetQualityDefinitionsModal}
/>
</PageContent>
);
@@ -1,22 +0,0 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import ResetQualityDefinitionsModalContent from './ResetQualityDefinitionsModalContent';
interface ResetQualityDefinitionsModalProps {
isOpen: boolean;
onModalClose: () => void;
}
function ResetQualityDefinitionsModal({
isOpen,
onModalClose,
}: ResetQualityDefinitionsModalProps) {
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<ResetQualityDefinitionsModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ResetQualityDefinitionsModal;
@@ -1,3 +0,0 @@
.messageContainer {
margin-bottom: 20px;
}
@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'messageContainer': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,87 +0,0 @@
import React, { useCallback, useState } from 'react';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './ResetQualityDefinitionsModalContent.css';
interface ResetQualityDefinitionsModalContentProps {
onModalClose: () => void;
}
function ResetQualityDefinitionsModalContent({
onModalClose,
}: ResetQualityDefinitionsModalContentProps) {
const executeCommand = useExecuteCommand();
const isResettingQualityDefinitions = useCommandExecuting(
CommandNames.ResetQualityDefinitions
);
const [resetDefinitionTitles, setResetDefinitionTitles] = useState(false);
const handleResetDefinitionTitlesChange = useCallback(
({ value }: InputChanged<boolean>) => {
setResetDefinitionTitles(value);
},
[]
);
const handleResetQualityDefinitionsConfirmed = useCallback(() => {
const resetTitles = resetDefinitionTitles;
setResetDefinitionTitles(false);
executeCommand({
name: CommandNames.ResetQualityDefinitions,
resetTitles,
});
onModalClose();
}, [resetDefinitionTitles, executeCommand, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('ResetQualityDefinitions')}</ModalHeader>
<ModalBody>
<div className={styles.messageContainer}>
{translate('ResetQualityDefinitionsMessageText')}
</div>
<FormGroup>
<FormLabel>{translate('ResetTitles')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="resetDefinitionTitles"
value={resetDefinitionTitles}
helpText={translate('ResetDefinitionTitlesHelpText')}
onChange={handleResetDefinitionTitlesChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button
kind={kinds.DANGER}
isDisabled={isResettingQualityDefinitions}
onPress={handleResetQualityDefinitionsConfirmed}
>
{translate('Reset')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default ResetQualityDefinitionsModalContent;