mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Use react-query for wanted missing and cutoff unmet
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<WantedEpisode>,
|
||||
AppSectionFilterState<WantedEpisode>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {}
|
||||
|
||||
interface WantedMissingAppState
|
||||
extends AppSectionState<WantedEpisode>,
|
||||
AppSectionFilterState<WantedEpisode>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {}
|
||||
|
||||
interface WantedAppState {
|
||||
cutoffUnmet: WantedCutoffUnmetAppState;
|
||||
missing: WantedMissingAppState;
|
||||
}
|
||||
|
||||
export default WantedAppState;
|
||||
@@ -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<Episode>(
|
||||
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<Episode>(
|
||||
queryClient,
|
||||
['/wanted/missing'],
|
||||
body.resource as Episode
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -385,3 +387,37 @@ function SignalRListener() {
|
||||
}
|
||||
|
||||
export default SignalRListener;
|
||||
|
||||
const updatePagedItem = <T extends ModelBase>(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
queryKey: QueryKey,
|
||||
updatedItem: T
|
||||
) => {
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey },
|
||||
(oldData: PagedQueryResponse<T> | 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;
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<EpisodeQueryKeyStore>(() => ({
|
||||
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<CalendarItem[]>(queryKey)
|
||||
?.find((e) => e.id === episodeId)
|
||||
: undefined;
|
||||
} else if (
|
||||
episodeEntity === 'wanted.cutoffUnmet' ||
|
||||
episodeEntity === 'wanted.missing'
|
||||
) {
|
||||
return queryKey
|
||||
? queryClient
|
||||
.getQueryData<PagedQueryResponse<Episode>>(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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<PageStore>(() => ({
|
||||
blocklist: 1,
|
||||
cutoffUnmet: 1,
|
||||
events: 1,
|
||||
history: 1,
|
||||
missing: 1,
|
||||
queue: 1,
|
||||
}));
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ interface PagedQueryOptions<T> extends QueryOptions<PagedQueryResponse<T>> {
|
||||
filters?: PropertyFilter[];
|
||||
}
|
||||
|
||||
interface PagedQueryResponse<T> {
|
||||
export interface PagedQueryResponse<T> {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey: string;
|
||||
@@ -94,6 +94,7 @@ const usePagedApiQuery = <T>(options: PagedQueryOptions<T>) => {
|
||||
|
||||
return {
|
||||
...query,
|
||||
queryKey,
|
||||
records: data?.records ?? DEFAULT_RECORDS,
|
||||
totalRecords: data?.totalRecords ?? 0,
|
||||
totalPages: data?.totalPages ?? 0,
|
||||
|
||||
@@ -210,7 +210,7 @@ function SeriesDetailsSeason({
|
||||
dispatch(
|
||||
toggleEpisodesMonitored({
|
||||
episodeIds,
|
||||
value,
|
||||
monitored: value,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
@@ -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<Episode, number>(items, 'id');
|
||||
}, [items]);
|
||||
return selectUniqueIds<Episode, number>(records, 'id');
|
||||
}, [records]);
|
||||
|
||||
const episodeFileIds = useMemo(() => {
|
||||
return selectUniqueIds<Episode, number>(items, 'episodeFileId');
|
||||
}, [items]);
|
||||
return selectUniqueIds<Episode, number>(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}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
@@ -280,7 +242,7 @@ function CutoffUnmetContent() {
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
filters={FILTERS}
|
||||
customFilters={[]}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
@@ -288,7 +250,7 @@ function CutoffUnmetContent() {
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
{isFetching && isLoading ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
@@ -296,11 +258,11 @@ function CutoffUnmetContent() {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !items.length ? (
|
||||
{!isLoading && !error && !records.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('CutoffUnmetNoItems')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !!items.length ? (
|
||||
{!isLoading && !error && !!records.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
@@ -315,7 +277,7 @@ function CutoffUnmetContent() {
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
{records.map((item) => {
|
||||
return (
|
||||
<CutoffUnmetRow
|
||||
key={item.id}
|
||||
@@ -332,11 +294,7 @@ function CutoffUnmetContent() {
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
onPageSelect={goToPage}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
@@ -367,10 +325,10 @@ function CutoffUnmetContent() {
|
||||
}
|
||||
|
||||
export default function CutoffUnmet() {
|
||||
const { items } = useSelector((state: AppState) => state.wanted.cutoffUnmet);
|
||||
const { records } = useCutoffUnmet();
|
||||
|
||||
return (
|
||||
<SelectProvider<Episode> items={items}>
|
||||
<SelectProvider<Episode> items={records}>
|
||||
<CutoffUnmetContent />
|
||||
</SelectProvider>
|
||||
);
|
||||
|
||||
67
frontend/src/Wanted/CutoffUnmet/cutoffUnmetOptionsStore.ts
Normal file
67
frontend/src/Wanted/CutoffUnmet/cutoffUnmetOptionsStore.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
createOptionsStore,
|
||||
PageableOptions,
|
||||
} from 'Helpers/Hooks/useOptionsStore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const { useOptions, useOption, setOptions, setOption } =
|
||||
createOptionsStore<PageableOptions>('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;
|
||||
69
frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx
Normal file
69
frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx
Normal file
@@ -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<Episode>({
|
||||
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;
|
||||
@@ -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<Episode, number>(items, 'id');
|
||||
}, [items]);
|
||||
return selectUniqueIds<Episode, number>(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 (
|
||||
<QueueDetailsProvider episodeIds={episodeIds}>
|
||||
@@ -261,7 +223,7 @@ function MissingContent() {
|
||||
}
|
||||
iconName={icons.MONITORED}
|
||||
isDisabled={!anySelected}
|
||||
isSpinning={isSaving}
|
||||
isSpinning={isToggling}
|
||||
onPress={handleToggleSelectedPress}
|
||||
/>
|
||||
|
||||
@@ -289,7 +251,7 @@ function MissingContent() {
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
filters={FILTERS}
|
||||
customFilters={[]}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
@@ -297,17 +259,17 @@ function MissingContent() {
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
{isFetching && isLoading ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('MissingLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !items.length ? (
|
||||
{!isLoading && !error && !records.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('MissingNoItems')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error && !!items.length ? (
|
||||
{!isLoading && !error && !!records.length ? (
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
@@ -322,7 +284,7 @@ function MissingContent() {
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
{records.map((item) => {
|
||||
return (
|
||||
<MissingRow key={item.id} columns={columns} {...item} />
|
||||
);
|
||||
@@ -335,11 +297,7 @@ function MissingContent() {
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
onPageSelect={goToPage}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
@@ -377,10 +335,10 @@ function MissingContent() {
|
||||
}
|
||||
|
||||
function Missing() {
|
||||
const { items } = useSelector((state: AppState) => state.wanted.missing);
|
||||
const { records } = useMissing();
|
||||
|
||||
return (
|
||||
<SelectProvider<Episode> items={items}>
|
||||
<SelectProvider<Episode> items={records}>
|
||||
<MissingContent />
|
||||
</SelectProvider>
|
||||
);
|
||||
|
||||
62
frontend/src/Wanted/Missing/missingOptionsStore.ts
Normal file
62
frontend/src/Wanted/Missing/missingOptionsStore.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
createOptionsStore,
|
||||
PageableOptions,
|
||||
} from 'Helpers/Hooks/useOptionsStore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const { useOptions, useOption, setOptions, setOption } =
|
||||
createOptionsStore<PageableOptions>('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;
|
||||
69
frontend/src/Wanted/Missing/useMissing.tsx
Normal file
69
frontend/src/Wanted/Missing/useMissing.tsx
Normal file
@@ -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<Episode>({
|
||||
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;
|
||||
Reference in New Issue
Block a user