1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-28 23:16:32 -04:00

Use react-query for commands

This commit is contained in:
Mark McDowall
2025-12-02 20:31:24 -08:00
parent dd12b9e076
commit dec6f4b5f2
51 changed files with 872 additions and 942 deletions
@@ -1,8 +1,8 @@
import classNames from 'classnames';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import TextTruncate from 'react-text-truncate';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -15,7 +15,6 @@ import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect
import { Statistics } from 'Series/Series';
import { useSeriesOverviewOptions } from 'Series/seriesOptionsStore';
import SeriesPoster from 'Series/SeriesPoster';
import { executeCommand } from 'Store/Actions/commandActions';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import translate from 'Utilities/String/translate';
@@ -60,6 +59,53 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
const overviewOptions = useSeriesOverviewOptions();
const executeCommand = useExecuteCommand();
const [isEditSeriesModalOpen, setIsEditSeriesModalOpen] = useState(false);
const [isDeleteSeriesModalOpen, setIsDeleteSeriesModalOpen] = useState(false);
const onRefreshPress = useCallback(() => {
executeCommand({
name: CommandNames.RefreshSeries,
seriesIds: [seriesId],
});
}, [seriesId, executeCommand]);
const onSearchPress = useCallback(() => {
executeCommand({
name: CommandNames.SeriesSearch,
seriesId,
});
}, [seriesId, executeCommand]);
const onEditSeriesPress = useCallback(() => {
setIsEditSeriesModalOpen(true);
}, [setIsEditSeriesModalOpen]);
const onEditSeriesModalClose = useCallback(() => {
setIsEditSeriesModalOpen(false);
}, [setIsEditSeriesModalOpen]);
const onDeleteSeriesPress = useCallback(() => {
setIsEditSeriesModalOpen(false);
setIsDeleteSeriesModalOpen(true);
}, [setIsDeleteSeriesModalOpen]);
const onDeleteSeriesModalClose = useCallback(() => {
setIsDeleteSeriesModalOpen(false);
}, [setIsDeleteSeriesModalOpen]);
const contentHeight = useMemo(() => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}, [rowHeight, isSmallScreen]);
const overviewHeight = contentHeight - TITLE_HEIGHT;
if (!series) {
return null;
}
const {
title,
monitored,
@@ -84,45 +130,6 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
sizeOnDisk = 0,
} = statistics;
const dispatch = useDispatch();
const [isEditSeriesModalOpen, setIsEditSeriesModalOpen] = useState(false);
const [isDeleteSeriesModalOpen, setIsDeleteSeriesModalOpen] = useState(false);
const onRefreshPress = useCallback(() => {
dispatch(
executeCommand({
name: REFRESH_SERIES,
seriesIds: [seriesId],
})
);
}, [seriesId, dispatch]);
const onSearchPress = useCallback(() => {
dispatch(
executeCommand({
name: SERIES_SEARCH,
seriesId,
})
);
}, [seriesId, dispatch]);
const onEditSeriesPress = useCallback(() => {
setIsEditSeriesModalOpen(true);
}, [setIsEditSeriesModalOpen]);
const onEditSeriesModalClose = useCallback(() => {
setIsEditSeriesModalOpen(false);
}, [setIsEditSeriesModalOpen]);
const onDeleteSeriesPress = useCallback(() => {
setIsEditSeriesModalOpen(false);
setIsDeleteSeriesModalOpen(true);
}, [setIsDeleteSeriesModalOpen]);
const onDeleteSeriesModalClose = useCallback(() => {
setIsDeleteSeriesModalOpen(false);
}, [setIsDeleteSeriesModalOpen]);
const link = `/series/${titleSlug}`;
const elementStyle = {
@@ -130,14 +137,6 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
height: `${posterHeight}px`,
};
const contentHeight = useMemo(() => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}, [rowHeight, isSmallScreen]);
const overviewHeight = contentHeight - TITLE_HEIGHT;
return (
<div>
<div className={styles.content}>
@@ -1,7 +1,8 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import { useSelector } from 'react-redux';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
@@ -15,7 +16,6 @@ import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect
import { Statistics } from 'Series/Series';
import { useSeriesPosterOptions } from 'Series/seriesOptionsStore';
import SeriesPoster from 'Series/SeriesPoster';
import { executeCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
@@ -50,52 +50,24 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
useSelector(createUISettingsSelector());
const {
title,
monitored,
status,
path,
titleSlug,
originalLanguage,
network,
nextAiring,
previousAiring,
added,
statistics = {} as Statistics,
images,
tags,
} = series;
const {
seasonCount = 0,
episodeCount = 0,
episodeFileCount = 0,
totalEpisodeCount = 0,
sizeOnDisk = 0,
} = statistics;
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const [hasPosterError, setHasPosterError] = useState(false);
const [isEditSeriesModalOpen, setIsEditSeriesModalOpen] = useState(false);
const [isDeleteSeriesModalOpen, setIsDeleteSeriesModalOpen] = useState(false);
const onRefreshPress = useCallback(() => {
dispatch(
executeCommand({
name: REFRESH_SERIES,
seriesIds: [seriesId],
})
);
}, [seriesId, dispatch]);
executeCommand({
name: CommandNames.RefreshSeries,
seriesIds: [seriesId],
});
}, [seriesId, executeCommand]);
const onSearchPress = useCallback(() => {
dispatch(
executeCommand({
name: SERIES_SEARCH,
seriesId,
})
);
}, [seriesId, dispatch]);
executeCommand({
name: CommandNames.SeriesSearch,
seriesId,
});
}, [seriesId, executeCommand]);
const onPosterLoadError = useCallback(() => {
setHasPosterError(true);
@@ -122,6 +94,34 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
setIsDeleteSeriesModalOpen(false);
}, [setIsDeleteSeriesModalOpen]);
if (!series) {
return null;
}
const {
title,
monitored,
status,
path,
titleSlug,
originalLanguage,
network,
nextAiring,
previousAiring,
added,
statistics = {} as Statistics,
images,
tags,
} = series;
const {
seasonCount = 0,
episodeCount = 0,
episodeFileCount = 0,
totalEpisodeCount = 0,
sizeOnDisk = 0,
} = statistics;
const link = `/series/${titleSlug}`;
const elementStyle = {
@@ -1,8 +1,8 @@
import { orderBy } from 'lodash';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import { RENAME_SERIES } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@@ -13,7 +13,6 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { icons, kinds } from 'Helpers/Props';
import Series from 'Series/Series';
import useSeries from 'Series/useSeries';
import { executeCommand } from 'Store/Actions/commandActions';
import translate from 'Utilities/String/translate';
import styles from './OrganizeSeriesModalContent.css';
@@ -25,7 +24,7 @@ function OrganizeSeriesModalContent({
onModalClose,
}: OrganizeSeriesModalContentProps) {
const { data: allSeries } = useSeries();
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { useSelectedIds } = useSelect<Series>();
const seriesIds = useSelectedIds();
@@ -46,15 +45,13 @@ function OrganizeSeriesModalContent({
}, [allSeries, seriesIds]);
const onOrganizePress = useCallback(() => {
dispatch(
executeCommand({
name: RENAME_SERIES,
seriesIds,
})
);
executeCommand({
name: CommandNames.RenameSeries,
seriesIds,
});
onModalClose();
}, [seriesIds, onModalClose, dispatch]);
}, [seriesIds, onModalClose, executeCommand]);
return (
<ModalContent onModalClose={onModalClose}>
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import { RENAME_SERIES } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting } from 'Commands/useCommands';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import usePrevious from 'Helpers/Hooks/usePrevious';
@@ -12,7 +12,6 @@ import {
useSaveSeriesEditor,
useUpdateSeriesMonitor,
} from 'Series/useSeries';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import DeleteSeriesModal from './Delete/DeleteSeriesModal';
import EditSeriesModal from './Edit/EditSeriesModal';
@@ -36,9 +35,7 @@ function SeriesIndexSelectFooter() {
useUpdateSeriesMonitor();
const { isBulkDeleting, bulkDeleteError } = useBulkDeleteSeries();
const isOrganizingSeries = useSelector(
createCommandExecutingSelector(RENAME_SERIES)
);
const isOrganizingSeries = useCommandExecuting(CommandNames.RenameSeries);
const isSaving = isSavingSeriesEditor || isUpdatingSeriesMonitor;
const isDeleting = isBulkDeleting;
+8 -14
View File
@@ -1,9 +1,9 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import { useAppDimension } from 'App/appStore';
import { SelectProvider } from 'App/Select/SelectContext';
import { RSS_SYNC } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@@ -27,9 +27,7 @@ import {
useSeriesOptions,
} from 'Series/seriesOptionsStore';
import { FILTERS, useSeriesIndex } from 'Series/useSeries';
import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import SeriesIndexFilterMenu from './Menus/SeriesIndexFilterMenu';
import SeriesIndexSortMenu from './Menus/SeriesIndexSortMenu';
@@ -80,11 +78,9 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
const customFilters = useCustomFiltersList('series');
const isRssSyncExecuting = useSelector(
createCommandExecutingSelector(RSS_SYNC)
);
const executeCommand = useExecuteCommand();
const isRssSyncExecuting = useCommandExecuting(CommandNames.RssSync);
const isSmallScreen = useAppDimension('isSmallScreen');
const dispatch = useDispatch();
const scrollerRef = useRef<HTMLDivElement>(null);
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
@@ -93,12 +89,10 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
const [isSelectMode, setIsSelectMode] = useState(false);
const onRssSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: RSS_SYNC,
})
);
}, [dispatch]);
executeCommand({
name: CommandNames.RssSync,
});
}, [executeCommand]);
const onSelectModePress = useCallback(() => {
setIsSelectMode(!isSelectMode);
@@ -1,12 +1,11 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import { REFRESH_SERIES } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import Series from 'Series/Series';
import { useSeriesIndex } from 'Series/useSeries';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
interface SeriesIndexRefreshSeriesButtonProps {
@@ -17,14 +16,12 @@ interface SeriesIndexRefreshSeriesButtonProps {
function SeriesIndexRefreshSeriesButton(
props: SeriesIndexRefreshSeriesButtonProps
) {
const isRefreshing = useSelector(
createCommandExecutingSelector(REFRESH_SERIES)
);
const isRefreshing = useCommandExecuting(CommandNames.RefreshSeries);
const { data, totalItems } = useSeriesIndex();
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { isSelectMode, selectedFilterKey } = props;
const { anySelected, getSelectedIds } = useSelect();
const { anySelected, getSelectedIds } = useSelect<Series>();
let refreshLabel = translate('UpdateAll');
@@ -38,13 +35,11 @@ function SeriesIndexRefreshSeriesButton(
const seriesToRefresh =
isSelectMode && anySelected ? getSelectedIds() : data.map((m) => m.id);
dispatch(
executeCommand({
name: REFRESH_SERIES,
seriesIds: seriesToRefresh,
})
);
}, [dispatch, anySelected, isSelectMode, data, getSelectedIds]);
executeCommand({
name: CommandNames.RefreshSeries,
seriesIds: seriesToRefresh,
});
}, [executeCommand, anySelected, isSelectMode, data, getSelectedIds]);
return (
<PageToolbarButton
@@ -1,8 +1,8 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import CheckInput from 'Components/Form/CheckInput';
import HeartRating from 'Components/HeartRating';
import IconButton from 'Components/Link/IconButton';
@@ -20,7 +20,6 @@ import { Statistics } from 'Series/Series';
import SeriesBanner from 'Series/SeriesBanner';
import { useSeriesTableOptions } from 'Series/seriesOptionsStore';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import { executeCommand } from 'Store/Actions/commandActions';
import { SelectStateInputProps } from 'typings/props';
import formatBytes from 'Utilities/Number/formatBytes';
import titleCase from 'Utilities/String/titleCase';
@@ -52,63 +51,25 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
const { showBanners, showSearchAction } = useSeriesTableOptions();
const {
title,
monitored,
monitorNewItems,
status,
path,
titleSlug,
nextAiring,
previousAiring,
added,
statistics = {} as Statistics,
seasonFolder,
images,
seriesType,
network,
originalLanguage,
certification,
year,
useSceneNumbering,
genres = [],
ratings,
seasons = [],
tags = [],
} = series;
const {
seasonCount = 0,
episodeCount = 0,
episodeFileCount = 0,
totalEpisodeCount = 0,
sizeOnDisk = 0,
releaseGroups = [],
} = statistics;
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const [hasBannerError, setHasBannerError] = useState(false);
const [isEditSeriesModalOpen, setIsEditSeriesModalOpen] = useState(false);
const [isDeleteSeriesModalOpen, setIsDeleteSeriesModalOpen] = useState(false);
const { getIsSelected, toggleSelected } = useSelect();
const onRefreshPress = useCallback(() => {
dispatch(
executeCommand({
name: REFRESH_SERIES,
seriesIds: [seriesId],
})
);
}, [seriesId, dispatch]);
executeCommand({
name: CommandNames.RefreshSeries,
seriesIds: [seriesId],
});
}, [seriesId, executeCommand]);
const onSearchPress = useCallback(() => {
dispatch(
executeCommand({
name: SERIES_SEARCH,
seriesId,
})
);
}, [seriesId, dispatch]);
executeCommand({
name: CommandNames.SeriesSearch,
seriesId,
});
}, [seriesId, executeCommand]);
const onBannerLoadError = useCallback(() => {
setHasBannerError(true);
@@ -150,6 +111,44 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
[toggleSelected]
);
if (!series) {
return null;
}
const {
title,
monitored,
monitorNewItems,
status,
path,
titleSlug,
nextAiring,
previousAiring,
added,
statistics = {} as Statistics,
seasonFolder,
images,
seriesType,
network,
originalLanguage,
certification,
year,
useSceneNumbering,
genres = [],
ratings,
seasons = [],
tags = [],
} = series;
const {
seasonCount = 0,
episodeCount = 0,
episodeFileCount = 0,
totalEpisodeCount = 0,
sizeOnDisk = 0,
releaseGroups = [],
} = statistics;
return (
<>
{isSelectMode ? (
+23 -35
View File
@@ -1,49 +1,37 @@
import { maxBy } from 'lodash';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import Command from 'Commands/Command';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting } from 'Commands/useCommands';
import { Season } from 'Series/Series';
import { useSingleSeries } from 'Series/useSeries';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
import createSeriesQualityProfileSelector from 'Store/Selectors/createSeriesQualityProfileSelector';
function useSeriesIndexItem(seriesId: number) {
export function useSeriesIndexItem(seriesId: number) {
const series = useSingleSeries(seriesId);
const qualityProfile = useSelector(
createSeriesQualityProfileSelector(series)
);
const executingCommands: Command[] = useSelector(
createExecutingCommandsSelector()
const isRefreshingSeries = useCommandExecuting(CommandNames.RefreshSeries, {
seriesIds: [seriesId],
});
const isSearchingSeries = useCommandExecuting(CommandNames.SeriesSearch, {
seriesId,
});
const latestSeason: Season | undefined = maxBy(
series?.seasons || [],
(season) => season.seasonNumber
);
return useMemo(() => {
if (!series) {
throw new Error('Series not found');
}
const isRefreshingSeries = executingCommands.some((command) => {
return (
command.name === REFRESH_SERIES &&
command.body.seriesIds?.includes(series.id)
);
});
const isSearchingSeries = executingCommands.some((command) => {
return (
command.name === SERIES_SEARCH && command.body.seriesId === seriesId
);
});
const latestSeason = maxBy(series.seasons, (season) => season.seasonNumber);
return {
series,
qualityProfile,
latestSeason,
isRefreshingSeries,
isSearchingSeries,
};
}, [series, qualityProfile, executingCommands, seriesId]);
return {
series,
qualityProfile,
latestSeason,
isRefreshingSeries,
isSearchingSeries,
};
}
export default useSeriesIndexItem;