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

View file

@ -1,18 +1,17 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router';
import {
setAddSeriesOption,
useAddSeriesOption,
} from 'AddSeries/addSeriesOptionsStore';
import { SelectProvider } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { kinds } from 'Helpers/Props';
import useRootFolders, { useRootFolder } from 'RootFolder/useRootFolders';
import { useQualityProfilesData } from 'Settings/Profiles/Quality/useQualityProfiles';
import translate from 'Utilities/String/translate';
import ImportSeriesFooter from './ImportSeriesFooter';
import { clearImportSeries } from './importSeriesStore';
@ -48,9 +47,7 @@ function ImportSeries() {
};
}, [rootFolders, rootFolderId]);
const qualityProfiles = useSelector(
(state: AppState) => state.settings.qualityProfiles.items
);
const qualityProfiles = useQualityProfilesData();
const defaultQualityProfileId = useAddSeriesOption('qualityProfileId');

View file

@ -1,6 +1,5 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemSchemaState,
AppSectionItemState,
AppSectionListState,
AppSectionSaveState,
@ -8,6 +7,7 @@ import AppSectionState, {
PagedAppSectionState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import { QualityProfileModel } from 'Settings/Profiles/Quality/useQualityProfiles';
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
import CustomFormat from 'typings/CustomFormat';
import CustomFormatSpecification from 'typings/CustomFormatSpecification';
@ -20,7 +20,6 @@ import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityDefinition from 'typings/QualityDefinition';
import QualityProfile from 'typings/QualityProfile';
import DownloadClientOptions from 'typings/Settings/DownloadClientOptions';
import General from 'typings/Settings/General';
import IndexerOptions from 'typings/Settings/IndexerOptions';
@ -105,16 +104,10 @@ export interface QualityDefinitionsAppState
extends AppSectionState<QualityDefinition>,
AppSectionSaveState {
pendingChanges: {
[key: number]: Partial<QualityProfile>;
[key: number]: Partial<QualityProfileModel>;
};
}
export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionItemSchemaState<QualityProfile>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
@ -163,7 +156,6 @@ interface SettingsAppState {
namingExamples: NamingExamplesAppState;
notifications: NotificationAppState;
qualityDefinitions: QualityDefinitionsAppState;
qualityProfiles: QualityProfilesAppState;
}
export default SettingsAppState;

View file

@ -1,7 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import React, { useMemo } from 'react';
import { useQualityProfileSchema } from 'Settings/Profiles/Quality/useQualityProfiles';
import getQualities from 'Utilities/Quality/getQualities';
import FilterBuilderRowValue, {
FilterBuilderRowValueProps,
@ -15,22 +13,12 @@ type QualityFilterBuilderRowValueProps<T> = Omit<
function QualityFilterBuilderRowValue<T>(
props: QualityFilterBuilderRowValueProps<T>
) {
const dispatch = useDispatch();
const { isSchemaPopulated, schema } = useSelector(
(state: AppState) => state.settings.qualityProfiles
);
const { schema } = useQualityProfileSchema(true);
const tagList = useMemo(() => {
return getQualities(schema.items);
}, [schema]);
useEffect(() => {
if (!isSchemaPopulated) {
dispatch(fetchQualityProfileSchema());
}
}, [isSchemaPopulated, dispatch]);
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}

View file

@ -1,21 +1,10 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { useQualityProfilesData } from 'Settings/Profiles/Quality/useQualityProfiles';
import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue, {
FilterBuilderRowValueProps,
} from './FilterBuilderRowValue';
function createQualityProfilesSelector() {
return createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfiles) => {
return qualityProfiles;
}
);
}
type QualityProfileFilterBuilderRowValueProps<T> = Omit<
FilterBuilderRowValueProps<T, number, string>,
'tagList'
@ -24,7 +13,7 @@ type QualityProfileFilterBuilderRowValueProps<T> = Omit<
function QualityProfileFilterBuilderRowValue<T>(
props: QualityProfileFilterBuilderRowValueProps<T>
) {
const qualityProfiles = useSelector(createQualityProfilesSelector());
const qualityProfiles = useQualityProfilesData();
const tagList = qualityProfiles
.map(({ id, name }) => ({ id, name }))

View file

@ -1,10 +1,6 @@
import React, { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { QualityProfilesAppState } from 'App/State/SettingsAppState';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useQualityProfilesData } from 'Settings/Profiles/Quality/useQualityProfiles';
import { EnhancedSelectInputChanged } from 'typings/inputs';
import QualityProfile from 'typings/QualityProfile';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput, {
@ -12,49 +8,46 @@ import EnhancedSelectInput, {
EnhancedSelectInputValue,
} from './EnhancedSelectInput';
function createQualityProfilesSelector(
const useValues = (
includeNoChange: boolean,
includeNoChangeDisabled: boolean,
includeMixed: boolean
) {
return createSelector(
createSortedSectionSelector<QualityProfile, QualityProfilesAppState>(
'settings.qualityProfiles',
sortByProp<QualityProfile, 'name'>('name')
),
(qualityProfiles: QualityProfilesAppState) => {
const values: EnhancedSelectInputValue<number | string>[] =
qualityProfiles.items.map((qualityProfile) => {
return {
key: qualityProfile.id,
value: qualityProfile.name,
};
});
) => {
const qualityProfiles = useQualityProfilesData();
if (includeNoChange) {
values.unshift({
key: 'noChange',
get value() {
return translate('NoChange');
},
isDisabled: includeNoChangeDisabled,
});
}
return useMemo(() => {
const values: EnhancedSelectInputValue<number | string>[] = qualityProfiles
.sort(sortByProp('name'))
.map((qualityProfile) => {
return {
key: qualityProfile.id,
value: qualityProfile.name,
};
});
if (includeMixed) {
values.unshift({
key: 'mixed',
get value() {
return `(${translate('Mixed')})`;
},
isDisabled: true,
});
}
return values;
if (includeNoChange) {
values.unshift({
key: 'noChange',
get value() {
return translate('NoChange');
},
isDisabled: includeNoChangeDisabled,
});
}
);
}
if (includeMixed) {
values.unshift({
key: 'mixed',
get value() {
return `(${translate('Mixed')})`;
},
isDisabled: true,
});
}
return values;
}, [qualityProfiles, includeNoChange, includeNoChangeDisabled, includeMixed]);
};
export interface QualityProfileSelectInputProps
extends Omit<
@ -79,12 +72,10 @@ function QualityProfileSelectInput({
onChange,
...otherProps
}: QualityProfileSelectInputProps) {
const values = useSelector(
createQualityProfilesSelector(
includeNoChange,
includeNoChangeDisabled,
includeMixed
)
const values = useValues(
includeNoChange,
includeNoChangeDisabled,
includeMixed
);
const handleChange = useCallback(

View file

@ -1,5 +1,4 @@
import React from 'react';
import { Error } from 'App/State/AppSectionState';
import { ApiError } from 'Utilities/Fetch/fetchJson';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
@ -12,7 +11,7 @@ interface ErrorPageProps {
seriesError: ApiError | null;
customFiltersError: ApiError | null;
tagsError: ApiError | null;
qualityProfilesError?: Error;
qualityProfilesError: ApiError | null;
uiSettingsError: ApiError | null;
systemStatusError: ApiError | null;
}

View file

@ -6,13 +6,13 @@ import { useTranslations } from 'App/useTranslations';
import useCommands from 'Commands/useCommands';
import useCustomFilters from 'Filters/useCustomFilters';
import useSeries from 'Series/useSeries';
import { useQualityProfiles } from 'Settings/Profiles/Quality/useQualityProfiles';
import { useUiSettings } from 'Settings/UI/useUiSettings';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchQualityProfiles,
} from 'Store/Actions/settingsActions';
import useSystemStatus from 'System/Status/useSystemStatus';
import useTags from 'Tags/useTags';
@ -25,6 +25,7 @@ const createErrorsSelector = ({
translationsError,
uiSettingsError,
seriesError,
qualityProfilesError,
}: {
customFiltersError: ApiError | null;
systemStatusError: ApiError | null;
@ -32,18 +33,13 @@ const createErrorsSelector = ({
translationsError: ApiError | null;
uiSettingsError: ApiError | null;
seriesError: ApiError | null;
qualityProfilesError: ApiError | null;
}) =>
createSelector(
(state: AppState) => state.settings.qualityProfiles.error,
(state: AppState) => state.settings.languages.error,
(state: AppState) => state.settings.importLists.error,
(state: AppState) => state.settings.indexerFlags.error,
(
qualityProfilesError,
languagesError,
importListsError,
indexerFlagsError
) => {
(languagesError, importListsError, indexerFlagsError) => {
const hasError = !!(
customFiltersError ||
seriesError ||
@ -97,9 +93,11 @@ const useAppPage = () => {
const { isFetched: isUiSettingsFetched, error: uiSettingsError } =
useUiSettings();
const { isFetched: isQualityProfilesFetched, error: qualityProfilesError } =
useQualityProfiles();
const isAppStatePopulated = useSelector(
(state: AppState) =>
state.settings.qualityProfiles.isPopulated &&
state.settings.languages.isPopulated &&
state.settings.importLists.isPopulated &&
state.settings.indexerFlags.isPopulated
@ -112,7 +110,8 @@ const useAppPage = () => {
isSystemStatusFetched &&
isTagsFetched &&
isTranslationsFetched &&
isUiSettingsFetched;
isUiSettingsFetched &&
isQualityProfilesFetched;
const { hasError, errors } = useSelector(
createErrorsSelector({
@ -122,6 +121,7 @@ const useAppPage = () => {
tagsError,
translationsError,
uiSettingsError,
qualityProfilesError,
})
);
@ -140,7 +140,6 @@ const useAppPage = () => {
useEffect(() => {
dispatch(fetchCustomFilters());
dispatch(fetchQualityProfiles());
dispatch(fetchLanguages());
dispatch(fetchImportLists());
dispatch(fetchIndexerFlags());

View file

@ -1,7 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import React, { useCallback, useMemo, useState } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@ -16,30 +13,11 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import Quality, { QualityModel } from 'Quality/Quality';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import { useQualityProfileSchema } from 'Settings/Profiles/Quality/useQualityProfiles';
import { InputChanged } from 'typings/inputs';
import getQualities from 'Utilities/Quality/getQualities';
import translate from 'Utilities/String/translate';
function createQualitySchemaSelector() {
return createSelector(
(state: AppState) => state.settings.qualityProfiles,
(qualityProfiles) => {
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
qualityProfiles;
const items = getQualities(schema.items);
return {
isFetching: isSchemaFetching,
isPopulated: isSchemaPopulated,
error: schemaError,
items,
};
}
);
}
interface SelectQualityModalContentProps {
qualityId: number;
proper: boolean;
@ -56,18 +34,12 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
const [proper, setProper] = useState(props.proper);
const [real, setReal] = useState(props.real);
const { isFetching, isPopulated, error, items } = useSelector(
createQualitySchemaSelector()
);
const dispatch = useDispatch();
const { schema, isSchemaFetching, isSchemaFetched, schemaError } =
useQualityProfileSchema(true);
useEffect(
() => {
dispatch(fetchQualityProfileSchema());
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const items = useMemo(() => {
return getQualities(schema.items);
}, [schema]);
const qualityOptions = useMemo(() => {
return items.map(({ id, name }): EnhancedSelectInputValue<number> => {
@ -119,13 +91,13 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
<ModalHeader>{modalTitle} - Select Quality</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{isSchemaFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
{!isSchemaFetching && schemaError ? (
<Alert kind={kinds.DANGER}>{translate('QualitiesLoadError')}</Alert>
) : null}
{isPopulated && !error ? (
{isSchemaFetched && !schemaError ? (
<Form>
<FormGroup>
<FormLabel>{translate('Quality')}</FormLabel>

View file

@ -1,12 +1,12 @@
import React, { useMemo } from 'react';
import { IconName } from 'Components/Icon';
import { icons } from 'Helpers/Props';
import { QualityProfileModel } from 'Settings/Profiles/Quality/useQualityProfiles';
import {
UiSettingsModel,
useUiSettingsValues,
} from 'Settings/UI/useUiSettings';
import dimensions from 'Styles/Variables/dimensions';
import QualityProfile from 'typings/QualityProfile';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
@ -39,7 +39,7 @@ interface SeriesIndexOverviewInfoProps {
monitored: boolean;
nextAiring?: string;
network?: string;
qualityProfile?: QualityProfile;
qualityProfile?: QualityProfileModel;
previousAiring?: string;
added?: string;
seasonCount: number;

View file

@ -1,7 +1,7 @@
import React from 'react';
import SeriesTagList from 'Components/SeriesTagList';
import Language from 'Language/Language';
import QualityProfile from 'typings/QualityProfile';
import { QualityProfileModel } from 'Settings/Profiles/Quality/useQualityProfiles';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
@ -12,7 +12,7 @@ interface SeriesIndexPosterInfoProps {
originalLanguage?: Language;
network?: string;
showQualityProfile: boolean;
qualityProfile?: QualityProfile;
qualityProfile?: QualityProfileModel;
previousAiring?: string;
added?: string;
seasonCount: number;

View file

@ -1,16 +1,13 @@
import { maxBy } from 'lodash';
import { useSelector } from 'react-redux';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting } from 'Commands/useCommands';
import { Season } from 'Series/Series';
import { useSingleSeries } from 'Series/useSeries';
import createSeriesQualityProfileSelector from 'Store/Selectors/createSeriesQualityProfileSelector';
import useSeriesQualityProfile from 'Series/useSeriesQualityProfile';
export function useSeriesIndexItem(seriesId: number) {
const series = useSingleSeries(seriesId);
const qualityProfile = useSelector(
createSeriesQualityProfileSelector(series)
);
const qualityProfile = useSeriesQualityProfile(series);
const isRefreshingSeries = useCommandExecuting(CommandNames.RefreshSeries, {
seriesIds: [seriesId],

View file

@ -0,0 +1,8 @@
import { useQualityProfile } from 'Settings/Profiles/Quality/useQualityProfiles';
import Series from './Series';
const useSeriesQualityProfile = (series: Series | undefined) => {
return useQualityProfile(series?.qualityProfileId);
};
export default useSeriesQualityProfile;

View file

@ -23,7 +23,7 @@ const NEW_REMOTE_PATH_MAPPING: RemotePathMappingModel = {
export const useRemotePathMapping = () => {};
export const useRemotePathMappings = () => {
return useProviderSettings<RemotePathMappingModel>(PATH);
return useProviderSettings<RemotePathMappingModel>({ path: PATH });
};
export const useManageRemotePathMappings = (id: number) => {

View file

@ -1,12 +1,11 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import SeriesTagList from 'Components/SeriesTagList';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
import { useQualityProfile } from 'Settings/Profiles/Quality/useQualityProfiles';
import ImportList from 'typings/ImportList';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
@ -37,9 +36,7 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
const { toggleSelected, useIsSelected } = useSelect<ImportList>();
const isSelected = useIsSelected(id);
const qualityProfile = useSelector(
createQualityProfileSelectorForHook(qualityProfileId)
);
const qualityProfile = useQualityProfile(qualityProfileId);
const onSelectedChangeWrapper = useCallback(
({ id, value, shiftKey }: SelectStateInputProps) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,19 +2,19 @@ import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import ModelBase from 'App/ModelBase';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import useApiQuery, { QueryOptions } from 'Helpers/Hooks/useApiQuery';
import { usePendingChangesStore } from 'Helpers/Hooks/usePendingChangesStore';
import selectSettings from 'Store/Selectors/selectSettings';
export const useProvider = <T extends ModelBase>(
id: number,
id: number | undefined,
defaultProvider: T,
path: string
) => {
const { data } = useProviderSettings<T>(path);
const { data } = useProviderSettings<T>({ path });
return useMemo(() => {
if (id === 0) {
if (!id) {
return defaultProvider;
}
@ -28,10 +28,10 @@ export const useProvider = <T extends ModelBase>(
}, [data, defaultProvider, id]);
};
export const useProviderSettings = <T extends ModelBase>(path: string) => {
const result = useApiQuery<T[]>({
path,
});
export const useProviderSettings = <T extends ModelBase>(
options: QueryOptions<T[]>
) => {
const result = useApiQuery<T[]>(options);
return {
...result,
@ -47,18 +47,18 @@ export const useSaveProviderSettings = <T extends ModelBase>(
const queryClient = useQueryClient();
const { mutate, isPending, error } = useApiMutation<T, T>({
path: id === 0 ? path : `${path}/${id}`,
method: id === 0 ? 'POST' : 'PUT',
path: id ? `${path}/${id}` : path,
method: id ? 'PUT' : 'POST',
mutationOptions: {
onSuccess: (updatedSettings: T) => {
queryClient.setQueryData<T[]>([path], (oldData = []) => {
if (id === 0) {
return [...oldData, updatedSettings];
if (id) {
return oldData.map((item) =>
item.id === updatedSettings.id ? updatedSettings : item
);
}
return oldData.map((item) =>
item.id === updatedSettings.id ? updatedSettings : item
);
return [...oldData, updatedSettings];
});
onSuccess?.();
},
@ -73,7 +73,7 @@ export const useSaveProviderSettings = <T extends ModelBase>(
};
export const useManageProviderSettings = <T extends ModelBase>(
id: number,
id: number | undefined,
defaultProvider: T,
path: string
) => {

View file

@ -21,7 +21,6 @@ import naming from './Settings/naming';
import namingExamples from './Settings/namingExamples';
import notifications from './Settings/notifications';
import qualityDefinitions from './Settings/qualityDefinitions';
import qualityProfiles from './Settings/qualityProfiles';
export * from './Settings/autoTaggingSpecifications';
export * from './Settings/autoTaggings';
@ -44,7 +43,6 @@ export * from './Settings/naming';
export * from './Settings/namingExamples';
export * from './Settings/notifications';
export * from './Settings/qualityDefinitions';
export * from './Settings/qualityProfiles';
//
// Variables
@ -76,8 +74,7 @@ export const defaultState = {
naming: naming.defaultState,
namingExamples: namingExamples.defaultState,
notifications: notifications.defaultState,
qualityDefinitions: qualityDefinitions.defaultState,
qualityProfiles: qualityProfiles.defaultState
qualityDefinitions: qualityDefinitions.defaultState
};
export const persistState = [
@ -108,8 +105,7 @@ export const actionHandlers = handleThunks({
...naming.actionHandlers,
...namingExamples.actionHandlers,
...notifications.actionHandlers,
...qualityDefinitions.actionHandlers,
...qualityProfiles.actionHandlers
...qualityDefinitions.actionHandlers
});
//
@ -136,7 +132,6 @@ export const reducers = createHandleActions({
...naming.reducers,
...namingExamples.reducers,
...notifications.reducers,
...qualityDefinitions.reducers,
...qualityProfiles.reducers
...qualityDefinitions.reducers
}, defaultState, section);

View file

@ -1,24 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createQualityProfileSelectorForHook(qualityProfileId: number) {
return createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfiles) => {
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
}
);
}
function createQualityProfileSelector() {
return createSelector(
(_: AppState, { qualityProfileId }: { qualityProfileId: number }) =>
qualityProfileId,
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfileId, qualityProfiles) => {
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
}
);
}
export default createQualityProfileSelector;

View file

@ -1,21 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Series from 'Series/Series';
import QualityProfile from 'typings/QualityProfile';
function createSeriesQualityProfileSelector(series?: Series) {
return createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfiles: QualityProfile[]) => {
if (!series) {
return undefined;
}
return qualityProfiles.find(
(profile) => profile.id === series.qualityProfileId
);
}
);
}
export default createSeriesQualityProfileSelector;

View file

@ -1,5 +1,5 @@
import Quality from 'Quality/Quality';
import { QualityProfileItems } from 'typings/QualityProfile';
import { QualityProfileItems } from 'Settings/Profiles/Quality/useQualityProfiles';
export default function getQualities(qualities?: QualityProfileItems) {
if (!qualities) {

View file

@ -1,36 +0,0 @@
import Quality from 'Quality/Quality';
import { QualityProfileFormatItem } from './CustomFormat';
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
)[];
interface QualityProfile {
name: string;
upgradeAllowed: boolean;
cutoff: number;
items: QualityProfileItems;
minFormatScore: number;
cutoffFormatScore: number;
minUpgradeFormatScore: number;
formatItems: QualityProfileFormatItem[];
id: number;
}
export default QualityProfile;