From 6b479a5a10903c11c7dc2efbd19d0125045fcb8c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 20 Dec 2025 19:40:55 -0800 Subject: [PATCH] Use react-query for episode and series history --- .../src/Activity/History/useEpisodeHistory.ts | 20 ++++ frontend/src/Activity/History/useHistory.ts | 13 ++- .../src/Activity/History/useSeriesHistory.ts | 24 ++++ frontend/src/App/State/AppState.ts | 3 - frontend/src/App/State/HistoryAppState.ts | 16 --- .../src/Episode/History/EpisodeHistory.tsx | 46 ++------ .../src/Episode/History/EpisodeHistoryRow.tsx | 8 +- .../History/SeriesHistoryModalContent.tsx | 54 ++------- .../src/Series/History/SeriesHistoryRow.tsx | 10 +- .../Store/Actions/episodeHistoryActions.js | 109 ------------------ frontend/src/Store/Actions/index.js | 4 - .../src/Store/Actions/seriesHistoryActions.js | 102 ---------------- 12 files changed, 81 insertions(+), 328 deletions(-) create mode 100644 frontend/src/Activity/History/useEpisodeHistory.ts create mode 100644 frontend/src/Activity/History/useSeriesHistory.ts delete mode 100644 frontend/src/App/State/HistoryAppState.ts delete mode 100644 frontend/src/Store/Actions/episodeHistoryActions.js delete mode 100644 frontend/src/Store/Actions/seriesHistoryActions.js diff --git a/frontend/src/Activity/History/useEpisodeHistory.ts b/frontend/src/Activity/History/useEpisodeHistory.ts new file mode 100644 index 000000000..4465760f4 --- /dev/null +++ b/frontend/src/Activity/History/useEpisodeHistory.ts @@ -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({ + path: '/history/episode', + queryParams: { + episodeId, + }, + }); + + return { + data: data ?? DEFAULT_HISTORY, + ...result, + }; +}; + +export default useEpisodeHistory; diff --git a/frontend/src/Activity/History/useHistory.ts b/frontend/src/Activity/History/useHistory.ts index 251673a10..8ae79278d 100644 --- a/frontend/src/Activity/History/useHistory.ts +++ b/frontend/src/Activity/History/useHistory.ts @@ -112,6 +112,13 @@ export const FILTER_BUILDER: FilterBuilderProp[] = [ }, ]; +type HistoryType = 'episode' | 'series'; + +const MARK_AS_FAILED_QUERY_KEYS: Record = { + 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(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'); diff --git a/frontend/src/Activity/History/useSeriesHistory.ts b/frontend/src/Activity/History/useSeriesHistory.ts new file mode 100644 index 000000000..7ce51cacd --- /dev/null +++ b/frontend/src/Activity/History/useSeriesHistory.ts @@ -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({ + path: '/history/series', + queryParams: { + seriesId, + seasonNumber, + }, + }); + + return { + data: data ?? DEFAULT_HISTORY, + ...result, + }; +}; + +export default useSeriesHistory; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 1140f8cb3..f0339167c 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -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; } diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts deleted file mode 100644 index 0ffa92e0e..000000000 --- a/frontend/src/App/State/HistoryAppState.ts +++ /dev/null @@ -1,16 +0,0 @@ -import AppSectionState, { - AppSectionFilterState, - PagedAppSectionState, - TableAppSectionState, -} from 'App/State/AppSectionState'; -import History from 'typings/History'; - -export type SeriesHistoryAppState = AppSectionState; - -interface HistoryAppState - extends AppSectionState, - AppSectionFilterState, - PagedAppSectionState, - TableAppSectionState {} - -export default HistoryAppState; diff --git a/frontend/src/Episode/History/EpisodeHistory.tsx b/frontend/src/Episode/History/EpisodeHistory.tsx index ea323ec60..2610bcca2 100644 --- a/frontend/src/Episode/History/EpisodeHistory.tsx +++ b/frontend/src/Episode/History/EpisodeHistory.tsx @@ -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 ; @@ -101,22 +77,16 @@ function EpisodeHistory({ episodeId }: EpisodeHistoryProps) { ); } - if (isPopulated && !hasItems && !error) { + if (isFetched && !hasItems && !error) { return {translate('NoEpisodeHistory')}; } - if (isPopulated && hasItems && !error) { + if (isFetched && hasItems && !error) { return ( - {items.map((item) => { - return ( - - ); + {data.map((item) => { + return ; })}
diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.tsx b/frontend/src/Episode/History/EpisodeHistoryRow.tsx index 97b8cb479..a5c4a14f5 100644 --- a/frontend/src/Episode/History/EpisodeHistoryRow.tsx +++ b/frontend/src/Episode/History/EpisodeHistoryRow.tsx @@ -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); diff --git a/frontend/src/Series/History/SeriesHistoryModalContent.tsx b/frontend/src/Series/History/SeriesHistoryModalContent.tsx index 779292ea4..76d3911d8 100644 --- a/frontend/src/Series/History/SeriesHistoryModalContent.tsx +++ b/frontend/src/Series/History/SeriesHistoryModalContent.tsx @@ -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 ( @@ -132,26 +99,25 @@ function SeriesHistoryModalContent({ - {isFetching && !isPopulated ? : null} + {isFetching && !isFetched ? : null} {!isFetching && !!error ? ( {translate('HistoryLoadError')} ) : null} - {isPopulated && !hasItems && !error ? ( + {isFetched && !hasItems && !error ? (
{translate('NoHistory')}
) : null} - {isPopulated && hasItems && !error ? ( + {isFetched && hasItems && !error ? ( - {items.map((item) => { + {data.map((item) => { return ( ); })} diff --git a/frontend/src/Series/History/SeriesHistoryRow.tsx b/frontend/src/Series/History/SeriesHistoryRow.tsx index dfaff18ea..c1d82ae26 100644 --- a/frontend/src/Series/History/SeriesHistoryRow.tsx +++ b/frontend/src/Series/History/SeriesHistoryRow.tsx @@ -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); diff --git a/frontend/src/Store/Actions/episodeHistoryActions.js b/frontend/src/Store/Actions/episodeHistoryActions.js deleted file mode 100644 index cdbc192c9..000000000 --- a/frontend/src/Store/Actions/episodeHistoryActions.js +++ /dev/null @@ -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); - diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index fa68a8cd8..be4c1d689 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -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 ]; diff --git a/frontend/src/Store/Actions/seriesHistoryActions.js b/frontend/src/Store/Actions/seriesHistoryActions.js deleted file mode 100644 index 7fde47b30..000000000 --- a/frontend/src/Store/Actions/seriesHistoryActions.js +++ /dev/null @@ -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); -