From 4071278183fd80fdf522c5fe2d766a790766fae6 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 14 Nov 2025 16:31:59 -0800 Subject: [PATCH] Use react-query for wanted missing and cutoff unmet --- frontend/src/App/State/AppState.ts | 2 - frontend/src/App/State/WantedAppState.ts | 29 -- frontend/src/Components/SignalRListener.tsx | 70 +++- frontend/src/Episode/EpisodeStatus.tsx | 13 +- frontend/src/Episode/useEpisode.ts | 89 +++-- frontend/src/Helpers/Hooks/usePage.ts | 4 + .../src/Helpers/Hooks/usePagedApiQuery.ts | 3 +- .../Series/Details/SeriesDetailsSeason.tsx | 2 +- frontend/src/Store/Actions/index.js | 4 +- frontend/src/Store/Actions/wantedActions.js | 330 ------------------ .../src/Wanted/CutoffUnmet/CutoffUnmet.tsx | 158 +++------ .../CutoffUnmet/cutoffUnmetOptionsStore.ts | 67 ++++ .../src/Wanted/CutoffUnmet/useCutoffUnmet.tsx | 69 ++++ frontend/src/Wanted/Missing/Missing.tsx | 154 +++----- .../src/Wanted/Missing/missingOptionsStore.ts | 62 ++++ frontend/src/Wanted/Missing/useMissing.tsx | 69 ++++ 16 files changed, 512 insertions(+), 613 deletions(-) delete mode 100644 frontend/src/App/State/WantedAppState.ts delete mode 100644 frontend/src/Store/Actions/wantedActions.js create mode 100644 frontend/src/Wanted/CutoffUnmet/cutoffUnmetOptionsStore.ts create mode 100644 frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx create mode 100644 frontend/src/Wanted/Missing/missingOptionsStore.ts create mode 100644 frontend/src/Wanted/Missing/useMissing.tsx diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 02d6e5f56..76180df56 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -21,7 +21,6 @@ import RootFolderAppState from './RootFolderAppState'; import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState'; import SettingsAppState from './SettingsAppState'; import TagsAppState from './TagsAppState'; -import WantedAppState from './WantedAppState'; export interface FilterBuilderPropOption { id: string; @@ -99,7 +98,6 @@ interface AppState { seriesIndex: SeriesIndexAppState; settings: SettingsAppState; tags: TagsAppState; - wanted: WantedAppState; } export default AppState; diff --git a/frontend/src/App/State/WantedAppState.ts b/frontend/src/App/State/WantedAppState.ts deleted file mode 100644 index 5031df5f9..000000000 --- a/frontend/src/App/State/WantedAppState.ts +++ /dev/null @@ -1,29 +0,0 @@ -import AppSectionState, { - AppSectionFilterState, - PagedAppSectionState, - TableAppSectionState, -} from 'App/State/AppSectionState'; -import Episode from 'Episode/Episode'; - -interface WantedEpisode extends Episode { - isSaving?: boolean; -} - -interface WantedCutoffUnmetAppState - extends AppSectionState, - AppSectionFilterState, - PagedAppSectionState, - TableAppSectionState {} - -interface WantedMissingAppState - extends AppSectionState, - AppSectionFilterState, - PagedAppSectionState, - TableAppSectionState {} - -interface WantedAppState { - cutoffUnmet: WantedCutoffUnmetAppState; - missing: WantedMissingAppState; -} - -export default WantedAppState; diff --git a/frontend/src/Components/SignalRListener.tsx b/frontend/src/Components/SignalRListener.tsx index 41dcb54cb..d87fdad0e 100644 --- a/frontend/src/Components/SignalRListener.tsx +++ b/frontend/src/Components/SignalRListener.tsx @@ -3,11 +3,13 @@ import { HubConnectionBuilder, LogLevel, } from '@microsoft/signalr'; -import { useQueryClient } from '@tanstack/react-query'; +import { QueryKey, useQueryClient } from '@tanstack/react-query'; import { useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; import ModelBase from 'App/ModelBase'; import Command from 'Commands/Command'; +import Episode from 'Episode/Episode'; +import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery'; import { setAppValue, setVersion } from 'Store/Actions/appActions'; import { removeItem, updateItem } from 'Store/Actions/baseActions'; import { @@ -315,30 +317,30 @@ function SignalRListener() { } if (name === 'wanted/cutoff') { - if (body.action === 'updated') { - dispatch( - updateItem({ - section: 'wanted.cutoffUnmet', - updateOnly: true, - ...body.resource, - }) - ); + if (version < 5 || body.action !== 'updated') { + return; } + updatePagedItem( + queryClient, + ['/wanted/cutoff'], + body.resource as Episode + ); + return; } if (name === 'wanted/missing') { - if (body.action === 'updated') { - dispatch( - updateItem({ - section: 'wanted.missing', - updateOnly: true, - ...body.resource, - }) - ); + if (version < 5 || body.action !== 'updated') { + return; } + updatePagedItem( + queryClient, + ['/wanted/missing'], + body.resource as Episode + ); + return; } @@ -385,3 +387,37 @@ function SignalRListener() { } export default SignalRListener; + +const updatePagedItem = ( + queryClient: ReturnType, + queryKey: QueryKey, + updatedItem: T +) => { + queryClient.setQueriesData( + { queryKey }, + (oldData: PagedQueryResponse | undefined) => { + if (!oldData) { + return oldData; + } + + const itemIndex = oldData.records.findIndex( + (item) => item.id === updatedItem.id + ); + + if (itemIndex === -1) { + return oldData; + } + + return { + ...oldData, + records: oldData.records.map((item) => { + if (item.id === updatedItem.id) { + return updatedItem; + } + + return item; + }), + }; + } + ); +}; diff --git a/frontend/src/Episode/EpisodeStatus.tsx b/frontend/src/Episode/EpisodeStatus.tsx index be470b411..924718564 100644 --- a/frontend/src/Episode/EpisodeStatus.tsx +++ b/frontend/src/Episode/EpisodeStatus.tsx @@ -3,7 +3,6 @@ import { useQueueItemForEpisode } from 'Activity/Queue/Details/QueueDetailsProvi import QueueDetails from 'Activity/Queue/QueueDetails'; import Icon from 'Components/Icon'; import ProgressBar from 'Components/ProgressBar'; -import Episode from 'Episode/Episode'; import useEpisode, { EpisodeEntity } from 'Episode/useEpisode'; import useEpisodeFile from 'EpisodeFile/useEpisodeFile'; import { icons, kinds, sizes } from 'Helpers/Props'; @@ -23,19 +22,19 @@ function EpisodeStatus({ episodeEntity = 'episodes', episodeFileId, }: EpisodeStatusProps) { - const { - airDateUtc, - monitored, - grabbed = false, - } = useEpisode(episodeId, episodeEntity) as Episode; - + const episode = useEpisode(episodeId, episodeEntity); const queueItem = useQueueItemForEpisode(episodeId); const episodeFile = useEpisodeFile(episodeFileId); + const { airDateUtc, grabbed, monitored } = episode || {}; const hasEpisodeFile = !!episodeFile; const isQueued = !!queueItem; const hasAired = isBefore(airDateUtc); + if (!episode) { + return null; + } + if (isQueued) { const { sizeLeft, size } = queueItem; diff --git a/frontend/src/Episode/useEpisode.ts b/frontend/src/Episode/useEpisode.ts index 88f822c36..aac16cf51 100644 --- a/frontend/src/Episode/useEpisode.ts +++ b/frontend/src/Episode/useEpisode.ts @@ -3,7 +3,10 @@ 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'; +import Episode from './Episode'; export type EpisodeEntity = | 'calendar' @@ -14,10 +17,14 @@ export type EpisodeEntity = interface EpisodeQueryKeyStore { calendar: QueryKey | null; + cutoffUnmet: QueryKey | null; + missing: QueryKey | null; } const episodeQueryKeyStore = create(() => ({ calendar: null, + cutoffUnmet: null, + missing: null, })); function createEpisodeSelector(episodeId?: number) { @@ -30,7 +37,7 @@ function createEpisodeSelector(episodeId?: number) { } // No-op...ish -function createCalendarEpisodeSelector(_episodeId?: number) { +function createNoOpEpisodeSelector(_episodeId?: number) { return createSelector( (state: AppState) => state.episodes.items, (_episodes) => { @@ -39,23 +46,18 @@ function createCalendarEpisodeSelector(_episodeId?: number) { ); } -function createWantedCutoffUnmetEpisodeSelector(episodeId?: number) { - return createSelector( - (state: AppState) => state.wanted.cutoffUnmet.items, - (episodes) => { - return episodes.find(({ id }) => id === episodeId); - } - ); -} - -function createWantedMissingEpisodeSelector(episodeId?: number) { - return createSelector( - (state: AppState) => state.wanted.missing.items, - (episodes) => { - return episodes.find(({ id }) => id === episodeId); - } - ); -} +const getQueryKey = (episodeEntity: EpisodeEntity) => { + switch (episodeEntity) { + case 'calendar': + return episodeQueryKeyStore.getState().calendar; + case 'wanted.cutoffUnmet': + return episodeQueryKeyStore.getState().cutoffUnmet; + case 'wanted.missing': + return episodeQueryKeyStore.getState().missing; + default: + return null; + } +}; export const setEpisodeQueryKey = ( episodeEntity: EpisodeEntity, @@ -65,6 +67,12 @@ export const setEpisodeQueryKey = ( case 'calendar': episodeQueryKeyStore.setState({ calendar: queryKey }); break; + case 'wanted.cutoffUnmet': + episodeQueryKeyStore.setState({ cutoffUnmet: queryKey }); + break; + case 'wanted.missing': + episodeQueryKeyStore.setState({ missing: queryKey }); + break; default: break; } @@ -79,31 +87,62 @@ const useEpisode = ( switch (episodeEntity) { case 'calendar': - selector = createCalendarEpisodeSelector; - break; case 'wanted.cutoffUnmet': - selector = createWantedCutoffUnmetEpisodeSelector; - break; case 'wanted.missing': - selector = createWantedMissingEpisodeSelector; + selector = createNoOpEpisodeSelector; break; default: break; } const result = useSelector(selector(episodeId)); + const queryKey = getQueryKey(episodeEntity); if (episodeEntity === 'calendar') { - const queryKey = episodeQueryKeyStore((state) => state.calendar); - return queryKey ? queryClient .getQueryData(queryKey) ?.find((e) => e.id === episodeId) : undefined; + } else if ( + episodeEntity === 'wanted.cutoffUnmet' || + episodeEntity === 'wanted.missing' + ) { + return queryKey + ? queryClient + .getQueryData>(queryKey) + ?.records?.find((e) => e.id === episodeId) + : undefined; } return result; }; export default useEpisode; + +interface ToggleEpisodesMonitored { + episodeIds: number[]; + monitored: boolean; +} + +export const useToggleEpisodesMonitored = (queryKey: QueryKey) => { + const queryClient = useQueryClient(); + + const { mutate, isPending } = useApiMutation< + unknown, + ToggleEpisodesMonitored + >({ + path: '/episode/monitor', + method: 'PUT', + mutationOptions: { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey }); + }, + }, + }); + + return { + toggleEpisodesMonitored: mutate, + isToggling: isPending, + }; +}; diff --git a/frontend/src/Helpers/Hooks/usePage.ts b/frontend/src/Helpers/Hooks/usePage.ts index 84b552632..af30c28a0 100644 --- a/frontend/src/Helpers/Hooks/usePage.ts +++ b/frontend/src/Helpers/Hooks/usePage.ts @@ -4,15 +4,19 @@ import { create } from 'zustand'; interface PageStore { blocklist: number; + cutoffUnmet: number; events: number; history: number; + missing: number; queue: number; } const pageStore = create(() => ({ blocklist: 1, + cutoffUnmet: 1, events: 1, history: 1, + missing: 1, queue: 1, })); diff --git a/frontend/src/Helpers/Hooks/usePagedApiQuery.ts b/frontend/src/Helpers/Hooks/usePagedApiQuery.ts index f25932ca2..d243832db 100644 --- a/frontend/src/Helpers/Hooks/usePagedApiQuery.ts +++ b/frontend/src/Helpers/Hooks/usePagedApiQuery.ts @@ -15,7 +15,7 @@ interface PagedQueryOptions extends QueryOptions> { filters?: PropertyFilter[]; } -interface PagedQueryResponse { +export interface PagedQueryResponse { page: number; pageSize: number; sortKey: string; @@ -94,6 +94,7 @@ const usePagedApiQuery = (options: PagedQueryOptions) => { return { ...query, + queryKey, records: data?.records ?? DEFAULT_RECORDS, totalRecords: data?.totalRecords ?? 0, totalPages: data?.totalPages ?? 0, diff --git a/frontend/src/Series/Details/SeriesDetailsSeason.tsx b/frontend/src/Series/Details/SeriesDetailsSeason.tsx index fd5298121..1fbde255f 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeason.tsx +++ b/frontend/src/Series/Details/SeriesDetailsSeason.tsx @@ -210,7 +210,7 @@ function SeriesDetailsSeason({ dispatch( toggleEpisodesMonitored({ episodeIds, - value, + monitored: value, }) ); }, diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 985dbdc1e..0c5db21d0 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -19,7 +19,6 @@ import * as seriesHistory from './seriesHistoryActions'; import * as seriesIndex from './seriesIndexActions'; import * as settings from './settingsActions'; import * as tags from './tagActions'; -import * as wanted from './wantedActions'; export default [ app, @@ -42,6 +41,5 @@ export default [ seriesHistory, seriesIndex, settings, - tags, - wanted + tags ]; diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js deleted file mode 100644 index dac4d0c8d..000000000 --- a/frontend/src/Store/Actions/wantedActions.js +++ /dev/null @@ -1,330 +0,0 @@ -import { createAction } from 'redux-actions'; -import { filterTypes, sortDirections } from 'Helpers/Props'; -import { createThunk, handleThunks } from 'Store/thunks'; -import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; -import translate from 'Utilities/String/translate'; -import createBatchToggleEpisodeMonitoredHandler from './Creators/createBatchToggleEpisodeMonitoredHandler'; -import createHandleActions from './Creators/createHandleActions'; -import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; -import createClearReducer from './Creators/Reducers/createClearReducer'; -import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; - -// -// Variables - -export const section = 'wanted'; - -// -// State - -export const defaultState = { - missing: { - isFetching: false, - isPopulated: false, - pageSize: 20, - sortKey: 'episodes.airDateUtc', - sortDirection: sortDirections.DESCENDING, - error: null, - items: [], - - columns: [ - { - name: 'series.sortTitle', - label: () => translate('SeriesTitle'), - isSortable: true, - isVisible: true - }, - { - name: 'episode', - label: () => translate('Episode'), - isVisible: true - }, - { - name: 'episodes.title', - label: () => translate('EpisodeTitle'), - isVisible: true - }, - { - name: 'episodes.airDateUtc', - label: () => translate('AirDate'), - isSortable: true, - isVisible: true - }, - { - name: 'episodes.lastSearchTime', - label: () => translate('LastSearched'), - isSortable: true, - isVisible: false - }, - { - name: 'status', - label: () => translate('Status'), - isVisible: true - }, - { - name: 'actions', - columnLabel: () => translate('Actions'), - isVisible: true, - isModifiable: false - } - ], - - selectedFilterKey: 'monitored', - - filters: [ - { - key: 'monitored', - label: () => translate('Monitored'), - filters: [ - { - key: 'monitored', - value: true, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'unmonitored', - label: () => translate('Unmonitored'), - filters: [ - { - key: 'monitored', - value: false, - type: filterTypes.EQUAL - } - ] - } - ] - }, - - cutoffUnmet: { - isFetching: false, - isPopulated: false, - pageSize: 20, - sortKey: 'episodes.airDateUtc', - sortDirection: sortDirections.DESCENDING, - items: [], - - columns: [ - { - name: 'series.sortTitle', - label: () => translate('SeriesTitle'), - isSortable: true, - isVisible: true - }, - { - name: 'episode', - label: () => translate('Episode'), - isVisible: true - }, - { - name: 'episodes.title', - label: () => translate('EpisodeTitle'), - isVisible: true - }, - { - name: 'episodes.airDateUtc', - label: () => translate('AirDate'), - isSortable: true, - isVisible: true - }, - { - name: 'episodes.lastSearchTime', - label: () => translate('LastSearched'), - isSortable: true, - isVisible: false - }, - { - name: 'languages', - label: () => translate('Languages'), - isVisible: false - }, - { - name: 'status', - label: () => translate('Status'), - isVisible: true - }, - { - name: 'actions', - columnLabel: () => translate('Actions'), - isVisible: true, - isModifiable: false - } - ], - - selectedFilterKey: 'monitored', - - filters: [ - { - key: 'monitored', - label: () => translate('Monitored'), - filters: [ - { - key: 'monitored', - value: true, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'unmonitored', - label: () => translate('Unmonitored'), - filters: [ - { - key: 'monitored', - value: false, - type: filterTypes.EQUAL - } - ] - } - ] - } -}; - -export const persistState = [ - 'wanted.missing.pageSize', - 'wanted.missing.sortKey', - 'wanted.missing.sortDirection', - 'wanted.missing.selectedFilterKey', - 'wanted.missing.columns', - 'wanted.cutoffUnmet.pageSize', - 'wanted.cutoffUnmet.sortKey', - 'wanted.cutoffUnmet.sortDirection', - 'wanted.cutoffUnmet.selectedFilterKey', - 'wanted.cutoffUnmet.columns' -]; - -// -// Actions Types - -export const FETCH_MISSING = 'wanted/missing/fetchMissing'; -export const GOTO_FIRST_MISSING_PAGE = 'wanted/missing/gotoMissingFirstPage'; -export const GOTO_PREVIOUS_MISSING_PAGE = 'wanted/missing/gotoMissingPreviousPage'; -export const GOTO_NEXT_MISSING_PAGE = 'wanted/missing/gotoMissingNextPage'; -export const GOTO_LAST_MISSING_PAGE = 'wanted/missing/gotoMissingLastPage'; -export const GOTO_MISSING_PAGE = 'wanted/missing/gotoMissingPage'; -export const SET_MISSING_SORT = 'wanted/missing/setMissingSort'; -export const SET_MISSING_FILTER = 'wanted/missing/setMissingFilter'; -export const SET_MISSING_TABLE_OPTION = 'wanted/missing/setMissingTableOption'; -export const CLEAR_MISSING = 'wanted/missing/clearMissing'; - -export const BATCH_TOGGLE_MISSING_EPISODES = 'wanted/missing/batchToggleMissingEpisodes'; - -export const FETCH_CUTOFF_UNMET = 'wanted/cutoffUnmet/fetchCutoffUnmet'; -export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFirstPage'; -export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPreviousPage'; -export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetNextPage'; -export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFastPage'; -export const GOTO_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPage'; -export const SET_CUTOFF_UNMET_SORT = 'wanted/cutoffUnmet/setCutoffUnmetSort'; -export const SET_CUTOFF_UNMET_FILTER = 'wanted/cutoffUnmet/setCutoffUnmetFilter'; -export const SET_CUTOFF_UNMET_TABLE_OPTION = 'wanted/cutoffUnmet/setCutoffUnmetTableOption'; -export const CLEAR_CUTOFF_UNMET = 'wanted/cutoffUnmet/clearCutoffUnmet'; - -export const BATCH_TOGGLE_CUTOFF_UNMET_EPISODES = 'wanted/cutoffUnmet/batchToggleCutoffUnmetEpisodes'; - -// -// Action Creators - -export const fetchMissing = createThunk(FETCH_MISSING); -export const gotoMissingFirstPage = createThunk(GOTO_FIRST_MISSING_PAGE); -export const gotoMissingPreviousPage = createThunk(GOTO_PREVIOUS_MISSING_PAGE); -export const gotoMissingNextPage = createThunk(GOTO_NEXT_MISSING_PAGE); -export const gotoMissingLastPage = createThunk(GOTO_LAST_MISSING_PAGE); -export const gotoMissingPage = createThunk(GOTO_MISSING_PAGE); -export const setMissingSort = createThunk(SET_MISSING_SORT); -export const setMissingFilter = createThunk(SET_MISSING_FILTER); -export const setMissingTableOption = createAction(SET_MISSING_TABLE_OPTION); -export const clearMissing = createAction(CLEAR_MISSING); - -export const batchToggleMissingEpisodes = createThunk(BATCH_TOGGLE_MISSING_EPISODES); - -export const fetchCutoffUnmet = createThunk(FETCH_CUTOFF_UNMET); -export const gotoCutoffUnmetFirstPage = createThunk(GOTO_FIRST_CUTOFF_UNMET_PAGE); -export const gotoCutoffUnmetPreviousPage = createThunk(GOTO_PREVIOUS_CUTOFF_UNMET_PAGE); -export const gotoCutoffUnmetNextPage = createThunk(GOTO_NEXT_CUTOFF_UNMET_PAGE); -export const gotoCutoffUnmetLastPage = createThunk(GOTO_LAST_CUTOFF_UNMET_PAGE); -export const gotoCutoffUnmetPage = createThunk(GOTO_CUTOFF_UNMET_PAGE); -export const setCutoffUnmetSort = createThunk(SET_CUTOFF_UNMET_SORT); -export const setCutoffUnmetFilter = createThunk(SET_CUTOFF_UNMET_FILTER); -export const setCutoffUnmetTableOption = createAction(SET_CUTOFF_UNMET_TABLE_OPTION); -export const clearCutoffUnmet = createAction(CLEAR_CUTOFF_UNMET); - -export const batchToggleCutoffUnmetEpisodes = createThunk(BATCH_TOGGLE_CUTOFF_UNMET_EPISODES); - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - - ...createServerSideCollectionHandlers( - 'wanted.missing', - '/wanted/missing', - fetchMissing, - { - [serverSideCollectionHandlers.FETCH]: FETCH_MISSING, - [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_MISSING_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_MISSING_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_MISSING_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_MISSING_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_MISSING_PAGE, - [serverSideCollectionHandlers.SORT]: SET_MISSING_SORT, - [serverSideCollectionHandlers.FILTER]: SET_MISSING_FILTER - } - ), - - [BATCH_TOGGLE_MISSING_EPISODES]: createBatchToggleEpisodeMonitoredHandler('wanted.missing', fetchMissing), - - ...createServerSideCollectionHandlers( - 'wanted.cutoffUnmet', - '/wanted/cutoff', - fetchCutoffUnmet, - { - [serverSideCollectionHandlers.FETCH]: FETCH_CUTOFF_UNMET, - [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_CUTOFF_UNMET_PAGE, - [serverSideCollectionHandlers.SORT]: SET_CUTOFF_UNMET_SORT, - [serverSideCollectionHandlers.FILTER]: SET_CUTOFF_UNMET_FILTER - } - ), - - [BATCH_TOGGLE_CUTOFF_UNMET_EPISODES]: createBatchToggleEpisodeMonitoredHandler('wanted.cutoffUnmet', fetchCutoffUnmet) - -}); - -// -// Reducers - -export const reducers = createHandleActions({ - - [SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('wanted.missing'), - [SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('wanted.cutoffUnmet'), - - [CLEAR_MISSING]: createClearReducer( - 'wanted.missing', - { - isFetching: false, - isPopulated: false, - error: null, - items: [], - totalPages: 0, - totalRecords: 0 - } - ), - - [CLEAR_CUTOFF_UNMET]: createClearReducer( - 'wanted.cutoffUnmet', - { - isFetching: false, - isPopulated: false, - error: null, - items: [], - totalPages: 0, - totalRecords: 0 - } - ) - -}, defaultState, section); diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx index 2444ecbc5..76a2a2ca2 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx @@ -2,7 +2,7 @@ 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 AppState, { Filter } from 'App/State/AppState'; +import { Filter } from 'App/State/AppState'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -18,21 +18,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 usePaging from 'Components/Table/usePaging'; import Episode from 'Episode/Episode'; -import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; +import { useToggleEpisodesMonitored } from 'Episode/useEpisode'; import { align, icons, kinds } from 'Helpers/Props'; import { executeCommand } from 'Store/Actions/commandActions'; import { fetchEpisodeFiles } from 'Store/Actions/episodeFileActions'; -import { - batchToggleCutoffUnmetEpisodes, - clearCutoffUnmet, - fetchCutoffUnmet, - gotoCutoffUnmetPage, - setCutoffUnmetFilter, - setCutoffUnmetSort, - setCutoffUnmetTableOption, -} from 'Store/Actions/wantedActions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import { CheckInputChanged } from 'typings/inputs'; import { TableOptionsChangePayload } from 'typings/Table'; @@ -43,34 +33,38 @@ import { unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; +import { + setCutoffUnmetOption, + setCutoffUnmetOptions, + useCutoffUnmetOptions, +} from './cutoffUnmetOptionsStore'; import CutoffUnmetRow from './CutoffUnmetRow'; +import useCutoffUnmet, { FILTERS } from './useCutoffUnmet'; function getMonitoredValue( filters: Filter[], - selectedFilterKey: string + selectedFilterKey: string | number ): boolean { return !!getFilterValue(filters, selectedFilterKey, 'monitored', false); } function CutoffUnmetContent() { const dispatch = useDispatch(); - const requestCurrentPage = useCurrentPage(); const { - isFetching, - isPopulated, - error, - items, - columns, - selectedFilterKey, - filters, - sortKey, - sortDirection, - page, - pageSize, + records, totalPages, - totalRecords = 0, - } = useSelector((state: AppState) => state.wanted.cutoffUnmet); + totalRecords, + error, + isFetching, + isLoading, + page, + goToPage, + refetch, + } = useCutoffUnmet(); + + const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } = + useCutoffUnmetOptions(); const isSearchingForAllEpisodes = useSelector( createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH) @@ -91,33 +85,21 @@ function CutoffUnmetContent() { const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] = useState(false); - const { - handleFirstPagePress, - handlePreviousPagePress, - handleNextPagePress, - handleLastPagePress, - handlePageSelect, - } = usePaging({ - page, - totalPages, - gotoPage: gotoCutoffUnmetPage, - }); + const { toggleEpisodesMonitored, isToggling } = useToggleEpisodesMonitored([ + '/wanted/cutoff', + ]); - const isSaving = useMemo(() => { - return items.filter((m) => m.isSaving).length > 1; - }, [items]); - - const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey); + const isShowingMonitored = getMonitoredValue(FILTERS, selectedFilterKey); const isSearchingForEpisodes = isSearchingForAllEpisodes || isSearchingForSelectedEpisodes; const episodeIds = useMemo(() => { - return selectUniqueIds(items, 'id'); - }, [items]); + return selectUniqueIds(records, 'id'); + }, [records]); const episodeFileIds = useMemo(() => { - return selectUniqueIds(items, 'episodeFileId'); - }, [items]); + return selectUniqueIds(records, 'episodeFileId'); + }, [records]); const handleSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { @@ -136,11 +118,11 @@ function CutoffUnmetContent() { name: commandNames.EPISODE_SEARCH, episodeIds: getSelectedIds(), commandFinished: () => { - dispatch(fetchCutoffUnmet()); + refetch(); }, }) ); - }, [getSelectedIds, dispatch]); + }, [getSelectedIds, dispatch, refetch]); const handleSearchAllPress = useCallback(() => { setIsConfirmSearchAllModalOpen(true); @@ -155,63 +137,43 @@ function CutoffUnmetContent() { executeCommand({ name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH, commandFinished: () => { - dispatch(fetchCutoffUnmet()); + refetch(); }, }) ); setIsConfirmSearchAllModalOpen(false); - }, [dispatch]); + }, [dispatch, refetch]); const handleToggleSelectedPress = useCallback(() => { - dispatch( - batchToggleCutoffUnmetEpisodes({ - episodeIds: getSelectedIds(), - monitored: !isShowingMonitored, - }) - ); - }, [isShowingMonitored, getSelectedIds, dispatch]); + toggleEpisodesMonitored({ + episodeIds: getSelectedIds(), + monitored: !isShowingMonitored, + }); + }, [isShowingMonitored, getSelectedIds, toggleEpisodesMonitored]); - const handleFilterSelect = useCallback( - (filterKey: number | string) => { - dispatch(setCutoffUnmetFilter({ selectedFilterKey: filterKey })); - }, - [dispatch] - ); + const handleFilterSelect = useCallback((filterKey: number | string) => { + setCutoffUnmetOption('selectedFilterKey', filterKey); + }, []); - const handleSortPress = useCallback( - (sortKey: string) => { - dispatch(setCutoffUnmetSort({ sortKey })); - }, - [dispatch] - ); + const handleSortPress = useCallback((sortKey: string) => { + setCutoffUnmetOption('sortKey', sortKey); + }, []); const handleTableOptionChange = useCallback( (payload: TableOptionsChangePayload) => { - dispatch(setCutoffUnmetTableOption(payload)); + setCutoffUnmetOptions(payload); if (payload.pageSize) { - dispatch(gotoCutoffUnmetPage({ page: 1 })); + goToPage(1); } }, - [dispatch] + [goToPage] ); - useEffect(() => { - if (requestCurrentPage) { - dispatch(fetchCutoffUnmet()); - } else { - dispatch(gotoCutoffUnmetPage({ page: 1 })); - } - - return () => { - dispatch(clearCutoffUnmet()); - }; - }, [requestCurrentPage, dispatch]); - useEffect(() => { const repopulate = () => { - dispatch(fetchCutoffUnmet()); + refetch(); }; registerPagePopulator(repopulate, [ @@ -223,7 +185,7 @@ function CutoffUnmetContent() { return () => { unregisterPagePopulator(repopulate); }; - }, [dispatch]); + }, [refetch]); useEffect(() => { if (episodeFileIds.length) { @@ -260,7 +222,7 @@ function CutoffUnmetContent() { } iconName={icons.MONITORED} isDisabled={!anySelected} - isSpinning={isSaving} + isSpinning={isToggling} onPress={handleToggleSelectedPress} /> @@ -280,7 +242,7 @@ function CutoffUnmetContent() { @@ -288,7 +250,7 @@ function CutoffUnmetContent() { - {isFetching && !isPopulated ? : null} + {isFetching && isLoading ? : null} {!isFetching && error ? ( @@ -296,11 +258,11 @@ function CutoffUnmetContent() { ) : null} - {isPopulated && !error && !items.length ? ( + {!isLoading && !error && !records.length ? ( {translate('CutoffUnmetNoItems')} ) : null} - {isPopulated && !error && !!items.length ? ( + {!isLoading && !error && !!records.length ? (
- {items.map((item) => { + {records.map((item) => { return ( state.wanted.cutoffUnmet); + const { records } = useCutoffUnmet(); return ( - items={items}> + items={records}> ); diff --git a/frontend/src/Wanted/CutoffUnmet/cutoffUnmetOptionsStore.ts b/frontend/src/Wanted/CutoffUnmet/cutoffUnmetOptionsStore.ts new file mode 100644 index 000000000..79c725d19 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/cutoffUnmetOptionsStore.ts @@ -0,0 +1,67 @@ +import { + createOptionsStore, + PageableOptions, +} from 'Helpers/Hooks/useOptionsStore'; +import translate from 'Utilities/String/translate'; + +const { useOptions, useOption, setOptions, setOption } = + createOptionsStore('cutoffUnmet_options', () => { + return { + pageSize: 20, + selectedFilterKey: 'monitored', + sortKey: 'episodes.airDateUtc', + sortDirection: 'descending', + columns: [ + { + name: 'series.sortTitle', + label: () => translate('SeriesTitle'), + isSortable: true, + isVisible: true, + }, + { + name: 'episode', + label: () => translate('Episode'), + isVisible: true, + }, + { + name: 'episodes.title', + label: () => translate('EpisodeTitle'), + isVisible: true, + }, + { + name: 'episodes.airDateUtc', + label: () => translate('AirDate'), + isSortable: true, + isVisible: true, + }, + { + name: 'episodes.lastSearchTime', + label: () => translate('LastSearched'), + isSortable: true, + isVisible: false, + }, + { + name: 'languages', + label: () => translate('Languages'), + isVisible: false, + }, + { + name: 'status', + label: () => translate('Status'), + isVisible: true, + }, + { + name: 'actions', + label: '', + columnLabel: () => translate('Actions'), + isVisible: true, + isModifiable: false, + }, + ], + }; + }); + +export const useCutoffUnmetOptions = useOptions; +export const setCutoffUnmetOptions = setOptions; +export const useCutoffUnmetOption = useOption; +export const setCutoffUnmetOption = setOption; diff --git a/frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx b/frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx new file mode 100644 index 000000000..5e4c8ffbc --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx @@ -0,0 +1,69 @@ +import { keepPreviousData } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { Filter } from 'App/State/AppState'; +import Episode from 'Episode/Episode'; +import { setEpisodeQueryKey } from 'Episode/useEpisode'; +import usePage from 'Helpers/Hooks/usePage'; +import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery'; +import translate from 'Utilities/String/translate'; +import { useCutoffUnmetOptions } from './cutoffUnmetOptionsStore'; + +export const FILTERS: Filter[] = [ + { + key: 'monitored', + label: () => translate('Monitored'), + filters: [ + { + key: 'monitored', + value: [true], + type: 'equal', + }, + ], + }, + { + key: 'unmonitored', + label: () => translate('Unmonitored'), + filters: [ + { + key: 'monitored', + value: [false], + type: 'equal', + }, + ], + }, +]; + +const useCutoffUnmet = () => { + const { page, goToPage } = usePage('cutoffUnmet'); + const { pageSize, selectedFilterKey, sortKey, sortDirection } = + useCutoffUnmetOptions(); + + const { isPlaceholderData, queryKey, ...query } = usePagedApiQuery({ + path: '/wanted/cutoff', + page, + pageSize, + queryParams: { + monitored: selectedFilterKey === 'monitored', + }, + sortKey, + sortDirection, + queryOptions: { + placeholderData: keepPreviousData, + }, + }); + + useEffect(() => { + if (!isPlaceholderData) { + setEpisodeQueryKey('wanted.cutoffUnmet', queryKey); + } + }, [isPlaceholderData, queryKey]); + + return { + ...query, + goToPage, + isPlaceholderData, + page, + }; +}; + +export default useCutoffUnmet; diff --git a/frontend/src/Wanted/Missing/Missing.tsx b/frontend/src/Wanted/Missing/Missing.tsx index e97fe7273..ad39333e3 100644 --- a/frontend/src/Wanted/Missing/Missing.tsx +++ b/frontend/src/Wanted/Missing/Missing.tsx @@ -2,7 +2,7 @@ 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 AppState, { Filter } from 'App/State/AppState'; +import { Filter } from 'App/State/AppState'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -18,21 +18,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 usePaging from 'Components/Table/usePaging'; import Episode from 'Episode/Episode'; -import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; +import { useToggleEpisodesMonitored } from 'Episode/useEpisode'; import { align, icons, kinds } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import { executeCommand } from 'Store/Actions/commandActions'; -import { - batchToggleMissingEpisodes, - clearMissing, - fetchMissing, - gotoMissingPage, - setMissingFilter, - setMissingSort, - setMissingTableOption, -} from 'Store/Actions/wantedActions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import { CheckInputChanged } from 'typings/inputs'; import { TableOptionsChangePayload } from 'typings/Table'; @@ -43,34 +33,38 @@ import { unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; +import { + setMissingOption, + setMissingOptions, + useMissingOptions, +} from './missingOptionsStore'; import MissingRow from './MissingRow'; +import useMissing, { FILTERS } from './useMissing'; function getMonitoredValue( filters: Filter[], - selectedFilterKey: string + selectedFilterKey: string | number ): boolean { return !!getFilterValue(filters, selectedFilterKey, 'monitored', false); } function MissingContent() { const dispatch = useDispatch(); - const requestCurrentPage = useCurrentPage(); const { - isFetching, - isPopulated, - error, - items, - columns, - selectedFilterKey, - filters, - sortKey, - sortDirection, - page, - pageSize, + records, totalPages, - totalRecords = 0, - } = useSelector((state: AppState) => state.wanted.missing); + totalRecords, + error, + isFetching, + isLoading, + page, + goToPage, + refetch, + } = useMissing(); + + const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } = + useMissingOptions(); const isSearchingForAllEpisodes = useSelector( createCommandExecutingSelector(commandNames.MISSING_EPISODE_SEARCH) @@ -94,29 +88,17 @@ function MissingContent() { const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] = useState(false); - const { - handleFirstPagePress, - handlePreviousPagePress, - handleNextPagePress, - handleLastPagePress, - handlePageSelect, - } = usePaging({ - page, - totalPages, - gotoPage: gotoMissingPage, - }); + const { toggleEpisodesMonitored, isToggling } = useToggleEpisodesMonitored([ + '/wanted/missing', + ]); - const isSaving = useMemo(() => { - return items.filter((m) => m.isSaving).length > 1; - }, [items]); - - const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey); + const isShowingMonitored = getMonitoredValue(FILTERS, selectedFilterKey); const isSearchingForEpisodes = isSearchingForAllEpisodes || isSearchingForSelectedEpisodes; const episodeIds = useMemo(() => { - return selectUniqueIds(items, 'id'); - }, [items]); + return selectUniqueIds(records, 'id'); + }, [records]); const handleSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { @@ -135,11 +117,11 @@ function MissingContent() { name: commandNames.EPISODE_SEARCH, episodeIds: getSelectedIds(), commandFinished: () => { - dispatch(fetchMissing()); + refetch(); }, }) ); - }, [getSelectedIds, dispatch]); + }, [getSelectedIds, dispatch, refetch]); const handleSearchAllPress = useCallback(() => { setIsConfirmSearchAllModalOpen(true); @@ -154,22 +136,20 @@ function MissingContent() { executeCommand({ name: commandNames.MISSING_EPISODE_SEARCH, commandFinished: () => { - dispatch(fetchMissing()); + refetch(); }, }) ); setIsConfirmSearchAllModalOpen(false); - }, [dispatch]); + }, [dispatch, refetch]); const handleToggleSelectedPress = useCallback(() => { - dispatch( - batchToggleMissingEpisodes({ - episodeIds: getSelectedIds(), - monitored: !isShowingMonitored, - }) - ); - }, [isShowingMonitored, getSelectedIds, dispatch]); + toggleEpisodesMonitored({ + episodeIds: getSelectedIds(), + monitored: !isShowingMonitored, + }); + }, [isShowingMonitored, getSelectedIds, toggleEpisodesMonitored]); const handleInteractiveImportPress = useCallback(() => { setIsInteractiveImportModalOpen(true); @@ -179,46 +159,28 @@ function MissingContent() { setIsInteractiveImportModalOpen(false); }, []); - const handleFilterSelect = useCallback( - (filterKey: number | string) => { - dispatch(setMissingFilter({ selectedFilterKey: filterKey })); - }, - [dispatch] - ); + const handleFilterSelect = useCallback((filterKey: number | string) => { + setMissingOption('selectedFilterKey', filterKey); + }, []); - const handleSortPress = useCallback( - (sortKey: string) => { - dispatch(setMissingSort({ sortKey })); - }, - [dispatch] - ); + const handleSortPress = useCallback((sortKey: string) => { + setMissingOption('sortKey', sortKey); + }, []); const handleTableOptionChange = useCallback( (payload: TableOptionsChangePayload) => { - dispatch(setMissingTableOption(payload)); + setMissingOptions(payload); if (payload.pageSize) { - dispatch(gotoMissingPage({ page: 1 })); + goToPage(1); } }, - [dispatch] + [goToPage] ); - useEffect(() => { - if (requestCurrentPage) { - dispatch(fetchMissing()); - } else { - dispatch(gotoMissingPage({ page: 1 })); - } - - return () => { - dispatch(clearMissing()); - }; - }, [requestCurrentPage, dispatch]); - useEffect(() => { const repopulate = () => { - dispatch(fetchMissing()); + refetch(); }; registerPagePopulator(repopulate, [ @@ -230,7 +192,7 @@ function MissingContent() { return () => { unregisterPagePopulator(repopulate); }; - }, [dispatch]); + }, [refetch]); return ( @@ -261,7 +223,7 @@ function MissingContent() { } iconName={icons.MONITORED} isDisabled={!anySelected} - isSpinning={isSaving} + isSpinning={isToggling} onPress={handleToggleSelectedPress} /> @@ -289,7 +251,7 @@ function MissingContent() { @@ -297,17 +259,17 @@ function MissingContent() { - {isFetching && !isPopulated ? : null} + {isFetching && isLoading ? : null} {!isFetching && error ? ( {translate('MissingLoadError')} ) : null} - {isPopulated && !error && !items.length ? ( + {!isLoading && !error && !records.length ? ( {translate('MissingNoItems')} ) : null} - {isPopulated && !error && !!items.length ? ( + {!isLoading && !error && !!records.length ? (
- {items.map((item) => { + {records.map((item) => { return ( ); @@ -335,11 +297,7 @@ function MissingContent() { totalPages={totalPages} totalRecords={totalRecords} isFetching={isFetching} - onFirstPagePress={handleFirstPagePress} - onPreviousPagePress={handlePreviousPagePress} - onNextPagePress={handleNextPagePress} - onLastPagePress={handleLastPagePress} - onPageSelect={handlePageSelect} + onPageSelect={goToPage} /> state.wanted.missing); + const { records } = useMissing(); return ( - items={items}> + items={records}> ); diff --git a/frontend/src/Wanted/Missing/missingOptionsStore.ts b/frontend/src/Wanted/Missing/missingOptionsStore.ts new file mode 100644 index 000000000..e6dae041e --- /dev/null +++ b/frontend/src/Wanted/Missing/missingOptionsStore.ts @@ -0,0 +1,62 @@ +import { + createOptionsStore, + PageableOptions, +} from 'Helpers/Hooks/useOptionsStore'; +import translate from 'Utilities/String/translate'; + +const { useOptions, useOption, setOptions, setOption } = + createOptionsStore('missing_options', () => { + return { + pageSize: 20, + selectedFilterKey: 'monitored', + sortKey: 'episodes.airDateUtc', + sortDirection: 'descending', + columns: [ + { + name: 'series.sortTitle', + label: () => translate('SeriesTitle'), + isSortable: true, + isVisible: true, + }, + { + name: 'episode', + label: () => translate('Episode'), + isVisible: true, + }, + { + name: 'episodes.title', + label: () => translate('EpisodeTitle'), + isVisible: true, + }, + { + name: 'episodes.airDateUtc', + label: () => translate('AirDate'), + isSortable: true, + isVisible: true, + }, + { + name: 'episodes.lastSearchTime', + label: () => translate('LastSearched'), + isSortable: true, + isVisible: false, + }, + { + name: 'status', + label: () => translate('Status'), + isVisible: true, + }, + { + name: 'actions', + label: '', + columnLabel: () => translate('Actions'), + isVisible: true, + isModifiable: false, + }, + ], + }; + }); + +export const useMissingOptions = useOptions; +export const setMissingOptions = setOptions; +export const useMissingOption = useOption; +export const setMissingOption = setOption; diff --git a/frontend/src/Wanted/Missing/useMissing.tsx b/frontend/src/Wanted/Missing/useMissing.tsx new file mode 100644 index 000000000..4c4e4dbbc --- /dev/null +++ b/frontend/src/Wanted/Missing/useMissing.tsx @@ -0,0 +1,69 @@ +import { keepPreviousData } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { Filter } from 'App/State/AppState'; +import Episode from 'Episode/Episode'; +import { setEpisodeQueryKey } from 'Episode/useEpisode'; +import usePage from 'Helpers/Hooks/usePage'; +import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery'; +import translate from 'Utilities/String/translate'; +import { useMissingOptions } from './missingOptionsStore'; + +export const FILTERS: Filter[] = [ + { + key: 'monitored', + label: () => translate('Monitored'), + filters: [ + { + key: 'monitored', + value: [true], + type: 'equal', + }, + ], + }, + { + key: 'unmonitored', + label: () => translate('Unmonitored'), + filters: [ + { + key: 'monitored', + value: [false], + type: 'equal', + }, + ], + }, +]; + +const useMissing = () => { + const { page, goToPage } = usePage('missing'); + const { pageSize, selectedFilterKey, sortKey, sortDirection } = + useMissingOptions(); + + const { isPlaceholderData, queryKey, ...query } = usePagedApiQuery({ + path: '/wanted/missing', + page, + pageSize, + queryParams: { + monitored: selectedFilterKey === 'monitored', + }, + sortKey, + sortDirection, + queryOptions: { + placeholderData: keepPreviousData, + }, + }); + + useEffect(() => { + if (!isPlaceholderData) { + setEpisodeQueryKey('wanted.missing', queryKey); + } + }, [isPlaceholderData, queryKey]); + + return { + ...query, + goToPage, + isPlaceholderData, + page, + }; +}; + +export default useMissing;