mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
Use react-query for episodes
This commit is contained in:
parent
a97f2c016b
commit
1178c98341
21 changed files with 400 additions and 566 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
|
|
@ -12,12 +11,10 @@ import Table from 'Components/Table/Table';
|
|||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
||||
import useEpisodes from 'Episode/useEpisodes';
|
||||
import { useCustomFiltersList } from 'Filters/useCustomFilters';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||
import HistoryItem from 'typings/History';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
|
|
@ -53,17 +50,22 @@ function History() {
|
|||
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
||||
useHistoryOptions();
|
||||
|
||||
const episodeIds = useMemo(() => {
|
||||
return selectUniqueIds<HistoryItem, number>(records, 'episodeId');
|
||||
}, [records]);
|
||||
|
||||
const {
|
||||
isFetching: isEpisodesFetching,
|
||||
isFetched: isEpisodesFetched,
|
||||
error: episodesError,
|
||||
} = useEpisodes({ episodeIds });
|
||||
|
||||
const filters = useFilters();
|
||||
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
||||
useSelector(createEpisodesFetchingSelector());
|
||||
const customFilters = useCustomFiltersList('history');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isFetchingAny = isLoading || isEpisodesFetching;
|
||||
const isAllPopulated = isFetched && (isEpisodesPopulated || !records.length);
|
||||
const isAllPopulated = isFetched && (isEpisodesFetched || !records.length);
|
||||
const hasError = error || episodesError;
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
|
|
@ -99,25 +101,6 @@ function History() {
|
|||
refetch();
|
||||
}, [goToPage, refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(clearEpisodes());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const episodeIds = selectUniqueIds<HistoryItem, number>(
|
||||
records,
|
||||
'episodeId'
|
||||
);
|
||||
|
||||
if (episodeIds.length) {
|
||||
dispatch(fetchEpisodes({ episodeIds }));
|
||||
} else {
|
||||
dispatch(clearEpisodes());
|
||||
}
|
||||
}, [records, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
refetch();
|
||||
|
|
|
|||
|
|
@ -22,12 +22,11 @@ import Table from 'Components/Table/Table';
|
|||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
||||
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 { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import QueueModel from 'typings/Queue';
|
||||
|
|
@ -79,8 +78,17 @@ function QueueContent() {
|
|||
const { isGrabbing, grabQueueItems } = useGrabQueueItems();
|
||||
|
||||
const { count } = useQueueStatus();
|
||||
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
||||
useSelector(createEpisodesFetchingSelector());
|
||||
|
||||
const episodeIds = useMemo(() => {
|
||||
return selectUniqueIds<QueueModel, number>(records, 'episodeIds');
|
||||
}, [records]);
|
||||
|
||||
const {
|
||||
isFetching: isEpisodesFetching,
|
||||
isFetched: isEpisodesFetched,
|
||||
error: episodesError,
|
||||
} = useEpisodes({ episodeIds });
|
||||
|
||||
const customFilters = useCustomFiltersList('queue');
|
||||
|
||||
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
||||
|
|
@ -109,7 +117,7 @@ function QueueContent() {
|
|||
// Use isLoading over isFetched to avoid losing the table UI when switching pages
|
||||
const isAllPopulated =
|
||||
!isLoading &&
|
||||
(isEpisodesPopulated ||
|
||||
(isEpisodesFetched ||
|
||||
!records.length ||
|
||||
records.every((e) => !e.episodeIds?.length));
|
||||
const hasError = error || episodesError;
|
||||
|
|
@ -187,16 +195,6 @@ function QueueContent() {
|
|||
[goToPage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const episodeIds = selectUniqueIds(records, 'episodeIds');
|
||||
|
||||
if (episodeIds.length) {
|
||||
dispatch(fetchEpisodes({ episodeIds }));
|
||||
} else {
|
||||
dispatch(clearEpisodes());
|
||||
}
|
||||
}, [records, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
refetch();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import useEpisodesWithIds from 'Episode/useEpisodesWithIds';
|
||||
import { useEpisodesWithIds } from 'Episode/useEpisode';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import Language from 'Language/Language';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||
import React from 'react';
|
||||
import DocumentTitle from 'react-document-title';
|
||||
|
|
@ -7,14 +7,13 @@ import { Store } from 'redux';
|
|||
import Page from 'Components/Page/Page';
|
||||
import ApplyTheme from './ApplyTheme';
|
||||
import AppRoutes from './AppRoutes';
|
||||
import { queryClient } from './queryClient';
|
||||
|
||||
interface AppProps {
|
||||
store: Store;
|
||||
history: ConnectedRouterProps['history'];
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function App({ store, history }: AppProps) {
|
||||
return (
|
||||
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { Error } from './AppSectionState';
|
|||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CaptchaAppState from './CaptchaAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import EpisodesAppState from './EpisodesAppState';
|
||||
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
|
||||
import ImportSeriesAppState from './ImportSeriesAppState';
|
||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
|
|
@ -41,7 +40,6 @@ interface AppState {
|
|||
captcha: CaptchaAppState;
|
||||
commands: CommandAppState;
|
||||
episodeHistory: HistoryAppState;
|
||||
episodes: EpisodesAppState;
|
||||
importSeries: ImportSeriesAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
oAuth: OAuthAppState;
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Episode from 'Episode/Episode';
|
||||
|
||||
interface EpisodesAppState extends AppSectionState<Episode> {
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export default EpisodesAppState;
|
||||
3
frontend/src/App/queryClient.ts
Normal file
3
frontend/src/App/queryClient.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClient = new QueryClient();
|
||||
|
|
@ -150,13 +150,37 @@ function SignalRListener() {
|
|||
}
|
||||
|
||||
if (name === 'episode') {
|
||||
if (version < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.action === 'updated') {
|
||||
dispatch(
|
||||
updateItem({
|
||||
section: 'episodes',
|
||||
updateOnly: true,
|
||||
...body.resource,
|
||||
})
|
||||
const updatedItem = body.resource as Episode;
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ['/episode'] },
|
||||
(oldData: Episode[] | undefined) => {
|
||||
if (!oldData) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
const itemIndex = oldData.findIndex(
|
||||
(item) => item.id === updatedItem.id
|
||||
);
|
||||
|
||||
// Don't add episode if not found
|
||||
if (itemIndex === -1) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
return oldData.map((item) => {
|
||||
if (item.id === updatedItem.id) {
|
||||
return updatedItem;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
|
|
@ -10,10 +9,13 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|||
import Episode from 'Episode/Episode';
|
||||
import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import useEpisode, { EpisodeEntity } from 'Episode/useEpisode';
|
||||
import useEpisode, {
|
||||
EpisodeEntity,
|
||||
getQueryKey,
|
||||
useToggleEpisodesMonitored,
|
||||
} from 'Episode/useEpisode';
|
||||
import Series from 'Series/Series';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeHistory from './History/EpisodeHistory';
|
||||
import EpisodeSearch from './Search/EpisodeSearch';
|
||||
|
|
@ -28,7 +30,6 @@ export interface EpisodeDetailsModalContentProps {
|
|||
episodeEntity: EpisodeEntity;
|
||||
seriesId: number;
|
||||
episodeTitle: string;
|
||||
isSaving?: boolean;
|
||||
showOpenSeriesButton?: boolean;
|
||||
selectedTab?: EpisodeDetailsTab;
|
||||
startInteractiveSearch?: boolean;
|
||||
|
|
@ -36,22 +37,17 @@ export interface EpisodeDetailsModalContentProps {
|
|||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
||||
const {
|
||||
function EpisodeDetailsModalContent({
|
||||
episodeId,
|
||||
episodeEntity = episodeEntities.EPISODES,
|
||||
seriesId,
|
||||
episodeTitle,
|
||||
isSaving = false,
|
||||
showOpenSeriesButton = false,
|
||||
startInteractiveSearch = false,
|
||||
selectedTab = 'details',
|
||||
onTabChange,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
}: EpisodeDetailsModalContentProps) {
|
||||
const [currentlySelectedTab, setCurrentlySelectedTab] = useState(selectedTab);
|
||||
|
||||
const {
|
||||
|
|
@ -70,6 +66,10 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
|||
monitored,
|
||||
} = useEpisode(episodeId, episodeEntity) as Episode;
|
||||
|
||||
const { toggleEpisodesMonitored, isToggling } = useToggleEpisodesMonitored(
|
||||
getQueryKey(episodeEntity)!
|
||||
);
|
||||
|
||||
const handleTabSelect = useCallback(
|
||||
(selectedIndex: number) => {
|
||||
const tab = TABS[selectedIndex];
|
||||
|
|
@ -81,15 +81,12 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
|||
|
||||
const handleMonitorEpisodePress = useCallback(
|
||||
(monitored: boolean) => {
|
||||
dispatch(
|
||||
toggleEpisodeMonitored({
|
||||
episodeEntity,
|
||||
episodeId,
|
||||
toggleEpisodesMonitored({
|
||||
episodeIds: [episodeId],
|
||||
monitored,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[episodeEntity, episodeId, dispatch]
|
||||
[episodeId, toggleEpisodesMonitored]
|
||||
);
|
||||
|
||||
const seriesLink = `/series/${titleSlug}`;
|
||||
|
|
@ -101,7 +98,7 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
|||
monitored={monitored}
|
||||
size={18}
|
||||
isDisabled={!seriesMonitored}
|
||||
isSaving={isSaving}
|
||||
isSaving={isToggling}
|
||||
onPress={handleMonitorEpisodePress}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createEpisodesFetchingSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes,
|
||||
(episodes) => {
|
||||
return {
|
||||
isEpisodesFetching: episodes.isFetching,
|
||||
isEpisodesPopulated: episodes.isPopulated,
|
||||
episodesError: episodes.error,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createEpisodesFetchingSelector;
|
||||
149
frontend/src/Episode/episodeOptionsStore.ts
Normal file
149
frontend/src/Episode/episodeOptionsStore.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { createElement } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Column from 'Components/Table/Column';
|
||||
import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface EpisodeSelectOptions {
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
const { useOptions, useOption, setOptions, setOption, setSort } =
|
||||
createOptionsStore<EpisodeSelectOptions>('episode_options', () => {
|
||||
return {
|
||||
sortKey: 'episodeNumber',
|
||||
sortDirection: 'descending',
|
||||
columns: [
|
||||
{
|
||||
name: 'monitored',
|
||||
label: '',
|
||||
columnLabel: () => translate('Monitored'),
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
{
|
||||
name: 'episodeNumber',
|
||||
label: '#',
|
||||
isVisible: true,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isVisible: true,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: () => translate('Path'),
|
||||
isVisible: false,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: () => translate('RelativePath'),
|
||||
isVisible: false,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'airDateUtc',
|
||||
label: () => translate('AirDate'),
|
||||
isVisible: true,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'runtime',
|
||||
label: () => translate('Runtime'),
|
||||
isVisible: false,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'audioInfo',
|
||||
label: () => translate('AudioInfo'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'videoCodec',
|
||||
label: () => translate('VideoCodec'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'videoDynamicRangeType',
|
||||
label: () => translate('VideoDynamicRange'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'audioLanguages',
|
||||
label: () => translate('AudioLanguages'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'subtitleLanguages',
|
||||
label: () => translate('SubtitleLanguages'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isVisible: false,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: () => translate('ReleaseGroup'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('Formats'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: () => translate('CustomFormatScore'),
|
||||
label: createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isVisible: false,
|
||||
isSortable: true,
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
columnLabel: () => translate('IndexerFlags'),
|
||||
label: createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags'),
|
||||
}),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('Status'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
columnLabel: () => translate('Actions'),
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export const useEpisodeOptions = useOptions;
|
||||
export const setEpisodeOptions = setOptions;
|
||||
export const useEpisodeOption = useOption;
|
||||
export const setEpisodeOption = setOption;
|
||||
export const setEpisodeSort = setSort;
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { create } from 'zustand';
|
||||
import AppState from 'App/State/AppState';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
|
||||
import { CalendarItem } from 'typings/Calendar';
|
||||
|
|
@ -17,39 +14,24 @@ export type EpisodeEntity =
|
|||
|
||||
interface EpisodeQueryKeyStore {
|
||||
calendar: QueryKey | null;
|
||||
episodes: QueryKey | null;
|
||||
cutoffUnmet: QueryKey | null;
|
||||
missing: QueryKey | null;
|
||||
}
|
||||
|
||||
const episodeQueryKeyStore = create<EpisodeQueryKeyStore>(() => ({
|
||||
calendar: null,
|
||||
episodes: null,
|
||||
cutoffUnmet: null,
|
||||
missing: null,
|
||||
}));
|
||||
|
||||
function createEpisodeSelector(episodeId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes.items,
|
||||
(episodes) => {
|
||||
return episodes.find(({ id }) => id === episodeId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// No-op...ish
|
||||
function createNoOpEpisodeSelector(_episodeId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes.items,
|
||||
(_episodes) => {
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const getQueryKey = (episodeEntity: EpisodeEntity) => {
|
||||
switch (episodeEntity) {
|
||||
case 'calendar':
|
||||
return episodeQueryKeyStore.getState().calendar;
|
||||
case 'episodes':
|
||||
return episodeQueryKeyStore.getState().episodes;
|
||||
case 'wanted.cutoffUnmet':
|
||||
return episodeQueryKeyStore.getState().cutoffUnmet;
|
||||
case 'wanted.missing':
|
||||
|
|
@ -67,6 +49,9 @@ export const setEpisodeQueryKey = (
|
|||
case 'calendar':
|
||||
episodeQueryKeyStore.setState({ calendar: queryKey });
|
||||
break;
|
||||
case 'episodes':
|
||||
episodeQueryKeyStore.setState({ episodes: queryKey });
|
||||
break;
|
||||
case 'wanted.cutoffUnmet':
|
||||
episodeQueryKeyStore.setState({ cutoffUnmet: queryKey });
|
||||
break;
|
||||
|
|
@ -83,19 +68,6 @@ const useEpisode = (
|
|||
episodeEntity: EpisodeEntity
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
let selector = createEpisodeSelector;
|
||||
|
||||
switch (episodeEntity) {
|
||||
case 'calendar':
|
||||
case 'wanted.cutoffUnmet':
|
||||
case 'wanted.missing':
|
||||
selector = createNoOpEpisodeSelector;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const result = useSelector(selector(episodeId));
|
||||
const queryKey = getQueryKey(episodeEntity);
|
||||
|
||||
if (episodeEntity === 'calendar') {
|
||||
|
|
@ -104,7 +76,17 @@ const useEpisode = (
|
|||
.getQueryData<CalendarItem[]>(queryKey)
|
||||
?.find((e) => e.id === episodeId)
|
||||
: undefined;
|
||||
} else if (
|
||||
}
|
||||
|
||||
if (episodeEntity === 'episodes') {
|
||||
return queryKey
|
||||
? queryClient
|
||||
.getQueryData<Episode[]>(queryKey)
|
||||
?.find((e) => e.id === episodeId)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
episodeEntity === 'wanted.cutoffUnmet' ||
|
||||
episodeEntity === 'wanted.missing'
|
||||
) {
|
||||
|
|
@ -115,7 +97,7 @@ const useEpisode = (
|
|||
: undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default useEpisode;
|
||||
|
|
@ -128,15 +110,33 @@ interface ToggleEpisodesMonitored {
|
|||
export const useToggleEpisodesMonitored = (queryKey: QueryKey) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, isPending } = useApiMutation<
|
||||
const { mutate, isPending, variables } = useApiMutation<
|
||||
unknown,
|
||||
ToggleEpisodesMonitored
|
||||
>({
|
||||
path: '/episode/monitor',
|
||||
method: 'PUT',
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.setQueryData<Episode[] | undefined>(
|
||||
queryKey,
|
||||
(oldEpisodes) => {
|
||||
if (!oldEpisodes) {
|
||||
return oldEpisodes;
|
||||
}
|
||||
|
||||
return oldEpisodes.map((oldEpisode) => {
|
||||
if (variables.episodeIds.includes(oldEpisode.id)) {
|
||||
return {
|
||||
...oldEpisode,
|
||||
monitored: variables.monitored,
|
||||
};
|
||||
}
|
||||
|
||||
return oldEpisode;
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -144,5 +144,20 @@ export const useToggleEpisodesMonitored = (queryKey: QueryKey) => {
|
|||
return {
|
||||
toggleEpisodesMonitored: mutate,
|
||||
isToggling: isPending,
|
||||
togglingEpisodeIds: variables?.episodeIds ?? [],
|
||||
togglingMonitored: variables?.monitored,
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_EPISODES: Episode[] = [];
|
||||
|
||||
export const useEpisodesWithIds = (episodeIds: number[]) => {
|
||||
const queryClient = useQueryClient();
|
||||
const queryKey = getQueryKey('episodes');
|
||||
|
||||
return queryKey
|
||||
? queryClient
|
||||
.getQueryData<Episode[]>(queryKey)
|
||||
?.filter((e) => episodeIds.includes(e.id)) ?? DEFAULT_EPISODES
|
||||
: DEFAULT_EPISODES;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import clientSideFilterAndSort from 'Utilities/Filter/clientSideFilterAndSort';
|
||||
import Episode from './Episode';
|
||||
import { useEpisodeOptions } from './episodeOptionsStore';
|
||||
import { setEpisodeQueryKey } from './useEpisode';
|
||||
|
||||
const DEFAULT_EPISODES: Episode[] = [];
|
||||
|
||||
|
|
@ -10,6 +14,7 @@ interface SeriesEpisodes {
|
|||
interface SeasonEpisodes {
|
||||
seriesId: number | undefined;
|
||||
seasonNumber: number | undefined;
|
||||
isSelection: boolean;
|
||||
}
|
||||
|
||||
interface EpisodeIds {
|
||||
|
|
@ -27,9 +32,17 @@ export type EpisodeFilter =
|
|||
| EpisodeFileId;
|
||||
|
||||
const useEpisodes = (params: EpisodeFilter) => {
|
||||
const result = useApiQuery<Episode[]>({
|
||||
const setQueryKey = !('isSelection' in params);
|
||||
|
||||
const { isPlaceholderData, queryKey, ...result } = useApiQuery<Episode[]>({
|
||||
path: '/episode',
|
||||
queryParams: { ...params },
|
||||
queryParams:
|
||||
'isSelection' in params
|
||||
? {
|
||||
seriesId: params.seriesId,
|
||||
seasonNumber: params.seasonNumber,
|
||||
}
|
||||
: { ...params },
|
||||
queryOptions: {
|
||||
enabled:
|
||||
('seriesId' in params && params.seriesId !== undefined) ||
|
||||
|
|
@ -38,10 +51,39 @@ const useEpisodes = (params: EpisodeFilter) => {
|
|||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (setQueryKey && !isPlaceholderData) {
|
||||
setEpisodeQueryKey('episodes', queryKey);
|
||||
}
|
||||
}, [setQueryKey, isPlaceholderData, queryKey]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
queryKey,
|
||||
data: result.data ?? DEFAULT_EPISODES,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEpisodes;
|
||||
|
||||
export const useSeasonEpisodes = (seriesId: number, seasonNumber: number) => {
|
||||
const { data, ...result } = useEpisodes({ seriesId });
|
||||
const { sortKey, sortDirection } = useEpisodeOptions();
|
||||
|
||||
const seasonEpisodes = useMemo(() => {
|
||||
const { data: seasonEpisodes } = clientSideFilterAndSort(
|
||||
data.filter((episode) => episode.seasonNumber === seasonNumber),
|
||||
{
|
||||
sortKey,
|
||||
sortDirection,
|
||||
}
|
||||
);
|
||||
|
||||
return seasonEpisodes;
|
||||
}, [data, seasonNumber, sortKey, sortDirection]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: seasonEpisodes,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Episode from './Episode';
|
||||
|
||||
function getEpisodes(episodeIds: number[], episodes: Episode[]) {
|
||||
return episodeIds.reduce<Episode[]>((acc, id) => {
|
||||
const episode = episodes.find((episode) => episode.id === id);
|
||||
|
||||
if (episode) {
|
||||
acc.push(episode);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function createEpisodeSelector(episodeIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes.items,
|
||||
(episodes) => {
|
||||
return getEpisodes(episodeIds, episodes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function useEpisodesWithIds(episodeIds: number[]) {
|
||||
return useSelector(createEpisodeSelector(episodeIds));
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ function SelectEpisodeModalContentInner(props: SelectEpisodeModalContentProps) {
|
|||
const { isFetching, isFetched, data, error } = useEpisodes({
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
isSelection: true,
|
||||
});
|
||||
|
||||
const { sortKey, sortDirection } = useEpisodeSelectionOptions();
|
||||
|
|
@ -255,6 +256,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
|||
const { data } = useEpisodes({
|
||||
seriesId: props.seriesId,
|
||||
seasonNumber: props.seasonNumber,
|
||||
isSelection: true,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import moment from 'moment';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
|
|
@ -20,6 +18,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import useEpisodes from 'Episode/useEpisodes';
|
||||
import useEpisodeFiles from 'EpisodeFile/useEpisodeFiles';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import {
|
||||
|
|
@ -43,7 +42,6 @@ import { getSeriesStatusDetails } from 'Series/SeriesStatus';
|
|||
import useSeries from 'Series/useSeries';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
|
|
@ -75,26 +73,6 @@ function getDateYear(date: string | undefined) {
|
|||
return dateDate.format('YYYY');
|
||||
}
|
||||
|
||||
function createEpisodesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes,
|
||||
(episodes) => {
|
||||
const { items, isFetching, isPopulated, error } = episodes;
|
||||
|
||||
const hasEpisodes = !!items.length;
|
||||
const hasMonitoredEpisodes = items.some((e) => e.monitored);
|
||||
|
||||
return {
|
||||
isEpisodesFetching: isFetching,
|
||||
isEpisodesPopulated: isPopulated,
|
||||
episodesError: error,
|
||||
hasEpisodes,
|
||||
hasMonitoredEpisodes,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface ExpandedState {
|
||||
allExpanded: boolean;
|
||||
allCollapsed: boolean;
|
||||
|
|
@ -112,12 +90,19 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
|
||||
const {
|
||||
isEpisodesFetching,
|
||||
isEpisodesPopulated,
|
||||
episodesError,
|
||||
hasEpisodes,
|
||||
hasMonitoredEpisodes,
|
||||
} = useSelector(createEpisodesSelector());
|
||||
isFetching: isEpisodesFetching,
|
||||
isFetched: isEpisodesFetched,
|
||||
error: episodesError,
|
||||
data,
|
||||
refetch: refetchEpisodes,
|
||||
} = useEpisodes({ seriesId });
|
||||
|
||||
const { hasEpisodes, hasMonitoredEpisodes } = useMemo(() => {
|
||||
return {
|
||||
hasEpisodes: data.length > 0,
|
||||
hasMonitoredEpisodes: data.some((e) => e.monitored),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
const {
|
||||
isFetching: isEpisodeFilesFetching,
|
||||
|
|
@ -358,8 +343,8 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
}, [seriesId, dispatch]);
|
||||
|
||||
const populate = useCallback(() => {
|
||||
dispatch(fetchEpisodes({ seriesId }));
|
||||
}, [seriesId, dispatch]);
|
||||
refetchEpisodes();
|
||||
}, [refetchEpisodes]);
|
||||
|
||||
useEffect(() => {
|
||||
populate();
|
||||
|
|
@ -370,7 +355,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
|
||||
return () => {
|
||||
unregisterPagePopulator(populate);
|
||||
dispatch(clearEpisodes());
|
||||
};
|
||||
}, [populate, dispatch]);
|
||||
|
||||
|
|
@ -439,7 +423,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
|
||||
const isPopulated = isEpisodesPopulated && isEpisodeFilesFetched;
|
||||
const isPopulated = isEpisodesFetched && isEpisodeFilesFetched;
|
||||
|
||||
return (
|
||||
<SeriesDetailsProvider seriesId={seriesId}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import EpisodesAppState from 'App/State/EpisodesAppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
|
|
@ -18,6 +17,13 @@ import Table from 'Components/Table/Table';
|
|||
import TableBody from 'Components/Table/TableBody';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Episode from 'Episode/Episode';
|
||||
import {
|
||||
setEpisodeOptions,
|
||||
setEpisodeSort,
|
||||
useEpisodeOptions,
|
||||
} from 'Episode/episodeOptionsStore';
|
||||
import { getQueryKey, useToggleEpisodesMonitored } from 'Episode/useEpisode';
|
||||
import { useSeasonEpisodes } from 'Episode/useEpisodes';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
|
|
@ -27,13 +33,7 @@ import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
|
|||
import SeasonInteractiveSearchModal from 'Series/Search/SeasonInteractiveSearchModal';
|
||||
import { Statistics } from 'Series/Series';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import {
|
||||
setEpisodesSort,
|
||||
setEpisodesTableOption,
|
||||
toggleEpisodesMonitored,
|
||||
} from 'Store/Actions/episodeActions';
|
||||
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
|
|
@ -86,21 +86,6 @@ function getSeasonStatistics(episodes: Episode[]) {
|
|||
};
|
||||
}
|
||||
|
||||
function createEpisodesSelector(seasonNumber: number) {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('episodes'),
|
||||
(episodes: EpisodesAppState) => {
|
||||
const { items, columns, sortKey, sortDirection } = episodes;
|
||||
|
||||
const episodesInSeason = items.filter(
|
||||
(episode) => episode.seasonNumber === seasonNumber
|
||||
);
|
||||
|
||||
return { items: episodesInSeason, columns, sortKey, sortDirection };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createIsSearchingSelector(seriesId: number, seasonNumber: number) {
|
||||
return createSelector(createCommandsSelector(), (commands) => {
|
||||
return isCommandExecuting(
|
||||
|
|
@ -134,10 +119,9 @@ function SeriesDetailsSeason({
|
|||
}: SeriesDetailsSeasonProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { monitored: seriesMonitored, path } = useSeries(seriesId)!;
|
||||
const { data: items } = useSeasonEpisodes(seriesId, seasonNumber);
|
||||
|
||||
const { items, columns, sortKey, sortDirection } = useSelector(
|
||||
createEpisodesSelector(seasonNumber)
|
||||
);
|
||||
const { columns, sortKey, sortDirection } = useEpisodeOptions();
|
||||
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
const isSearching = useSelector(
|
||||
|
|
@ -162,10 +146,11 @@ function SeriesDetailsSeason({
|
|||
const [isInteractiveSearchModalOpen, setIsInteractiveSearchModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const lastToggledEpisode = useRef<number | null>(null);
|
||||
const itemsRef = useRef(items);
|
||||
const { toggleEpisodesMonitored, isToggling, togglingEpisodeIds } =
|
||||
useToggleEpisodesMonitored(getQueryKey('episodes')!);
|
||||
|
||||
itemsRef.current = items;
|
||||
const lastToggledEpisode = useRef<number | null>(null);
|
||||
const hasSetInitalExpand = useRef(false);
|
||||
|
||||
const seasonNumberTitle =
|
||||
seasonNumber === 0
|
||||
|
|
@ -196,25 +181,23 @@ function SeriesDetailsSeason({
|
|||
{ shiftKey }: { shiftKey: boolean }
|
||||
) => {
|
||||
const lastToggled = lastToggledEpisode.current;
|
||||
const episodeIds = [episodeId];
|
||||
const episodeIds = new Set([episodeId]);
|
||||
|
||||
if (shiftKey && lastToggled) {
|
||||
const { lower, upper } = getToggledRange(items, episodeId, lastToggled);
|
||||
for (let i = lower; i < upper; i++) {
|
||||
episodeIds.push(items[i].id);
|
||||
episodeIds.add(items[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
lastToggledEpisode.current = episodeId;
|
||||
|
||||
dispatch(
|
||||
toggleEpisodesMonitored({
|
||||
episodeIds,
|
||||
episodeIds: Array.from(episodeIds),
|
||||
monitored: value,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[items, dispatch]
|
||||
[items, toggleEpisodesMonitored]
|
||||
);
|
||||
|
||||
const handleSearchPress = useCallback(() => {
|
||||
|
|
@ -259,32 +242,36 @@ function SeriesDetailsSeason({
|
|||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string, sortDirection?: SortDirection) => {
|
||||
dispatch(
|
||||
setEpisodesSort({
|
||||
setEpisodeSort({
|
||||
sortKey,
|
||||
sortDirection,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setEpisodesTableOption(payload));
|
||||
setEpisodeOptions(payload);
|
||||
},
|
||||
[dispatch]
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasSetInitalExpand.current || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasSetInitalExpand.current = true;
|
||||
|
||||
const expand =
|
||||
itemsRef.current.some(
|
||||
items.some(
|
||||
(item) =>
|
||||
isAfter(item.airDateUtc) || isAfter(item.airDateUtc, { days: -30 })
|
||||
) || itemsRef.current.every((item) => !item.airDateUtc);
|
||||
) || items.every((item) => !item.airDateUtc);
|
||||
|
||||
onExpandPress(seasonNumber, expand && seasonNumber > 0);
|
||||
}, [seriesId, seasonNumber, onExpandPress]);
|
||||
}, [items, seriesId, seasonNumber, onExpandPress]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((previousEpisodeFileCount ?? 0) > 0 && episodeFileCount === 0) {
|
||||
|
|
@ -505,6 +492,9 @@ function SeriesDetailsSeason({
|
|||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
isSaving={
|
||||
isToggling && togglingEpisodeIds.includes(item.id)
|
||||
}
|
||||
onMonitorEpisodePress={handleMonitorEpisodePress}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,296 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import Icon from 'Components/Icon';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { updateItem } from './baseActions';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'episodes';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'episodeNumber',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
items: [],
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'monitored',
|
||||
columnLabel: () => translate('Monitored'),
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'episodeNumber',
|
||||
label: '#',
|
||||
isVisible: true,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isVisible: true,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: () => translate('Path'),
|
||||
isVisible: false,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: () => translate('RelativePath'),
|
||||
isVisible: false,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'airDateUtc',
|
||||
label: () => translate('AirDate'),
|
||||
isVisible: true,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'runtime',
|
||||
label: () => translate('Runtime'),
|
||||
isVisible: false,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'audioInfo',
|
||||
label: () => translate('AudioInfo'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'videoCodec',
|
||||
label: () => translate('VideoCodec'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'videoDynamicRangeType',
|
||||
label: () => translate('VideoDynamicRange'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'audioLanguages',
|
||||
label: () => translate('AudioLanguages'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'subtitleLanguages',
|
||||
label: () => translate('SubtitleLanguages'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isVisible: false,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: () => translate('ReleaseGroup'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('Formats'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: () => translate('CustomFormatScore'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isVisible: false,
|
||||
isSortable: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
columnLabel: () => translate('IndexerFlags'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('Status'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: () => translate('Actions'),
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'episodes.columns',
|
||||
'episodes.sortDirection',
|
||||
'episodes.sortKey'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_EPISODES = 'episodes/fetchEpisodes';
|
||||
export const SET_EPISODES_SORT = 'episodes/setEpisodesSort';
|
||||
export const SET_EPISODES_TABLE_OPTION = 'episodes/setEpisodesTableOption';
|
||||
export const CLEAR_EPISODES = 'episodes/clearEpisodes';
|
||||
export const TOGGLE_EPISODE_MONITORED = 'episodes/toggleEpisodeMonitored';
|
||||
export const TOGGLE_EPISODES_MONITORED = 'episodes/toggleEpisodesMonitored';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchEpisodes = createThunk(FETCH_EPISODES);
|
||||
export const setEpisodesSort = createAction(SET_EPISODES_SORT);
|
||||
export const setEpisodesTableOption = createAction(SET_EPISODES_TABLE_OPTION);
|
||||
export const clearEpisodes = createAction(CLEAR_EPISODES);
|
||||
export const toggleEpisodeMonitored = createThunk(TOGGLE_EPISODE_MONITORED);
|
||||
export const toggleEpisodesMonitored = createThunk(TOGGLE_EPISODES_MONITORED);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_EPISODES]: createFetchHandler(section, '/episode'),
|
||||
|
||||
[TOGGLE_EPISODE_MONITORED]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
episodeId: id,
|
||||
episodeEntity = episodeEntities.EPISODES,
|
||||
monitored
|
||||
} = payload;
|
||||
|
||||
dispatch(updateItem({
|
||||
id,
|
||||
section: episodeEntity,
|
||||
isSaving: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/episode/${id}`,
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({ monitored }),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(updateItem({
|
||||
id,
|
||||
section: episodeEntity,
|
||||
isSaving: false,
|
||||
monitored
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(updateItem({
|
||||
id,
|
||||
section: episodeEntity,
|
||||
isSaving: false
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[TOGGLE_EPISODES_MONITORED]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
episodeIds,
|
||||
episodeEntity = episodeEntities.EPISODES,
|
||||
monitored
|
||||
} = payload;
|
||||
|
||||
const episodeSection = _.last(episodeEntity.split('.'));
|
||||
|
||||
dispatch(batchActions(
|
||||
episodeIds.map((episodeId) => {
|
||||
return updateItem({
|
||||
id: episodeId,
|
||||
section: episodeSection,
|
||||
isSaving: true
|
||||
});
|
||||
})
|
||||
));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/episode/monitor',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({ episodeIds, monitored }),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions(
|
||||
episodeIds.map((episodeId) => {
|
||||
return updateItem({
|
||||
id: episodeId,
|
||||
section: episodeSection,
|
||||
isSaving: false,
|
||||
monitored
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(batchActions(
|
||||
episodeIds.map((episodeId) => {
|
||||
return updateItem({
|
||||
id: episodeId,
|
||||
section: episodeSection,
|
||||
isSaving: false
|
||||
});
|
||||
})
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_EPISODES_TABLE_OPTION]: createSetTableOptionReducer(section),
|
||||
|
||||
[CLEAR_EPISODES]: (state) => {
|
||||
return Object.assign({}, state, {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
});
|
||||
},
|
||||
|
||||
[SET_EPISODES_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import * as app from './appActions';
|
||||
import * as captcha from './captchaActions';
|
||||
import * as commands from './commandActions';
|
||||
import * as episodes from './episodeActions';
|
||||
import * as episodeHistory from './episodeHistoryActions';
|
||||
import * as importSeries from './importSeriesActions';
|
||||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
|
|
@ -17,7 +16,6 @@ export default [
|
|||
app,
|
||||
captcha,
|
||||
commands,
|
||||
episodes,
|
||||
episodeHistory,
|
||||
importSeries,
|
||||
interactiveImportActions,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { queryClient } from 'App/queryClient';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
|
||||
import getFilterTypePredicate from 'Helpers/Props/getFilterTypePredicate';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
|
|
@ -14,7 +15,6 @@ import createHandleActions from './Creators/createHandleActions';
|
|||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
||||
import { fetchEpisodes } from './episodeActions';
|
||||
|
||||
//
|
||||
// Local
|
||||
|
|
@ -720,7 +720,7 @@ export const actionHandlers = handleThunks({
|
|||
|
||||
promise.done((data) => {
|
||||
if (shouldFetchEpisodesAfterUpdate) {
|
||||
dispatch(fetchEpisodes({ seriesId: seriesIds[0] }));
|
||||
queryClient.invalidateQueries({ queryKey: ['/episode'] });
|
||||
}
|
||||
|
||||
dispatch(set({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import KeysMatching from 'typings/Helpers/KeysMatching';
|
||||
|
||||
function selectUniqueIds<T, K>(items: T[], idProp: KeysMatching<T, K>) {
|
||||
function selectUniqueIds<T, K>(
|
||||
items: T[],
|
||||
idProp: KeysMatching<T, K | K[]>
|
||||
): K[] {
|
||||
const result = items.reduce((acc: Set<K>, item) => {
|
||||
if (!item[idProp]) {
|
||||
return acc;
|
||||
|
|
|
|||
Loading…
Reference in a new issue