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