1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05: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

View File

@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setQueueOptions } from 'Activity/Queue/queueOptionsStore';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import * as commandNames 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 FilterMenu from 'Components/Menu/FilterMenu';
@@ -19,8 +19,6 @@ import TablePager from 'Components/Table/TablePager';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import BlockListModel from 'typings/Blocklist';
import { CheckInputChanged } from 'typings/inputs';
import { TableOptionsChangePayload } from 'typings/Table';
@@ -62,10 +60,10 @@ function BlocklistContent() {
const { isRemoving, removeBlocklistItems } = useRemoveBlocklistItems();
const customFilters = useCustomFiltersList('blocklist');
const isClearingBlocklistExecuting = useSelector(
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
const executeCommand = useExecuteCommand();
const isClearingBlocklistExecuting = useCommandExecuting(
CommandNames.ClearBlocklist
);
const dispatch = useDispatch();
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
useState(false);
@@ -109,16 +107,11 @@ function BlocklistContent() {
}, [setIsConfirmClearModalOpen]);
const handleClearBlocklistConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.CLEAR_BLOCKLIST,
commandFinished: () => {
goToPage(1);
},
})
);
executeCommand({ name: CommandNames.ClearBlocklist }, () => {
goToPage(1);
});
setIsConfirmClearModalOpen(false);
}, [setIsConfirmClearModalOpen, goToPage, dispatch]);
}, [setIsConfirmClearModalOpen, goToPage, executeCommand]);
const handleConfirmClearModalClose = useCallback(() => {
setIsConfirmClearModalOpen(false);

View File

@@ -6,9 +6,9 @@ import React, {
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import * as commandNames 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 FilterMenu from 'Components/Menu/FilterMenu';
@@ -26,8 +26,6 @@ import useEpisodes from 'Episode/useEpisodes';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import QueueModel from 'typings/Queue';
import { TableOptionsChangePayload } from 'typings/Table';
@@ -55,7 +53,7 @@ import useQueue, {
} from './useQueue';
function QueueContent() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const {
records,
@@ -91,8 +89,8 @@ function QueueContent() {
const customFilters = useCustomFiltersList('queue');
const isRefreshMonitoredDownloadsExecuting = useSelector(
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
const isRefreshMonitoredDownloadsExecuting = useCommandExecuting(
CommandNames.RefreshMonitoredDownloads
);
const shouldBlockRefresh = useRef(false);
@@ -136,12 +134,10 @@ function QueueContent() {
);
const handleRefreshPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
})
);
}, [dispatch]);
executeCommand({
name: CommandNames.RefreshMonitoredDownloads,
});
}, [executeCommand]);
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
shouldBlockRefresh.current = isOpen;

View File

@@ -1,6 +1,5 @@
import BlocklistAppState from './BlocklistAppState';
import CaptchaAppState from './CaptchaAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
import ImportSeriesAppState from './ImportSeriesAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
@@ -12,7 +11,6 @@ import SettingsAppState from './SettingsAppState';
interface AppState {
blocklist: BlocklistAppState;
captcha: CaptchaAppState;
commands: CommandAppState;
episodeHistory: HistoryAppState;
importSeries: ImportSeriesAppState;
interactiveImport: InteractiveImportAppState;

View File

@@ -1,6 +0,0 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View File

@@ -1,12 +1,11 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting } from 'Commands/useCommands';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { kinds } from 'Helpers/Props';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import {
registerPagePopulator,
unregisterPagePopulator,
@@ -23,16 +22,13 @@ import styles from './Calendar.css';
const UPDATE_DELAY = 3600000; // 1 hour
function Calendar() {
const dispatch = useDispatch();
const requestCurrentPage = useCurrentPage();
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
const { isFetching, isLoading, error, refetch } = useCalendar();
const view = useCalendarOption('view');
const isRefreshingSeries = useSelector(
createCommandExecutingSelector(commandNames.REFRESH_SERIES)
);
const isRefreshingSeries = useCommandExecuting(CommandNames.RefreshSeries);
const wasRefreshingSeries = usePrevious(isRefreshingSeries);
@@ -59,7 +55,7 @@ function Calendar() {
if (!requestCurrentPage) {
goToToday();
}
}, [requestCurrentPage, dispatch]);
}, [requestCurrentPage]);
useEffect(() => {
const repopulate = () => {

View File

@@ -1,11 +1,9 @@
import moment from 'moment';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useQueueDetails } from 'Activity/Queue/Details/QueueDetailsProvider';
import { useCommands } from 'Commands/useCommands';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons } from 'Helpers/Props';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { isCommandExecuting } from 'Utilities/Command';
import isBefore from 'Utilities/Date/isBefore';
import translate from 'Utilities/String/translate';
@@ -14,18 +12,18 @@ import useCalendar, {
useCalendarSearchMissingCommandId,
} from './useCalendar';
function createIsSearchingSelector(searchMissingCommandId: number | undefined) {
return createSelector(createCommandsSelector(), (commands) => {
if (searchMissingCommandId == null) {
return false;
}
function useIsSearching(searchMissingCommandId: number | undefined) {
const { data: commands } = useCommands();
return isCommandExecuting(
commands.find((command) => {
return command.id === searchMissingCommandId;
})
);
});
if (searchMissingCommandId == null) {
return false;
}
return isCommandExecuting(
commands.find((command) => {
return command.id === searchMissingCommandId;
})
);
}
const useMissingEpisodeIdsSelector = () => {
@@ -55,9 +53,7 @@ const useMissingEpisodeIdsSelector = () => {
export default function CalendarMissingEpisodeSearchButton() {
const searchMissingCommandId = useCalendarSearchMissingCommandId();
const missingEpisodeIds = useMissingEpisodeIdsSelector();
const isSearchingForMissing = useSelector(
createIsSearchingSelector(searchMissingCommandId)
);
const isSearchingForMissing = useIsSearching(searchMissingCommandId);
const handlePress = useCallback(() => {}, []);

View File

@@ -5,9 +5,9 @@ import React, {
useMemo,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -22,8 +22,6 @@ import useMeasure from 'Helpers/Hooks/useMeasure';
import { align, icons } from 'Helpers/Props';
import NoSeries from 'Series/NoSeries';
import { useHasSeries } from 'Series/useSeries';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import translate from 'Utilities/String/translate';
import Calendar from './Calendar';
@@ -43,16 +41,14 @@ import styles from './CalendarPage.css';
const MINIMUM_DAY_WIDTH = 120;
function CalendarPage() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const selectedFilterKey = useCalendarOption('selectedFilterKey');
const { data } = useCalendar();
useCalendarPage();
const isRssSyncExecuting = useSelector(
createCommandExecutingSelector(commandNames.RSS_SYNC)
);
const isRssSyncExecuting = useCommandExecuting(CommandNames.RssSync);
const customFilters = useCustomFiltersList('calendar');
const hasSeries = useHasSeries();
@@ -80,12 +76,10 @@ function CalendarPage() {
}, []);
const handleRssSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.RSS_SYNC,
})
);
}, [dispatch]);
executeCommand({
name: CommandNames.RssSync,
});
}, [executeCommand]);
const handleFilterSelect = useCallback((key: string | number) => {
setCalendarOption('selectedFilterKey', key);

View File

@@ -1,17 +1,14 @@
import { keepPreviousData } from '@tanstack/react-query';
import moment from 'moment';
import { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { create } from 'zustand';
import AppState from 'App/State/AppState';
import Command from 'Commands/Command';
import * as commandNames from 'Commands/commandNames';
import { setEpisodeQueryKey } from 'Episode/useEpisode';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { filterBuilderValueTypes } from 'Helpers/Props';
import { executeCommandHelper } from 'Store/Actions/commandActions';
import { CalendarItem } from 'typings/Calendar';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
@@ -182,19 +179,6 @@ export const useCalendarSearchMissingCommandId = () => {
return calendarStore((state) => state.searchMissingCommandId);
};
export const useSearchMissing = (episodeIds: number[]) => {
const dispatch = useDispatch();
const commandPayload = {
name: commandNames.EPISODE_SEARCH,
episodeIds,
};
executeCommandHelper(commandPayload, dispatch).then((data: Command) => {
calendarStore.setState({ searchMissingCommandId: data.id });
});
};
export const setCalendarDayCount = (dayCount: number) => {
calendarStore.setState({ dayCount });
};

View File

@@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
import { InteractiveImportCommandOptions } from 'InteractiveImport/InteractiveImport';
export type CommandStatus =
| 'queued'
@@ -11,7 +12,8 @@ export type CommandStatus =
export type CommandResult = 'unknown' | 'successful' | 'unsuccessful';
export interface CommandBody {
// Base command body with common properties
export interface BaseCommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
@@ -23,13 +25,102 @@ export interface CommandBody {
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
}
// Specific command body interfaces
export interface SeriesCommandBody extends BaseCommandBody {
seriesId: number;
}
export interface MultipleSeriesCommandBody extends BaseCommandBody {
seriesIds: number[];
}
export interface SeasonCommandBody extends BaseCommandBody {
seriesId: number;
seasonNumber: number;
}
export interface EpisodeCommandBody extends BaseCommandBody {
episodeIds: number[];
}
export interface SeriesEpisodeCommandBody extends BaseCommandBody {
seriesId: number;
episodeIds: number[];
}
export interface RenameFilesCommandBody extends BaseCommandBody {
seriesId: number;
files: number[];
}
export interface MoveSeriesCommandBody extends BaseCommandBody {
seriesId: number;
destinationPath: string;
}
export interface ManualImportCommandBody extends BaseCommandBody {
files: Array<{
path: string;
seriesId: number;
episodeIds: number[];
quality: Record<string, unknown>;
language: Record<string, unknown>;
releaseGroup?: string;
}>;
}
export type CommandBody =
| SeriesCommandBody
| MultipleSeriesCommandBody
| SeasonCommandBody
| EpisodeCommandBody
| SeriesEpisodeCommandBody
| RenameFilesCommandBody
| MoveSeriesCommandBody
| ManualImportCommandBody
| BaseCommandBody;
// Simplified interface for creating new commands
export interface NewCommandBody {
name: string;
seriesId?: number;
seriesIds?: number[];
seasonNumber?: number;
episodeIds?: number[];
[key: string]: string | number | boolean | number[] | undefined;
files?: number[] | InteractiveImportCommandOptions[];
destinationPath?: string;
[key: string]: string | number | boolean | number[] | object | undefined;
}
export interface CommandBodyMap {
RefreshSeries: SeriesCommandBody | MultipleSeriesCommandBody;
SeriesSearch: SeriesCommandBody;
SeasonSearch: SeasonCommandBody;
EpisodeSearch: EpisodeCommandBody | SeriesEpisodeCommandBody;
MissingEpisodeSearch: BaseCommandBody;
CutoffUnmetEpisodeSearch: BaseCommandBody;
RenameFiles: RenameFilesCommandBody;
RenameSeries: MultipleSeriesCommandBody;
MoveSeries: MoveSeriesCommandBody;
ManualImport: ManualImportCommandBody;
DownloadedEpisodesScan: SeriesCommandBody | BaseCommandBody;
RssSync: BaseCommandBody;
ApplicationUpdate: BaseCommandBody;
Backup: BaseCommandBody;
ClearBlocklist: BaseCommandBody;
ClearLog: BaseCommandBody;
DeleteLogFiles: BaseCommandBody;
DeleteUpdateLogFiles: BaseCommandBody;
RefreshMonitoredDownloads: BaseCommandBody;
ResetApiKey: BaseCommandBody;
ResetQualityDefinitions: BaseCommandBody;
}
export type CommandBodyForName<T extends keyof CommandBodyMap> =
CommandBodyMap[T];
interface Command extends ModelBase {
name: string;
commandName: string;

View File

@@ -0,0 +1,25 @@
enum CommandNames {
ApplicationUpdate = 'ApplicationUpdate',
Backup = 'Backup',
ClearBlocklist = 'ClearBlocklist',
ClearLog = 'ClearLog',
CutoffUnmetEpisodeSearch = 'CutoffUnmetEpisodeSearch',
DeleteLogFiles = 'DeleteLogFiles',
DeleteUpdateLogFiles = 'DeleteUpdateLogFiles',
DownloadedEpisodesScan = 'DownloadedEpisodesScan',
EpisodeSearch = 'EpisodeSearch',
ManualImport = 'ManualImport',
MissingEpisodeSearch = 'MissingEpisodeSearch',
MoveSeries = 'MoveSeries',
RefreshMonitoredDownloads = 'RefreshMonitoredDownloads',
RefreshSeries = 'RefreshSeries',
RenameFiles = 'RenameFiles',
RenameSeries = 'RenameSeries',
ResetApiKey = 'ResetApiKey',
ResetQualityDefinitions = 'ResetQualityDefinitions',
RssSync = 'RssSync',
SeasonSearch = 'SeasonSearch',
SeriesSearch = 'SeriesSearch',
}
export default CommandNames;

View File

@@ -1,21 +0,0 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLOCKLIST = 'ClearBlocklist';
export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles';
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
export const DOWNLOADED_EPISODES_SCAN = 'DownloadedEpisodesScan';
export const EPISODE_SEARCH = 'EpisodeSearch';
export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';
export const MOVE_SERIES = 'MoveSeries';
export const REFRESH_SERIES = 'RefreshSeries';
export const RENAME_FILES = 'RenameFiles';
export const RENAME_SERIES = 'RenameSeries';
export const RESET_API_KEY = 'ResetApiKey';
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
export const RSS_SYNC = 'RssSync';
export const SEASON_SEARCH = 'SeasonSearch';
export const SERIES_SEARCH = 'SeriesSearch';

View File

@@ -0,0 +1,240 @@
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo, useRef } from 'react';
import { showMessage } from 'App/messagesStore';
import Command, { CommandBody, NewCommandBody } from 'Commands/Command';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import {
ERROR,
INFO,
type MessageType,
SUCCESS,
} from 'Helpers/Props/messageTypes';
import { isSameCommand } from 'Utilities/Command';
const DEFAULT_COMMANDS: Command[] = [];
const COMMAND_REFETCH_INTERVAL = 5 * 60 * 1000;
const commandFinishedCallbacks: Record<number, (command: Command) => void> = {};
export const useCommands = () => {
const result = useApiQuery<Command[]>({
path: '/command',
queryOptions: {
refetchInterval: COMMAND_REFETCH_INTERVAL,
},
});
return {
...result,
data: result.data ?? DEFAULT_COMMANDS,
};
};
export default useCommands;
export const useExecuteCommand = () => {
const queryClient = useQueryClient();
const lastCommandRef = useRef<{
command: NewCommandBody;
timestamp: number;
} | null>(null);
const { mutate } = useApiMutation<Command, NewCommandBody>({
method: 'POST',
path: '/command',
mutationOptions: {
onSuccess: (newCommand: Command) => {
queryClient.setQueryData<Command[]>(
['/command'],
(oldCommands = []) => {
return [...oldCommands, newCommand];
}
);
},
},
});
const executeCommand = useCallback(
(body: NewCommandBody, commandFinished?: (command: Command) => void) => {
const now = Date.now();
const lastCommand = lastCommandRef.current;
// Check if the same command was run within the last 5 seconds
if (
lastCommand &&
now - lastCommand.timestamp < 5000 &&
isSameCommand(lastCommand.command, body)
) {
console.warn(
'Please wait at least 5 seconds before running this command again'
);
return;
}
// Update last command reference
lastCommandRef.current = {
command: body,
timestamp: now,
};
const executeWithCallback = (commandBody: NewCommandBody) => {
mutate(commandBody, {
onSuccess: (command) => {
if (commandFinished) {
commandFinishedCallbacks[command.id] = commandFinished;
}
},
});
};
executeWithCallback(body);
},
[mutate]
);
return executeCommand;
};
export const useCancelCommand = (id: number) => {
const queryClient = useQueryClient();
const { mutate, isPending, error } = useApiMutation<void, void>({
method: 'DELETE',
path: `/command/${id}`,
mutationOptions: {
onSuccess: () => {
queryClient.setQueryData<Command[]>(
['/command'],
(oldCommands = []) => {
return oldCommands.filter((command) => command.id !== id);
}
);
},
},
});
return {
cancelCommand: mutate,
isCancellingCommand: isPending,
commandCancelError: error,
};
};
export const useCommand = (
commandName: string,
constraints: Partial<CommandBody> = {}
) => {
const { data: commands } = useCommands();
return useMemo(() => {
return commands.find((command) => {
if (command.name !== commandName) {
return undefined;
}
return (Object.keys(constraints) as Array<keyof CommandBody>).every(
(key) => {
const constraintValue = constraints[key];
const commandValue = command.body?.[key];
if (constraintValue === undefined) {
return true;
}
if (Array.isArray(constraintValue) && Array.isArray(commandValue)) {
return constraintValue.every((value) =>
commandValue.includes(value)
);
}
return constraintValue === commandValue;
}
);
});
}, [commands, commandName, constraints]);
};
export const useCommandExecuting = (
commandName: string,
constraints: Partial<CommandBody> = {}
) => {
const command = useCommand(commandName, constraints);
return command
? command.status === 'queued' || command.status === 'started'
: false;
};
export const useExecutingCommands = () => {
const { data: commands } = useCommands();
return commands.filter(
(command) => command.status === 'queued' || command.status === 'started'
);
};
export const useUpdateCommand = () => {
const queryClient = useQueryClient();
return (command: Command) => {
queryClient.setQueryData<Command[]>(['/command'], (oldCommands = []) => {
return oldCommands.map((existingCommand) =>
existingCommand.id === command.id ? command : existingCommand
);
});
// Show command message for user feedback
showCommandMessage(command);
// Both successful and failed commands need to be
// completed, otherwise they spin until they time out.
const isFinished =
command.status === 'completed' || command.status === 'failed';
if (isFinished) {
const commandFinished = commandFinishedCallbacks[command.id];
if (commandFinished) {
commandFinished(command);
delete commandFinishedCallbacks[command.id];
}
}
};
};
function showCommandMessage(command: Command) {
const {
id,
name,
trigger,
message,
body = {} as CommandBody,
status,
} = command;
const { sendUpdatesToClient, suppressMessages } = body;
if (!message || !body || !sendUpdatesToClient || suppressMessages) {
return;
}
let type: MessageType = INFO;
let hideAfter = 0;
if (status === 'completed') {
type = SUCCESS;
hideAfter = 4;
} else if (status === 'failed') {
type = ERROR;
hideAfter = trigger === 'manual' ? 10 : 4;
}
showMessage({
id,
name,
message,
type,
hideAfter,
});
}

View File

@@ -9,16 +9,12 @@ import { useDispatch } from 'react-redux';
import { setAppValue, setVersion } from 'App/appStore';
import ModelBase from 'App/ModelBase';
import Command from 'Commands/Command';
import { useUpdateCommand } from 'Commands/useCommands';
import Episode from 'Episode/Episode';
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
import Series from 'Series/Series';
import { removeItem, updateItem } from 'Store/Actions/baseActions';
import {
fetchCommands,
finishCommand,
updateCommand,
} from 'Store/Actions/commandActions';
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
import { repopulatePage } from 'Utilities/pagePopulator';
import SignalRLogger from 'Utilities/SignalRLogger';
@@ -37,6 +33,7 @@ interface SignalRMessage {
function SignalRListener() {
const queryClient = useQueryClient();
const updateCommand = useUpdateCommand();
const dispatch = useDispatch();
const connection = useRef<HubConnection | null>(null);
@@ -79,7 +76,9 @@ function SignalRListener() {
// Repopulate the page (if a repopulator is set) to ensure things
// are in sync after reconnecting.
queryClient.invalidateQueries({ queryKey: ['/series'] });
dispatch(fetchCommands());
queryClient.invalidateQueries({ queryKey: ['/command'] });
repopulatePage();
});
@@ -112,21 +111,13 @@ function SignalRListener() {
if (name === 'command') {
if (body.action === 'sync') {
dispatch(fetchCommands());
queryClient.invalidateQueries({ queryKey: ['/command'] });
return;
}
const resource = body.resource as Command;
const status = resource.status;
// Both successful and failed commands need to be
// completed, otherwise they spin until they time out.
if (status === 'completed' || status === 'failed') {
dispatch(finishCommand(resource));
} else {
dispatch(updateCommand(resource));
}
updateCommand(resource);
return;
}

View File

@@ -1,14 +1,12 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EPISODE_SEARCH } from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { EpisodeEntity } from 'Episode/useEpisode';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
import translate from 'Utilities/String/translate';
import EpisodeDetailsModal from './EpisodeDetailsModal';
import styles from './EpisodeSearchCell.css';
@@ -28,25 +26,21 @@ function EpisodeSearchCell({
episodeTitle,
showOpenSeriesButton,
}: EpisodeSearchCellProps) {
const executingCommands = useSelector(createExecutingCommandsSelector());
const isSearching = executingCommands.some(({ name, body }) => {
const { episodeIds = [] } = body;
return name === EPISODE_SEARCH && episodeIds.indexOf(episodeId) > -1;
const isSearching = useCommandExecuting(CommandNames.EpisodeSearch, {
episodeIds: [episodeId],
});
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const [isDetailsModalOpen, setDetailsModalOpen, setDetailsModalClosed] =
useModalOpenState(false);
const handleSearchPress = useCallback(() => {
dispatch(
executeCommand({
name: EPISODE_SEARCH,
episodeIds: [episodeId],
})
);
}, [episodeId, dispatch]);
executeCommand({
name: CommandNames.EpisodeSearch,
episodeIds: [episodeId],
});
}, [episodeId, executeCommand]);
return (
<TableRowCell className={styles.episodeSearchCell}>

View File

@@ -1,12 +1,11 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props';
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
import useReleases from 'InteractiveSearch/useReleases';
import { executeCommand } from 'Store/Actions/commandActions';
import translate from 'Utilities/String/translate';
import styles from './EpisodeSearch.css';
@@ -21,7 +20,7 @@ function EpisodeSearch({
startInteractiveSearch,
onModalClose,
}: EpisodeSearchProps) {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { isFetched } = useReleases({ episodeId });
const [isInteractiveSearchOpen, setIsInteractiveSearchOpen] = useState(
@@ -29,15 +28,13 @@ function EpisodeSearch({
);
const handleQuickSearchPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: [episodeId],
})
);
executeCommand({
name: CommandNames.EpisodeSearch,
episodeIds: [episodeId],
});
onModalClose();
}, [episodeId, dispatch, onModalClose]);
}, [episodeId, executeCommand, onModalClose]);
const handleInteractiveSearchPress = useCallback(() => {
setIsInteractiveSearchOpen(true);

View File

@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { useTranslations } from 'App/useTranslations';
import useCommands from 'Commands/useCommands';
import useCustomFilters from 'Filters/useCustomFilters';
import useSeries from 'Series/useSeries';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
@@ -77,6 +78,8 @@ const createErrorsSelector = ({
const useAppPage = () => {
const dispatch = useDispatch();
useCommands();
const { isFetched: isCustomFiltersFetched, error: customFiltersError } =
useCustomFilters();

View File

@@ -2,7 +2,8 @@ import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import PathInput from 'Components/Form/PathInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@@ -14,7 +15,6 @@ import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import FavoriteFolderRow from './FavoriteFolderRow';
@@ -64,6 +64,8 @@ function InteractiveImportSelectFolderModalContent(
const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState('');
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { favoriteFolders, recentFolders } = useSelector(
createSelector(
(state: AppState) => state.interactiveImport,
@@ -97,15 +99,13 @@ function InteractiveImportSelectFolderModalContent(
const onQuickImportPress = useCallback(() => {
dispatch(addRecentFolder({ folder }));
dispatch(
executeCommand({
name: commandNames.DOWNLOADED_EPISODES_SCAN,
path: folder,
})
);
executeCommand({
name: CommandNames.DownloadedEpisodesScan,
path: folder,
});
onModalClose();
}, [folder, onModalClose, dispatch]);
}, [folder, onModalClose, dispatch, executeCommand]);
const onInteractiveImportPress = useCallback(() => {
dispatch(addRecentFolder({ folder }));

View File

@@ -5,7 +5,8 @@ import { createSelector } from 'reselect';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@@ -46,7 +47,6 @@ import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import Series from 'Series/Series';
import { executeCommand } from 'Store/Actions/commandActions';
import {
clearInteractiveImport,
fetchInteractiveImportItems,
@@ -280,6 +280,7 @@ function InteractiveImportModalContentInner(
useState<string | null>(null);
const previousIsDeleting = usePrevious(isDeleting);
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const {
allSelected,
@@ -601,13 +602,11 @@ function InteractiveImportModalContentInner(
}
if (files.length) {
dispatch(
executeCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode: finalImportMode,
})
);
executeCommand({
name: CommandNames.ManualImport,
files,
importMode: finalImportMode,
});
shouldClose = true;
}
@@ -623,7 +622,7 @@ function InteractiveImportModalContentInner(
originalItems,
selectedIds,
onModalClose,
dispatch,
executeCommand,
updateEpisodeFiles,
]);

View File

@@ -3,7 +3,8 @@ import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import { OrganizePreviewModel } from 'App/State/OrganizePreviewAppState';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import Alert from 'Components/Alert';
import CheckInput from 'Components/Form/CheckInput';
import Button from 'Components/Link/Button';
@@ -16,7 +17,6 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import formatSeason from 'Season/formatSeason';
import { useSingleSeries } from 'Series/useSeries';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
import { CheckInputChanged } from 'typings/inputs';
@@ -46,6 +46,7 @@ function OrganizePreviewModalContentInner({
onModalClose,
}: OrganizePreviewModalContentProps) {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const {
items,
isFetching: isPreviewFetching,
@@ -87,16 +88,14 @@ function OrganizePreviewModalContentInner({
const handleOrganizePress = useCallback(() => {
const files = getSelectedIds();
dispatch(
executeCommand({
name: commandNames.RENAME_FILES,
files,
seriesId,
})
);
executeCommand({
name: CommandNames.RenameFiles,
files,
seriesId,
});
onModalClose();
}, [seriesId, getSelectedIds, dispatch, onModalClose]);
}, [seriesId, getSelectedIds, executeCommand, onModalClose]);
useEffect(() => {
dispatch(fetchOrganizePreview({ seriesId, seasonNumber }));

View File

@@ -1,7 +1,7 @@
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommands, useExecuteCommand } from 'Commands/useCommands';
import Alert from 'Components/Alert';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
@@ -44,8 +44,6 @@ import useSeries, {
useToggleSeriesMonitored,
} from 'Series/useSeries';
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -85,7 +83,7 @@ interface SeriesDetailsProps {
}
function SeriesDetails({ seriesId }: SeriesDetailsProps) {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const series = useSingleSeries(seriesId);
const { toggleSeriesMonitored, isTogglingSeriesMonitored } =
@@ -114,11 +112,11 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
hasEpisodeFiles,
} = useEpisodeFiles({ seriesId });
const commands = useSelector(createCommandsSelector());
const { data: commands } = useCommands();
const { isRefreshing, isRenaming, isSearching } = useMemo(() => {
const seriesRefreshingCommand = findCommand(commands, {
name: commandNames.REFRESH_SERIES,
name: CommandNames.RefreshSeries,
});
const isSeriesRefreshingCommandExecuting = isCommandExecuting(
@@ -127,33 +125,39 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
const allSeriesRefreshing =
isSeriesRefreshingCommandExecuting &&
!seriesRefreshingCommand?.body.seriesIds?.length;
seriesRefreshingCommand &&
(!('seriesIds' in seriesRefreshingCommand.body) ||
seriesRefreshingCommand.body.seriesIds.length === 0);
const isSeriesRefreshing =
isSeriesRefreshingCommandExecuting &&
seriesRefreshingCommand?.body.seriesIds?.includes(seriesId);
seriesRefreshingCommand &&
'seriesIds' in seriesRefreshingCommand.body &&
seriesRefreshingCommand.body.seriesIds.includes(seriesId);
const isSearchingExecuting = isCommandExecuting(
findCommand(commands, {
name: commandNames.SERIES_SEARCH,
name: CommandNames.SeriesSearch,
seriesId,
})
);
const isRenamingFiles = isCommandExecuting(
findCommand(commands, {
name: commandNames.RENAME_FILES,
name: CommandNames.RenameFiles,
seriesId,
})
);
const isRenamingSeriesCommand = findCommand(commands, {
name: commandNames.RENAME_SERIES,
name: CommandNames.RenameSeries,
});
const isRenamingSeries =
isCommandExecuting(isRenamingSeriesCommand) &&
isRenamingSeriesCommand?.body?.seriesIds?.includes(seriesId);
isRenamingSeriesCommand &&
'seriesIds' in isRenamingSeriesCommand.body &&
isRenamingSeriesCommand.body.seriesIds.includes(seriesId);
return {
isRefreshing: isSeriesRefreshing || allSeriesRefreshing,
@@ -325,22 +329,18 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
);
const handleRefreshPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.REFRESH_SERIES,
seriesId,
})
);
}, [seriesId, dispatch]);
executeCommand({
name: CommandNames.RefreshSeries,
seriesId,
});
}, [seriesId, executeCommand]);
const handleSearchPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.SERIES_SEARCH,
seriesId,
})
);
}, [seriesId, dispatch]);
executeCommand({
name: CommandNames.SeriesSearch,
seriesId,
});
}, [seriesId, executeCommand]);
const populate = useCallback(() => {
refetchEpisodes();
@@ -356,7 +356,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
return () => {
unregisterPagePopulator(populate);
};
}, [populate, dispatch]);
}, [populate]);
useEffect(() => {
if ((!isRefreshing && wasRefreshing) || (!isRenaming && wasRenaming)) {

View File

@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useDispatch } from 'react-redux';
import { useAppDimension } from 'App/appStore';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommands } from 'Commands/useCommands';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
@@ -34,7 +34,6 @@ import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
import SeasonInteractiveSearchModal from 'Series/Search/SeasonInteractiveSearchModal';
import { Statistics } from 'Series/Series';
import { useSingleSeries, useToggleSeasonMonitored } from 'Series/useSeries';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { TableOptionsChangePayload } from 'typings/Table';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import isAfter from 'Utilities/Date/isAfter';
@@ -85,16 +84,15 @@ function getSeasonStatistics(episodes: Episode[]) {
};
}
function createIsSearchingSelector(seriesId: number, seasonNumber: number) {
return createSelector(createCommandsSelector(), (commands) => {
return isCommandExecuting(
findCommand(commands, {
name: commandNames.SEASON_SEARCH,
seriesId,
seasonNumber,
})
);
});
function useIsSearching(seriesId: number, seasonNumber: number) {
const { data: commands } = useCommands();
return isCommandExecuting(
findCommand(commands, {
name: CommandNames.SeasonSearch,
seriesId,
seasonNumber,
})
);
}
interface SeriesDetailsSeasonProps {
@@ -121,9 +119,7 @@ function SeriesDetailsSeason({
const { columns, sortKey, sortDirection } = useEpisodeOptions();
const isSmallScreen = useAppDimension('isSmallScreen');
const isSearching = useSelector(
createIsSearchingSelector(seriesId, seasonNumber)
);
const isSearching = useIsSearching(seriesId, seasonNumber);
const { sizeOnDisk = 0 } = statistics;
@@ -199,7 +195,7 @@ function SeriesDetailsSeason({
const handleSearchPress = useCallback(() => {
dispatch({
name: commandNames.SEASON_SEARCH,
name: CommandNames.SeasonSearch,
seriesId,
seasonNumber,
});

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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;

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting } from 'Commands/useCommands';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -16,7 +17,6 @@ import {
saveGeneralSettings,
setGeneralSettingsValue,
} from 'Store/Actions/settingsActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { useIsWindowsService } from 'System/Status/useSystemStatus';
import { useRestart } from 'System/useSystem';
@@ -47,9 +47,7 @@ function GeneralSettings() {
const dispatch = useDispatch();
const isWindowsService = useIsWindowsService();
const { mutate: restart } = useRestart();
const isResettingApiKey = useSelector(
createCommandExecutingSelector(commandNames.RESET_API_KEY)
);
const isResettingApiKey = useCommandExecuting(CommandNames.ResetApiKey);
const {
isFetching,

View File

@@ -1,6 +1,6 @@
import React, { FocusEvent, useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import FieldSet from 'Components/FieldSet';
import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
@@ -11,7 +11,6 @@ import Icon from 'Components/Icon';
import ClipboardButton from 'Components/Link/ClipboardButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { InputChanged } from 'typings/inputs';
import { PendingSection } from 'typings/pending';
import General from 'typings/Settings/General';
@@ -108,7 +107,7 @@ function SecuritySettings({
isResettingApiKey,
onInputChange,
}: SecuritySettingsProps) {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const [isConfirmApiKeyResetModalOpen, setIsConfirmApiKeyResetModalOpen] =
useState(false);
@@ -127,14 +126,14 @@ function SecuritySettings({
const handleConfirmResetApiKey = useCallback(() => {
setIsConfirmApiKeyResetModalOpen(false);
dispatch(executeCommand({ name: commandNames.RESET_API_KEY }));
}, [dispatch]);
executeCommand({ name: CommandNames.ResetApiKey });
}, [executeCommand]);
const handleCloseResetApiKeyModal = useCallback(() => {
setIsConfirmApiKeyResetModalOpen(false);
}, []);
// createCommandExecutingSelector(commandNames.RESET_API_KEY),
// createCommandExecutingSelector(CommandNames.RESET_API_KEY),
const authenticationEnabled =
authenticationMethod && authenticationMethod.value !== 'none';

View File

@@ -1,13 +1,12 @@
import React, { useCallback, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting } from 'Commands/useCommands';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import {
SaveCallback,
SettingsStateChange,
@@ -17,8 +16,8 @@ import QualityDefinitions from './Definition/QualityDefinitions';
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
function Quality() {
const isResettingQualityDefinitions = useSelector(
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS)
const isResettingQualityDefinitions = useCommandExecuting(
CommandNames.ResetQualityDefinitions
);
const saveDefinitions = useRef<() => void>();

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
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';
@@ -10,8 +10,6 @@ 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 { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './ResetQualityDefinitionsModalContent.css';
@@ -23,10 +21,9 @@ interface ResetQualityDefinitionsModalContentProps {
function ResetQualityDefinitionsModalContent({
onModalClose,
}: ResetQualityDefinitionsModalContentProps) {
const dispatch = useDispatch();
const isResettingQualityDefinitions = useSelector(
createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS)
const executeCommand = useExecuteCommand();
const isResettingQualityDefinitions = useCommandExecuting(
CommandNames.ResetQualityDefinitions
);
const [resetDefinitionTitles, setResetDefinitionTitles] = useState(false);
@@ -43,14 +40,12 @@ function ResetQualityDefinitionsModalContent({
setResetDefinitionTitles(false);
dispatch(
executeCommand({
name: commandNames.RESET_QUALITY_DEFINITIONS,
resetTitles,
})
);
executeCommand({
name: CommandNames.ResetQualityDefinitions,
resetTitles,
});
onModalClose();
}, [resetDefinitionTitles, dispatch, onModalClose]);
}, [resetDefinitionTitles, executeCommand, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>

View File

@@ -1,219 +0,0 @@
import { batchActions } from 'redux-batched-actions';
import { hideMessage, showMessage } from 'App/messagesStore';
import { messageTypes } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import { isSameCommand } from 'Utilities/Command';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { removeItem, updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
//
// Variables
export const section = 'commands';
let lastCommand = null;
let lastCommandTimeout = null;
const removeCommandTimeoutIds = {};
const commandFinishedCallbacks = {};
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: [],
handlers: {}
};
//
// Actions Types
export const FETCH_COMMANDS = 'commands/fetchCommands';
export const EXECUTE_COMMAND = 'commands/executeCommand';
export const CANCEL_COMMAND = 'commands/cancelCommand';
export const ADD_COMMAND = 'commands/addCommand';
export const UPDATE_COMMAND = 'commands/updateCommand';
export const FINISH_COMMAND = 'commands/finishCommand';
export const REMOVE_COMMAND = 'commands/removeCommand';
//
// Action Creators
export const fetchCommands = createThunk(FETCH_COMMANDS);
export const executeCommand = createThunk(EXECUTE_COMMAND);
export const cancelCommand = createThunk(CANCEL_COMMAND);
export const addCommand = createThunk(ADD_COMMAND);
export const updateCommand = createThunk(UPDATE_COMMAND);
export const finishCommand = createThunk(FINISH_COMMAND);
export const removeCommand = createThunk(REMOVE_COMMAND);
//
// Helpers
function showCommandMessage(payload, dispatch) {
const {
id,
name,
trigger,
message,
body = {},
status
} = payload;
const {
sendUpdatesToClient,
suppressMessages
} = body;
if (!message || !body || !sendUpdatesToClient || suppressMessages) {
return;
}
let type = messageTypes.INFO;
let hideAfter = 0;
if (status === 'completed') {
type = messageTypes.SUCCESS;
hideAfter = 4;
} else if (status === 'failed') {
type = messageTypes.ERROR;
hideAfter = trigger === 'manual' ? 10 : 4;
}
showMessage({
id,
name,
message,
type,
hideAfter
});
}
function scheduleRemoveCommand(command, dispatch) {
const {
id,
status
} = command;
if (status === 'queued') {
return;
}
const timeoutId = removeCommandTimeoutIds[id];
if (timeoutId) {
clearTimeout(timeoutId);
}
removeCommandTimeoutIds[id] = setTimeout(() => {
dispatch(batchActions([
removeCommand({ section: 'commands', id })
]));
hideMessage({ id });
delete removeCommandTimeoutIds[id];
}, 60000 * 5);
}
export function executeCommandHelper(payload, dispatch) {
// TODO: show a message for the user
if (lastCommand && isSameCommand(lastCommand, payload)) {
console.warn('Please wait at least 5 seconds before running this command again');
}
lastCommand = payload;
// clear last command after 5 seconds.
if (lastCommandTimeout) {
clearTimeout(lastCommandTimeout);
}
lastCommandTimeout = setTimeout(() => {
lastCommand = null;
}, 5000);
const {
commandFinished,
...requestPayload
} = payload;
const promise = createAjaxRequest({
url: '/command',
method: 'POST',
data: JSON.stringify(requestPayload),
dataType: 'json'
}).request;
return promise.then((data) => {
if (commandFinished) {
commandFinishedCallbacks[data.id] = commandFinished;
}
dispatch(addCommand(data));
});
}
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_COMMANDS]: createFetchHandler('commands', '/command'),
[EXECUTE_COMMAND]: function(getState, payload, dispatch) {
executeCommandHelper(payload, dispatch);
},
[CANCEL_COMMAND]: createRemoveItemHandler(section, '/command'),
[ADD_COMMAND]: function(getState, payload, dispatch) {
dispatch(updateItem({ section: 'commands', ...payload }));
},
[UPDATE_COMMAND]: function(getState, payload, dispatch) {
dispatch(updateItem({ section: 'commands', ...payload }));
showCommandMessage(payload, dispatch);
scheduleRemoveCommand(payload, dispatch);
},
[FINISH_COMMAND]: function(getState, payload, dispatch) {
const state = getState();
const handlers = state.commands.handlers;
Object.keys(handlers).forEach((key) => {
const handler = handlers[key];
if (handler.name === payload.name) {
dispatch(handler.handler(payload));
}
});
const commandFinished = commandFinishedCallbacks[payload.id];
if (commandFinished) {
commandFinished(payload);
}
delete commandFinishedCallbacks[payload.id];
dispatch(updateItem({ section: 'commands', ...payload }));
scheduleRemoveCommand(payload, dispatch);
showCommandMessage(payload, dispatch);
},
[REMOVE_COMMAND]: function(getState, payload, dispatch) {
dispatch(removeItem({ section: 'commands', ...payload }));
}
});
//
// Reducers
export const reducers = createHandleActions({}, defaultState, section);

View File

@@ -1,5 +1,4 @@
import * as captcha from './captchaActions';
import * as commands from './commandActions';
import * as episodeHistory from './episodeHistoryActions';
import * as importSeries from './importSeriesActions';
import * as interactiveImportActions from './interactiveImportActions';
@@ -11,7 +10,6 @@ import * as settings from './settingsActions';
export default [
captcha,
commands,
episodeHistory,
importSeries,
interactiveImportActions,

View File

@@ -1,11 +0,0 @@
import { createSelector } from 'reselect';
import { isCommandExecuting } from 'Utilities/Command';
import createCommandSelector from './createCommandSelector';
function createCommandExecutingSelector(name: string, constraints = {}) {
return createSelector(createCommandSelector(name, constraints), (command) => {
return command ? isCommandExecuting(command) : false;
});
}
export default createCommandExecutingSelector;

View File

@@ -1,11 +0,0 @@
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name: string, constraints = {}) {
return createSelector(createCommandsSelector(), (commands) => {
return findCommand(commands, { name, ...constraints });
});
}
export default createCommandSelector;

View File

@@ -1,13 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createCommandsSelector() {
return createSelector(
(state: AppState) => state.commands,
(commands) => {
return commands.items;
}
);
}
export default createCommandsSelector;

View File

@@ -1,14 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { isCommandExecuting } from 'Utilities/Command';
function createExecutingCommandsSelector() {
return createSelector(
(state: AppState) => state.commands.items,
(commands) => {
return commands.filter((command) => isCommandExecuting(command));
}
);
}
export default createExecutingCommandsSelector;

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames 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';
@@ -13,8 +13,6 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import BackupRow from './BackupRow';
import RestoreBackupModal from './RestoreBackupModal';
@@ -49,12 +47,10 @@ const columns: Column[] = [
];
function Backups() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { data: items, isLoading: isFetching, error, refetch } = useBackups();
const isBackupExecuting = useSelector(
createCommandExecutingSelector(commandNames.BACKUP)
);
const isBackupExecuting = useCommandExecuting(CommandNames.Backup);
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
@@ -63,12 +59,10 @@ function Backups() {
const noBackups = !items.length && !isFetching && !error;
const handleBackupPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.BACKUP,
})
);
}, [dispatch]);
executeCommand({
name: CommandNames.Backup,
});
}, [executeCommand]);
const handleRestorePress = useCallback(() => {
setIsRestoreModalOpen(true);

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames 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 FilterMenu from 'Components/Menu/FilterMenu';
@@ -15,8 +15,6 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { TableOptionsChangePayload } from 'typings/Table';
import translate from 'Utilities/String/translate';
import {
@@ -29,7 +27,7 @@ import LogsTableRow from './LogsTableRow';
import useEvents, { useFilters } from './useEvents';
function LogsTable() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const {
records,
totalPages,
@@ -47,9 +45,7 @@ function LogsTable() {
const filters = useFilters();
const isClearLogExecuting = useSelector(
createCommandExecutingSelector(commandNames.CLEAR_LOGS)
);
const isClearLogExecuting = useCommandExecuting(CommandNames.ClearLog);
const handleFilterSelect = useCallback(
(selectedFilterKey: string | number) => {
@@ -84,15 +80,15 @@ function LogsTable() {
}, [goToPage]);
const handleClearLogsPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.CLEAR_LOGS,
commandFinished: () => {
goToPage(1);
},
})
executeCommand(
{
name: CommandNames.ClearLog,
},
() => {
goToPage(1);
}
);
}, [dispatch, goToPage]);
}, [executeCommand, goToPage]);
return (
<PageContent title={translate('Logs')}>

View File

@@ -1,17 +1,15 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import LogFiles from '../LogFiles';
import useLogFiles from '../useLogFiles';
function AppLogFiles() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { data = [], isFetching, refetch } = useLogFiles();
const isDeleteFilesExecuting = useSelector(
createCommandExecutingSelector(commandNames.DELETE_LOG_FILES)
const isDeleteFilesExecuting = useCommandExecuting(
CommandNames.DeleteLogFiles
);
const handleRefreshPress = useCallback(() => {
@@ -19,15 +17,15 @@ function AppLogFiles() {
}, [refetch]);
const handleDeleteFilesPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.DELETE_LOG_FILES,
commandFinished: () => {
refetch();
},
})
executeCommand(
{
name: CommandNames.DeleteLogFiles,
},
() => {
refetch();
}
);
}, [dispatch, refetch]);
}, [executeCommand, refetch]);
return (
<LogFiles

View File

@@ -1,17 +1,15 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import LogFiles from '../LogFiles';
import { useUpdateLogFiles } from '../useLogFiles';
function UpdateLogFiles() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { data = [], isFetching, refetch } = useUpdateLogFiles();
const isDeleteFilesExecuting = useSelector(
createCommandExecutingSelector(commandNames.DELETE_UPDATE_LOG_FILES)
const isDeleteFilesExecuting = useCommandExecuting(
CommandNames.DeleteUpdateLogFiles
);
const handleRefreshPress = useCallback(() => {
@@ -19,15 +17,15 @@ function UpdateLogFiles() {
}, [refetch]);
const handleDeleteFilesPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.DELETE_UPDATE_LOG_FILES,
commandFinished: () => {
refetch();
},
})
executeCommand(
{
name: CommandNames.DeleteUpdateLogFiles,
},
() => {
refetch();
}
);
}, [dispatch, refetch]);
}, [executeCommand, refetch]);
return (
<LogFiles

View File

@@ -1,7 +1,8 @@
import moment from 'moment';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import { useCancelCommand } from 'Commands/useCommands';
import Icon, { IconProps } from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -9,7 +10,6 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
@@ -118,7 +118,7 @@ export default function QueuedTaskRow(props: QueuedTaskRowProps) {
clientUserAgent,
} = props;
const dispatch = useDispatch();
const { cancelCommand } = useCancelCommand(id);
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
useSelector(createUISettingsSelector());
@@ -142,8 +142,8 @@ export default function QueuedTaskRow(props: QueuedTaskRowProps) {
] = useModalOpenState(false);
const handleCancelPress = useCallback(() => {
dispatch(cancelCommand({ id }));
}, [id, dispatch]);
cancelCommand();
}, [cancelCommand]);
useEffect(() => {
updateTimeTimeoutId.current = setTimeout(() => {

View File

@@ -32,9 +32,9 @@ export default function QueuedTaskRowNameCell(
props: QueuedTaskRowNameCellProps
) {
const { commandName, body, clientUserAgent } = props;
const seriesIds = [...(body.seriesIds ?? [])];
const seriesIds = 'seriesIds' in body ? [...body.seriesIds] : [];
if (body.seriesId) {
if ('seriesId' in body && body.seriesId) {
seriesIds.push(body.seriesId);
}
@@ -48,7 +48,7 @@ export default function QueuedTaskRowNameCell(
{sortedSeries.length ? (
<span> - {formatTitles(sortedSeries.map((s) => s.title))}</span>
) : null}
{body.seasonNumber ? (
{'seasonNumber' in body && body.seasonNumber ? (
<span>
{' '}
{translate('SeasonNumberToken', {

View File

@@ -1,12 +1,10 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import React from 'react';
import { useCommands } from 'Commands/useCommands';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { fetchCommands } from 'Store/Actions/commandActions';
import translate from 'Utilities/String/translate';
import QueuedTaskRow from './QueuedTaskRow';
@@ -49,28 +47,33 @@ const columns: Column[] = [
];
export default function QueuedTasks() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items } = useSelector(
(state: AppState) => state.commands
);
const { data: commands, isLoading, isError } = useCommands();
useEffect(() => {
dispatch(fetchCommands());
}, [dispatch]);
if (isLoading) {
return (
<FieldSet legend={translate('Queue')}>
<LoadingIndicator />
</FieldSet>
);
}
if (isError) {
return (
<FieldSet legend={translate('Queue')}>
<div>Error loading commands</div>
</FieldSet>
);
}
return (
<FieldSet legend={translate('Queue')}>
{isFetching && !isPopulated && <LoadingIndicator />}
{isPopulated && (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return <QueuedTaskRow key={item.id} {...item} />;
})}
</TableBody>
</Table>
)}
<Table columns={columns}>
<TableBody>
{commands.map((item) => {
return <QueuedTaskRow key={item.id} {...item} />;
})}
</TableBody>
</Table>
</FieldSet>
);
}

View File

@@ -1,12 +1,11 @@
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useCommand, useExecuteCommand } from 'Commands/useCommands';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandSelector from 'Store/Selectors/createCommandSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { isCommandExecuting } from 'Utilities/Command';
import formatDate from 'Utilities/Date/formatDate';
@@ -34,10 +33,10 @@ function ScheduledTaskRow({
lastDuration,
nextExecution,
}: ScheduledTaskRowProps) {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { showRelativeDates, longDateFormat, shortDateFormat, timeFormat } =
useSelector(createUISettingsSelector());
const command = useSelector(createCommandSelector(taskName));
const command = useCommand(taskName);
const [time, setTime] = useState(Date.now());
@@ -81,8 +80,8 @@ function ScheduledTaskRow({
]);
const handleExecutePress = useCallback(() => {
dispatch(executeCommand({ name: taskName }));
}, [taskName, dispatch]);
executeCommand({ name: taskName });
}, [taskName, executeCommand]);
useEffect(() => {
const interval = setInterval(() => setTime(Date.now()), 1000);

View File

@@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useAppValue } from 'App/appStore';
import * as commandNames from 'Commands/commandNames';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
@@ -13,9 +14,7 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import useUpdateSettings from 'Settings/General/useUpdateSettings';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useSystemStatusData } from 'System/Status/useSystemStatus';
import { UpdateMechanism } from 'typings/Settings/General';
@@ -35,8 +34,8 @@ function Updates() {
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const isInstallingUpdate = useSelector(
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
const isInstallingUpdate = useCommandExecuting(
CommandNames.ApplicationUpdate
);
const {
@@ -53,6 +52,7 @@ function Updates() {
} = useUpdateSettings();
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
const isFetching = isLoadingUpdates || isLoadingSettings;
const isPopulated = isUpdatesFetched && isSettingsFetched;
@@ -92,20 +92,18 @@ function Updates() {
if (isMajorUpdate) {
setIsMajorUpdateModalOpen(true);
} else {
dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
executeCommand({ name: CommandNames.ApplicationUpdate });
}
}, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
}, [isMajorUpdate, setIsMajorUpdateModalOpen, executeCommand]);
const handleInstallLatestMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);
dispatch(
executeCommand({
name: commandNames.APPLICATION_UPDATE,
installMajorUpdate: true,
})
);
}, [setIsMajorUpdateModalOpen, dispatch]);
executeCommand({
name: CommandNames.ApplicationUpdate,
installMajorUpdate: true,
});
}, [setIsMajorUpdateModalOpen, executeCommand]);
const handleCancelMajorVersionPress = useCallback(() => {
setIsMajorUpdateModalOpen(false);

View File

@@ -3,42 +3,35 @@ import { CommandBody } from 'Commands/Command';
function isSameCommand(
commandA: Partial<CommandBody>,
commandB: Partial<CommandBody>
) {
if (
commandA.name?.toLocaleLowerCase() !== commandB.name?.toLocaleLowerCase()
) {
): boolean {
if (commandA.name?.toLowerCase() !== commandB.name?.toLowerCase()) {
return false;
}
for (const key in commandB) {
if (key !== 'name') {
const value = commandB[key];
const valueB = commandB[key as keyof CommandBody];
const valueA = commandA[key as keyof CommandBody];
if (Array.isArray(value)) {
const sortedB = [...value].sort((a, b) => a - b);
const commandAProp = commandA[key];
const sortedA = Array.isArray(commandAProp)
? [...commandAProp].sort((a, b) => a - b)
: [];
if (sortedA === sortedB) {
return true;
}
if (sortedA == null || sortedB == null) {
if (Array.isArray(valueB)) {
if (!Array.isArray(valueA)) {
return false;
}
// Sort both arrays for comparison
const sortedB = [...(valueB as number[])].sort((a, b) => a - b);
const sortedA = [...(valueA as number[])].sort((a, b) => a - b);
if (sortedA.length !== sortedB.length) {
return false;
}
for (let i = 0; i < sortedB.length; ++i) {
for (let i = 0; i < sortedB.length; i++) {
if (sortedB[i] !== sortedA[i]) {
return false;
}
}
} else if (value !== commandA[key]) {
} else if (valueB !== valueA) {
return false;
}
}

View File

@@ -5,10 +5,10 @@ import React, {
useMemo,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import * as commandNames 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 FilterMenu from 'Components/Menu/FilterMenu';
@@ -29,8 +29,6 @@ import EpisodeFileProvider from 'EpisodeFile/EpisodeFileProvider';
import { Filter } from 'Filters/Filter';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
@@ -57,7 +55,7 @@ function getMonitoredValue(
}
function CutoffUnmetContent() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const {
records,
@@ -74,11 +72,11 @@ function CutoffUnmetContent() {
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
useCutoffUnmetOptions();
const isSearchingForAllEpisodes = useSelector(
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH)
const isSearchingForAllEpisodes = useCommandExecuting(
CommandNames.CutoffUnmetEpisodeSearch
);
const isSearchingForSelectedEpisodes = useSelector(
createCommandExecutingSelector(commandNames.EPISODE_SEARCH)
const isSearchingForSelectedEpisodes = useCommandExecuting(
CommandNames.EpisodeSearch
);
const {
@@ -121,16 +119,16 @@ function CutoffUnmetContent() {
);
const handleSearchSelectedPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.EPISODE_SEARCH,
executeCommand(
{
name: CommandNames.EpisodeSearch,
episodeIds: getSelectedIds(),
commandFinished: () => {
refetch();
},
})
},
() => {
refetch();
}
);
}, [getSelectedIds, dispatch, refetch]);
}, [getSelectedIds, executeCommand, refetch]);
const handleSearchAllPress = useCallback(() => {
setIsConfirmSearchAllModalOpen(true);
@@ -141,17 +139,17 @@ function CutoffUnmetContent() {
}, []);
const handleSearchAllCutoffUnmetConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH,
commandFinished: () => {
refetch();
},
})
executeCommand(
{
name: CommandNames.CutoffUnmetEpisodeSearch,
},
() => {
refetch();
}
);
setIsConfirmSearchAllModalOpen(false);
}, [dispatch, refetch]);
}, [executeCommand, refetch]);
const handleToggleSelectedPress = useCallback(() => {
toggleEpisodesMonitored({

View File

@@ -1,8 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import * as commandNames 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 FilterMenu from 'Components/Menu/FilterMenu';
@@ -23,8 +23,6 @@ import { Filter } from 'Filters/Filter';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
@@ -51,7 +49,7 @@ function getMonitoredValue(
}
function MissingContent() {
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const {
records,
@@ -68,11 +66,11 @@ function MissingContent() {
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
useMissingOptions();
const isSearchingForAllEpisodes = useSelector(
createCommandExecutingSelector(commandNames.MISSING_EPISODE_SEARCH)
const isSearchingForAllEpisodes = useCommandExecuting(
CommandNames.MissingEpisodeSearch
);
const isSearchingForSelectedEpisodes = useSelector(
createCommandExecutingSelector(commandNames.EPISODE_SEARCH)
const isSearchingForSelectedEpisodes = useCommandExecuting(
CommandNames.EpisodeSearch
);
const {
@@ -114,16 +112,16 @@ function MissingContent() {
);
const handleSearchSelectedPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.EPISODE_SEARCH,
executeCommand(
{
name: CommandNames.EpisodeSearch,
episodeIds: getSelectedIds(),
commandFinished: () => {
refetch();
},
})
},
() => {
refetch();
}
);
}, [getSelectedIds, dispatch, refetch]);
}, [getSelectedIds, executeCommand, refetch]);
const handleSearchAllPress = useCallback(() => {
setIsConfirmSearchAllModalOpen(true);
@@ -134,17 +132,17 @@ function MissingContent() {
}, []);
const handleSearchAllMissingConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.MISSING_EPISODE_SEARCH,
commandFinished: () => {
refetch();
},
})
executeCommand(
{
name: CommandNames.MissingEpisodeSearch,
},
() => {
refetch();
}
);
setIsConfirmSearchAllModalOpen(false);
}, [dispatch, refetch]);
}, [executeCommand, refetch]);
const handleToggleSelectedPress = useCallback(() => {
toggleEpisodesMonitored({