Use react-query for episode and series history

This commit is contained in:
Mark McDowall 2025-12-20 19:40:55 -08:00
parent 5eb18fe274
commit 6b479a5a10
12 changed files with 81 additions and 328 deletions

View file

@ -0,0 +1,20 @@
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import History from 'typings/History';
const DEFAULT_HISTORY: History[] = [];
const useEpisodeHistory = (episodeId: number) => {
const { data, ...result } = useApiQuery<History[]>({
path: '/history/episode',
queryParams: {
episodeId,
},
});
return {
data: data ?? DEFAULT_HISTORY,
...result,
};
};
export default useEpisodeHistory;

View file

@ -112,6 +112,13 @@ export const FILTER_BUILDER: FilterBuilderProp<History>[] = [
},
];
type HistoryType = 'episode' | 'series';
const MARK_AS_FAILED_QUERY_KEYS: Record<HistoryType, string> = {
episode: '/history/episode',
series: '/history/series',
} as const;
const useHistory = () => {
const { page, goToPage } = usePage('history');
const { pageSize, selectedFilterKey, sortKey, sortDirection } =
@ -155,7 +162,7 @@ export const useFilters = () => {
return FILTERS;
};
export const useMarkAsFailed = (id: number) => {
export const useMarkAsFailed = (id: number, type?: HistoryType) => {
const queryClient = useQueryClient();
const [error, setError] = useState<string | null>(null);
@ -167,7 +174,9 @@ export const useMarkAsFailed = (id: number) => {
setError(null);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/history'] });
const queryKey = type ? MARK_AS_FAILED_QUERY_KEYS[type] : '/history';
queryClient.invalidateQueries({ queryKey: [queryKey] });
},
onError: () => {
setError('Error marking history item as failed');

View file

@ -0,0 +1,24 @@
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import History from 'typings/History';
const DEFAULT_HISTORY: History[] = [];
const useSeriesHistory = (
seriesId: number,
seasonNumber: number | undefined
) => {
const { data, ...result } = useApiQuery<History[]>({
path: '/history/series',
queryParams: {
seriesId,
seasonNumber,
},
});
return {
data: data ?? DEFAULT_HISTORY,
...result,
};
};
export default useSeriesHistory;

View file

@ -1,6 +1,5 @@
import BlocklistAppState from './BlocklistAppState';
import CaptchaAppState from './CaptchaAppState';
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
import ImportSeriesAppState from './ImportSeriesAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import OAuthAppState from './OAuthAppState';
@ -11,13 +10,11 @@ import SettingsAppState from './SettingsAppState';
interface AppState {
blocklist: BlocklistAppState;
captcha: CaptchaAppState;
episodeHistory: HistoryAppState;
importSeries: ImportSeriesAppState;
interactiveImport: InteractiveImportAppState;
oAuth: OAuthAppState;
organizePreview: OrganizePreviewAppState;
providerOptions: ProviderOptionsAppState;
seriesHistory: SeriesHistoryAppState;
settings: SettingsAppState;
}

View file

@ -1,16 +0,0 @@
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from 'App/State/AppSectionState';
import History from 'typings/History';
export type SeriesHistoryAppState = AppSectionState<History>;
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History>,
PagedAppSectionState,
TableAppSectionState {}
export default HistoryAppState;

View file

@ -1,6 +1,5 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import React from 'react';
import useEpisodeHistory from 'Activity/History/useEpisodeHistory';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -8,11 +7,6 @@ import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import {
clearEpisodeHistory,
episodeHistoryMarkAsFailed,
fetchEpisodeHistory,
} from 'Store/Actions/episodeHistoryActions';
import translate from 'Utilities/String/translate';
import EpisodeHistoryRow from './EpisodeHistoryRow';
@ -69,27 +63,9 @@ interface EpisodeHistoryProps {
}
function EpisodeHistory({ episodeId }: EpisodeHistoryProps) {
const dispatch = useDispatch();
const { items, isFetching, isPopulated, error } = useSelector(
(state: AppState) => state.episodeHistory
);
const { data, isFetching, isFetched, error } = useEpisodeHistory(episodeId);
const handleMarkAsFailedPress = useCallback(
(historyId: number) => {
dispatch(episodeHistoryMarkAsFailed({ historyId, episodeId }));
},
[episodeId, dispatch]
);
const hasItems = !!items.length;
useEffect(() => {
dispatch(fetchEpisodeHistory({ episodeId }));
return () => {
dispatch(clearEpisodeHistory());
};
}, [episodeId, dispatch]);
const hasItems = !!data.length;
if (isFetching) {
return <LoadingIndicator />;
@ -101,22 +77,16 @@ function EpisodeHistory({ episodeId }: EpisodeHistoryProps) {
);
}
if (isPopulated && !hasItems && !error) {
if (isFetched && !hasItems && !error) {
return <Alert kind={kinds.INFO}>{translate('NoEpisodeHistory')}</Alert>;
}
if (isPopulated && hasItems && !error) {
if (isFetched && hasItems && !error) {
return (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return (
<EpisodeHistoryRow
key={item.id}
{...item}
onMarkAsFailedPress={handleMarkAsFailedPress}
/>
);
{data.map((item) => {
return <EpisodeHistoryRow key={item.id} {...item} />;
})}
</TableBody>
</Table>

View file

@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import HistoryDetails from 'Activity/History/Details/HistoryDetails';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import { useMarkAsFailed } from 'Activity/History/useHistory';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@ -51,7 +52,6 @@ interface EpisodeHistoryRowProps {
date: string;
data: HistoryData;
downloadId?: string;
onMarkAsFailedPress: (id: number) => void;
}
function EpisodeHistoryRow({
@ -66,18 +66,18 @@ function EpisodeHistoryRow({
date,
data,
downloadId,
onMarkAsFailedPress,
}: EpisodeHistoryRowProps) {
const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false);
const { markAsFailed } = useMarkAsFailed(id, 'episode');
const handleMarkAsFailedPress = useCallback(() => {
setIsMarkAsFailedModalOpen(true);
}, []);
const handleConfirmMarkAsFailed = useCallback(() => {
onMarkAsFailedPress(id);
markAsFailed();
setIsMarkAsFailedModalOpen(false);
}, [id, onMarkAsFailedPress]);
}, [markAsFailed]);
const handleMarkAsFailedModalClose = useCallback(() => {
setIsMarkAsFailedModalOpen(false);

View file

@ -1,6 +1,5 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import React from 'react';
import useSeriesHistory from 'Activity/History/useSeriesHistory';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@ -14,11 +13,6 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import formatSeason from 'Season/formatSeason';
import {
clearSeriesHistory,
fetchSeriesHistory,
seriesHistoryMarkAsFailed,
} from 'Store/Actions/seriesHistoryActions';
import translate from 'Utilities/String/translate';
import SeriesHistoryRow from './SeriesHistoryRow';
@ -86,40 +80,13 @@ function SeriesHistoryModalContent({
seasonNumber,
onModalClose,
}: SeriesHistoryModalContentProps) {
const dispatch = useDispatch();
const { isFetching, isPopulated, error, items } = useSelector(
(state: AppState) => state.seriesHistory
const { isFetching, isFetched, error, data } = useSeriesHistory(
seriesId,
seasonNumber
);
const fullSeries = seasonNumber == null;
const hasItems = !!items.length;
const handleMarkAsFailedPress = useCallback(
(historyId: number) => {
dispatch(
seriesHistoryMarkAsFailed({
historyId,
seriesId,
seasonNumber,
})
);
},
[seriesId, seasonNumber, dispatch]
);
useEffect(() => {
dispatch(
fetchSeriesHistory({
seriesId,
seasonNumber,
})
);
return () => {
dispatch(clearSeriesHistory());
};
}, [seriesId, seasonNumber, dispatch]);
const hasItems = !!data.length;
return (
<ModalContent onModalClose={onModalClose}>
@ -132,26 +99,25 @@ function SeriesHistoryModalContent({
</ModalHeader>
<ModalBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{isFetching && !isFetched ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
) : null}
{isPopulated && !hasItems && !error ? (
{isFetched && !hasItems && !error ? (
<div>{translate('NoHistory')}</div>
) : null}
{isPopulated && hasItems && !error ? (
{isFetched && hasItems && !error ? (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
{data.map((item) => {
return (
<SeriesHistoryRow
key={item.id}
fullSeries={fullSeries}
{...item}
onMarkAsFailedPress={handleMarkAsFailedPress}
/>
);
})}

View file

@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import HistoryDetails from 'Activity/History/Details/HistoryDetails';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import { useMarkAsFailed } from 'Activity/History/useHistory';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@ -39,7 +40,6 @@ interface SeriesHistoryRowProps {
downloadId?: string;
fullSeries: boolean;
customFormatScore: number;
onMarkAsFailedPress: (historyId: number) => void;
}
function SeriesHistoryRow({
@ -57,11 +57,10 @@ function SeriesHistoryRow({
downloadId,
fullSeries,
customFormatScore,
onMarkAsFailedPress,
}: SeriesHistoryRowProps) {
const series = useSingleSeries(seriesId);
const episode = useEpisode(episodeId, 'episodes');
const { markAsFailed } = useMarkAsFailed(id, 'series');
const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false);
const EpisodeComponent = fullSeries ? SeasonEpisodeNumber : EpisodeNumber;
@ -90,9 +89,8 @@ function SeriesHistoryRow({
}, []);
const handleConfirmMarkAsFailed = useCallback(() => {
onMarkAsFailedPress(id);
setIsMarkAsFailedModalOpen(false);
}, [id, onMarkAsFailedPress]);
markAsFailed();
}, [markAsFailed]);
const handleMarkAsFailedModalClose = useCallback(() => {
setIsMarkAsFailedModalOpen(false);

View file

@ -1,109 +0,0 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'episodeHistory';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_EPISODE_HISTORY = 'episodeHistory/fetchEpisodeHistory';
export const CLEAR_EPISODE_HISTORY = 'episodeHistory/clearEpisodeHistory';
export const EPISODE_HISTORY_MARK_AS_FAILED = 'episodeHistory/episodeHistoryMarkAsFailed';
//
// Action Creators
export const fetchEpisodeHistory = createThunk(FETCH_EPISODE_HISTORY);
export const clearEpisodeHistory = createAction(CLEAR_EPISODE_HISTORY);
export const episodeHistoryMarkAsFailed = createThunk(EPISODE_HISTORY_MARK_AS_FAILED);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_EPISODE_HISTORY]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
const queryParams = {
pageSize: 1000,
page: 1,
sortKey: 'date',
sortDirection: sortDirections.DESCENDING,
episodeId: payload.episodeId
};
const promise = createAjaxRequest({
url: '/history',
data: queryParams
}).request;
promise.done((data) => {
dispatch(batchActions([
update({ section, data: data.records }),
set({
section,
isFetching: false,
isPopulated: true,
error: null
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
},
[EPISODE_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) {
const {
historyId,
episodeId
} = payload;
const promise = createAjaxRequest({
url: `/history/failed/${historyId}`,
method: 'POST'
}).request;
promise.done(() => {
dispatch(fetchEpisodeHistory({ episodeId }));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[CLEAR_EPISODE_HISTORY]: (state) => {
return Object.assign({}, state, defaultState);
}
}, defaultState, section);

View file

@ -1,21 +1,17 @@
import * as captcha from './captchaActions';
import * as episodeHistory from './episodeHistoryActions';
import * as importSeries from './importSeriesActions';
import * as interactiveImportActions from './interactiveImportActions';
import * as oAuth from './oAuthActions';
import * as organizePreview from './organizePreviewActions';
import * as providerOptions from './providerOptionActions';
import * as seriesHistory from './seriesHistoryActions';
import * as settings from './settingsActions';
export default [
captcha,
episodeHistory,
importSeries,
interactiveImportActions,
oAuth,
organizePreview,
providerOptions,
seriesHistory,
settings
];

View file

@ -1,102 +0,0 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'seriesHistory';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_SERIES_HISTORY = 'seriesHistory/fetchSeriesHistory';
export const CLEAR_SERIES_HISTORY = 'seriesHistory/clearSeriesHistory';
export const SERIES_HISTORY_MARK_AS_FAILED = 'seriesHistory/seriesHistoryMarkAsFailed';
//
// Action Creators
export const fetchSeriesHistory = createThunk(FETCH_SERIES_HISTORY);
export const clearSeriesHistory = createAction(CLEAR_SERIES_HISTORY);
export const seriesHistoryMarkAsFailed = createThunk(SERIES_HISTORY_MARK_AS_FAILED);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_SERIES_HISTORY]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
url: '/history/series',
data: payload
}).request;
promise.done((data) => {
dispatch(batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
},
[SERIES_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) {
const {
historyId,
seriesId,
seasonNumber
} = payload;
const promise = createAjaxRequest({
url: `/history/failed/${historyId}`,
method: 'POST',
dataType: 'json'
}).request;
promise.done(() => {
dispatch(fetchSeriesHistory({ seriesId, seasonNumber }));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[CLEAR_SERIES_HISTORY]: (state) => {
return Object.assign({}, state, defaultState);
}
}, defaultState, section);