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

Add Series with ReactQuery Mutation

This commit is contained in:
Mark McDowall
2025-02-16 14:48:31 -08:00
parent 094df71301
commit 591b569bdd
28 changed files with 812 additions and 388 deletions
@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AddSeries } from 'App/State/AddSeriesAppState';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput';
@@ -10,7 +9,6 @@ import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import useDebounce from 'Helpers/Hooks/useDebounce';
import useQueryParams from 'Helpers/Hooks/useQueryParams';
import { icons, kinds } from 'Helpers/Props';
@@ -18,6 +16,7 @@ import { InputChanged } from 'typings/inputs';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import AddNewSeriesSearchResult from './AddNewSeriesSearchResult';
import { useLookupSeries } from './useAddSeries';
import styles from './AddNewSeries.css';
function AddNewSeries() {
@@ -48,12 +47,7 @@ function AddNewSeries() {
isFetching: isFetchingApi,
error,
data = [],
} = useApiQuery<AddSeries[]>({
path: `/series/lookup?term=${query}`,
queryOptions: {
enabled: !!query,
},
});
} = useLookupSeries(query);
useEffect(() => {
setIsFetching(isFetchingApi);
@@ -103,7 +97,9 @@ function AddNewSeries() {
{!isFetching && !error && !!data.length ? (
<div className={styles.searchResults}>
{data.map((item) => {
return <AddNewSeriesSearchResult key={item.tvdbId} {...item} />;
return (
<AddNewSeriesSearchResult key={item.tvdbId} series={item} />
);
})}
</div>
) : null}
@@ -1,9 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import AddSeries from 'AddSeries/AddSeries';
import {
AddSeriesOptions,
setAddSeriesOption,
useAddSeriesOptions,
} from 'AddSeries/addSeriesOptionsStore';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
import { AddSeries } from 'App/State/AddSeriesAppState';
import AppState from 'App/State/AppState';
import CheckInput from 'Components/Form/CheckInput';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -17,46 +21,43 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import { SeriesType } from 'Series/Series';
import SeriesPoster from 'Series/SeriesPoster';
import { addSeries, setAddSeriesDefault } from 'Store/Actions/addSeriesActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import useIsWindows from 'System/useIsWindows';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import { useAddSeries } from './useAddSeries';
import styles from './AddNewSeriesModalContent.css';
export interface AddNewSeriesModalContentProps
extends Pick<
AddSeries,
'tvdbId' | 'title' | 'year' | 'overview' | 'images' | 'folder'
> {
initialSeriesType: string;
export interface AddNewSeriesModalContentProps {
series: AddSeries;
initialSeriesType: SeriesType;
onModalClose: () => void;
}
function AddNewSeriesModalContent({
tvdbId,
title,
year,
overview,
images,
folder,
series,
initialSeriesType,
onModalClose,
}: AddNewSeriesModalContentProps) {
const dispatch = useDispatch();
const { isAdding, addError, defaults } = useSelector(
(state: AppState) => state.addSeries
);
const { title, year, overview, images, folder } = series;
const options = useAddSeriesOptions();
const { isSmallScreen } = useSelector(createDimensionsSelector());
const isWindows = useIsWindows();
const { settings, validationErrors, validationWarnings } = useMemo(() => {
return selectSettings(defaults, {}, addError);
}, [defaults, addError]);
const {
isPending: isAdding,
error: addError,
mutate: addSeries,
} = useAddSeries();
const [seriesType, setSeriesType] = useState(
const { settings, validationErrors, validationWarnings } = useMemo(() => {
return selectSettings(options, {}, addError);
}, [options, addError]);
const [seriesType, setSeriesType] = useState<SeriesType>(
initialSeriesType === 'standard'
? settings.seriesType.value
: initialSeriesType
@@ -74,35 +75,33 @@ function AddNewSeriesModalContent({
} = settings;
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
dispatch(setAddSeriesDefault({ [name]: value }));
({ name, value }: InputChanged<string | number | boolean | number[]>) => {
setAddSeriesOption(name as keyof AddSeriesOptions, value);
},
[dispatch]
[]
);
const handleQualityProfileIdChange = useCallback(
({ value }: InputChanged<string | number>) => {
dispatch(setAddSeriesDefault({ qualityProfileId: value }));
setAddSeriesOption('qualityProfileId', value as number);
},
[dispatch]
[]
);
const handleAddSeriesPress = useCallback(() => {
dispatch(
addSeries({
tvdbId,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
seriesType,
seasonFolder: seasonFolder.value,
searchForMissingEpisodes: searchForMissingEpisodes.value,
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
tags: tags.value,
})
);
addSeries({
...series,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
seriesType,
seasonFolder: seasonFolder.value,
searchForMissingEpisodes: searchForMissingEpisodes.value,
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
tags: tags.value,
});
}, [
tvdbId,
series,
seriesType,
rootFolderPath,
monitor,
@@ -111,7 +110,7 @@ function AddNewSeriesModalContent({
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
tags,
dispatch,
addSeries,
]);
useEffect(() => {
@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { AddSeries } from 'App/State/AddSeriesAppState';
import AddSeries from 'AddSeries/AddSeries';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
@@ -16,24 +16,27 @@ import translate from 'Utilities/String/translate';
import AddNewSeriesModal from './AddNewSeriesModal';
import styles from './AddNewSeriesSearchResult.css';
type AddNewSeriesSearchResultProps = AddSeries;
interface AddNewSeriesSearchResultProps {
series: AddSeries;
}
function AddNewSeriesSearchResult({ series }: AddNewSeriesSearchResultProps) {
const {
tvdbId,
titleSlug,
title,
year,
network,
originalLanguage,
genres = [],
status,
statistics = {} as Statistics,
ratings,
overview,
seriesType,
images,
} = series;
function AddNewSeriesSearchResult({
tvdbId,
titleSlug,
title,
year,
network,
originalLanguage,
genres = [],
status,
statistics = {} as Statistics,
ratings,
folder,
overview,
seriesType,
images,
}: AddNewSeriesSearchResultProps) {
const isExistingSeries = useSelector(createExistingSeriesSelector(tvdbId));
const { isSmallScreen } = useSelector(createDimensionsSelector());
const [isNewAddSeriesModalOpen, setIsNewAddSeriesModalOpen] = useState(false);
@@ -168,13 +171,8 @@ function AddNewSeriesSearchResult({
<AddNewSeriesModal
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
tvdbId={tvdbId}
title={title}
year={year}
overview={overview}
folder={folder}
series={series}
initialSeriesType={seriesType}
images={images}
onModalClose={handleAddSeriesModalClose}
/>
</div>
@@ -0,0 +1,40 @@
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import AddSeries from 'AddSeries/AddSeries';
import { AddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import Series from 'Series/Series';
import { updateItem } from 'Store/Actions/baseActions';
type AddSeriesPayload = AddSeries & AddSeriesOptions;
export const useLookupSeries = (query: string) => {
return useApiQuery<AddSeries[]>({
path: `/series/lookup?term=${query}`,
queryOptions: {
enabled: !!query,
// Disable refetch on window focus to prevent refetching when the user switch tabs
refetchOnWindowFocus: false,
},
});
};
export const useAddSeries = () => {
const dispatch = useDispatch();
const onAddSuccess = useCallback(
(data: Series) => {
dispatch(updateItem({ section: 'series', ...data }));
},
[dispatch]
);
return useApiMutation<Series, AddSeriesPayload>({
path: '/series',
method: 'POST',
mutationOptions: {
onSuccess: onAddSuccess,
},
});
};