mirror of
https://github.com/Sonarr/Sonarr
synced 2026-04-29 00:21:23 +02:00
Use react-query for episode selection
This commit is contained in:
parent
d252fa8ed6
commit
b32f7e0c25
9 changed files with 130 additions and 158 deletions
|
|
@ -15,7 +15,7 @@ import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import useEpisodes from 'Episode/useEpisodes';
|
||||
import useEpisodesWithIds from 'Episode/useEpisodesWithIds';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import Language from 'Language/Language';
|
||||
|
|
@ -106,7 +106,7 @@ function QueueRow(props: QueueRowProps) {
|
|||
} = props;
|
||||
|
||||
const series = useSeries(seriesId);
|
||||
const episodes = useEpisodes(episodeIds);
|
||||
const episodes = useEpisodesWithIds(episodeIds);
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ interface AppState {
|
|||
commands: CommandAppState;
|
||||
episodeHistory: HistoryAppState;
|
||||
episodes: EpisodesAppState;
|
||||
episodesSelection: EpisodesAppState;
|
||||
importSeries: ImportSeriesAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
oAuth: OAuthAppState;
|
||||
|
|
|
|||
21
frontend/src/Episode/episodeSelectionOptionsStore.ts
Normal file
21
frontend/src/Episode/episodeSelectionOptionsStore.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
|
||||
interface EpisodeSelectOptions {
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
const { useOptions, useOption, setOptions, setOption, setSort } =
|
||||
createOptionsStore<EpisodeSelectOptions>('episode_selection_options', () => {
|
||||
return {
|
||||
sortKey: 'episodeNumber',
|
||||
sortDirection: 'ascending',
|
||||
};
|
||||
});
|
||||
|
||||
export const useEpisodeSelectionOptions = useOptions;
|
||||
export const setEpisodeSelectionOptions = setOptions;
|
||||
export const useEpisodeSelectionOption = useOption;
|
||||
export const setEpisodeSelectionOption = setOption;
|
||||
export const setEpisodeSelectionSort = setSort;
|
||||
|
|
@ -1,29 +1,47 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import Episode from './Episode';
|
||||
|
||||
function getEpisodes(episodeIds: number[], episodes: Episode[]) {
|
||||
return episodeIds.reduce<Episode[]>((acc, id) => {
|
||||
const episode = episodes.find((episode) => episode.id === id);
|
||||
const DEFAULT_EPISODES: Episode[] = [];
|
||||
|
||||
if (episode) {
|
||||
acc.push(episode);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
interface SeriesEpisodes {
|
||||
seriesId: number;
|
||||
}
|
||||
|
||||
function createEpisodeSelector(episodeIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes.items,
|
||||
(episodes) => {
|
||||
return getEpisodes(episodeIds, episodes);
|
||||
}
|
||||
);
|
||||
interface SeasonEpisodes {
|
||||
seriesId: number | undefined;
|
||||
seasonNumber: number | undefined;
|
||||
}
|
||||
|
||||
export default function useEpisodes(episodeIds: number[]) {
|
||||
return useSelector(createEpisodeSelector(episodeIds));
|
||||
interface EpisodeIds {
|
||||
episodeIds: number[];
|
||||
}
|
||||
|
||||
interface EpisodeFileId {
|
||||
episodeFileId: number;
|
||||
}
|
||||
|
||||
export type EpisodeFilter =
|
||||
| SeriesEpisodes
|
||||
| SeasonEpisodes
|
||||
| EpisodeIds
|
||||
| EpisodeFileId;
|
||||
|
||||
const useEpisodes = (params: EpisodeFilter) => {
|
||||
const result = useApiQuery<Episode[]>({
|
||||
path: '/episode',
|
||||
queryParams: { ...params },
|
||||
queryOptions: {
|
||||
enabled:
|
||||
('seriesId' in params && params.seriesId !== undefined) ||
|
||||
('episodeIds' in params && params.episodeIds?.length > 0) ||
|
||||
('episodeFileId' in params && params.episodeFileId !== undefined),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? DEFAULT_EPISODES,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEpisodes;
|
||||
|
|
|
|||
29
frontend/src/Episode/useEpisodesWithIds.ts
Normal file
29
frontend/src/Episode/useEpisodesWithIds.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Episode from './Episode';
|
||||
|
||||
function getEpisodes(episodeIds: number[], episodes: Episode[]) {
|
||||
return episodeIds.reduce<Episode[]>((acc, id) => {
|
||||
const episode = episodes.find((episode) => episode.id === id);
|
||||
|
||||
if (episode) {
|
||||
acc.push(episode);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function createEpisodeSelector(episodeIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodes.items,
|
||||
(episodes) => {
|
||||
return getEpisodes(episodeIds, episodes);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function useEpisodesWithIds(episodeIds: number[]) {
|
||||
return useSelector(createEpisodeSelector(episodeIds));
|
||||
}
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
|
||||
import EpisodesAppState from 'App/State/EpisodesAppState';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
|
@ -14,15 +11,15 @@ import Scroller from 'Components/Scroller/Scroller';
|
|||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import Episode from 'Episode/Episode';
|
||||
import {
|
||||
setEpisodeSelectionSort,
|
||||
useEpisodeSelectionOptions,
|
||||
} from 'Episode/episodeSelectionOptionsStore';
|
||||
import useEpisodes from 'Episode/useEpisodes';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import {
|
||||
clearEpisodes,
|
||||
fetchEpisodes,
|
||||
setEpisodesSort,
|
||||
} from 'Store/Actions/episodeSelectionActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { CheckInputChanged, InputChanged } from 'typings/inputs';
|
||||
import clientSideFilterAndSort from 'Utilities/Filter/clientSideFilterAndSort';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectEpisodeRow from './SelectEpisodeRow';
|
||||
|
|
@ -47,15 +44,6 @@ const columns = [
|
|||
},
|
||||
];
|
||||
|
||||
function episodesSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('episodeSelection'),
|
||||
(episodes: EpisodesAppState) => {
|
||||
return episodes;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectedEpisode {
|
||||
id: number;
|
||||
episodes: Episode[];
|
||||
|
|
@ -72,20 +60,7 @@ interface SelectEpisodeModalContentProps {
|
|||
onModalClose(): unknown;
|
||||
}
|
||||
|
||||
interface SelectEpisodeModalContentInnerProps {
|
||||
selectedIds: number[] | string[];
|
||||
seriesId?: number;
|
||||
seasonNumber?: number;
|
||||
selectedDetails?: string;
|
||||
isAnime: boolean;
|
||||
modalTitle: string;
|
||||
onEpisodesSelect(selectedEpisodes: SelectedEpisode[]): unknown;
|
||||
onModalClose(): unknown;
|
||||
}
|
||||
|
||||
function SelectEpisodeModalContentInner(
|
||||
props: SelectEpisodeModalContentInnerProps
|
||||
) {
|
||||
function SelectEpisodeModalContentInner(props: SelectEpisodeModalContentProps) {
|
||||
const {
|
||||
selectedIds,
|
||||
seriesId,
|
||||
|
|
@ -99,9 +74,13 @@ function SelectEpisodeModalContentInner(
|
|||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const { isFetching, isPopulated, items, error, sortKey, sortDirection } =
|
||||
useSelector(episodesSelector());
|
||||
const dispatch = useDispatch();
|
||||
const { isFetching, isFetched, data, error } = useEpisodes({
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
});
|
||||
|
||||
const { sortKey, sortDirection } = useEpisodeSelectionOptions();
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
|
|
@ -137,20 +116,18 @@ function SelectEpisodeModalContentInner(
|
|||
|
||||
const onSortPress = useCallback(
|
||||
(newSortKey: string, newSortDirection?: SortDirection) => {
|
||||
dispatch(
|
||||
setEpisodesSort({
|
||||
sortKey: newSortKey,
|
||||
sortDirection: newSortDirection,
|
||||
})
|
||||
);
|
||||
setEpisodeSelectionSort({
|
||||
sortKey: newSortKey,
|
||||
sortDirection: newSortDirection,
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
[]
|
||||
);
|
||||
|
||||
const onEpisodesSelectWrapper = useCallback(() => {
|
||||
const episodeIds: number[] = getSelectedIds();
|
||||
|
||||
const selectedEpisodes = items.reduce((acc: Episode[], item) => {
|
||||
const selectedEpisodes = data.reduce((acc: Episode[], item) => {
|
||||
if (episodeIds.indexOf(item.id) > -1) {
|
||||
acc.push(item);
|
||||
}
|
||||
|
|
@ -177,19 +154,7 @@ function SelectEpisodeModalContentInner(
|
|||
});
|
||||
|
||||
onEpisodesSelect(mappedEpisodes);
|
||||
}, [selectedIds, items, getSelectedIds, onEpisodesSelect]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
dispatch(fetchEpisodes({ seriesId, seasonNumber }));
|
||||
|
||||
return () => {
|
||||
dispatch(clearEpisodes());
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
}, [selectedIds, data, getSelectedIds, onEpisodesSelect]);
|
||||
|
||||
let details = selectedDetails;
|
||||
|
||||
|
|
@ -200,6 +165,13 @@ function SelectEpisodeModalContentInner(
|
|||
: translate('CountSelectedFile', { selectedCount });
|
||||
}
|
||||
|
||||
const { data: items } = useMemo(() => {
|
||||
return clientSideFilterAndSort<Episode>(data, {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
});
|
||||
}, [data, sortKey, sortDirection]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
|
@ -224,7 +196,7 @@ function SelectEpisodeModalContentInner(
|
|||
|
||||
{error ? <div>{errorMessage}</div> : null}
|
||||
|
||||
{isPopulated && !!items.length ? (
|
||||
{isFetched && !!items.length ? (
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
|
|
@ -254,7 +226,7 @@ function SelectEpisodeModalContentInner(
|
|||
</Table>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !items.length
|
||||
{isFetched && !data.length
|
||||
? translate('NoEpisodesFoundForSelectedSeason')
|
||||
: null}
|
||||
</Scroller>
|
||||
|
|
@ -280,10 +252,13 @@ function SelectEpisodeModalContentInner(
|
|||
}
|
||||
|
||||
function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
||||
const { items } = useSelector(episodesSelector());
|
||||
const { data } = useEpisodes({
|
||||
seriesId: props.seriesId,
|
||||
seasonNumber: props.seasonNumber,
|
||||
});
|
||||
|
||||
return (
|
||||
<SelectProvider items={items}>
|
||||
<SelectProvider items={data}>
|
||||
<SelectEpisodeModalContentInner {...props} />
|
||||
</SelectProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'episodeSelection';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isReprocessing: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
sortKey: 'episodeNumber',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: []
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'episodeSelection.sortKey',
|
||||
'episodeSelection.sortDirection'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_EPISODES = 'episodeSelection/fetchEpisodes';
|
||||
export const SET_EPISODES_SORT = 'episodeSelection/setEpisodesSort';
|
||||
export const CLEAR_EPISODES = 'episodeSelection/clearEpisodes';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchEpisodes = createThunk(FETCH_EPISODES);
|
||||
export const setEpisodesSort = createAction(SET_EPISODES_SORT);
|
||||
export const clearEpisodes = createAction(CLEAR_EPISODES);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_EPISODES]: createFetchHandler(section, '/episode')
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_EPISODES_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||
|
||||
[CLEAR_EPISODES]: (state) => {
|
||||
return updateSectionState(state, section, {
|
||||
...defaultState,
|
||||
sortKey: state.sortKey,
|
||||
sortDirection: state.sortDirection
|
||||
});
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
@ -3,7 +3,6 @@ import * as captcha from './captchaActions';
|
|||
import * as commands from './commandActions';
|
||||
import * as episodes from './episodeActions';
|
||||
import * as episodeHistory from './episodeHistoryActions';
|
||||
import * as episodeSelection from './episodeSelectionActions';
|
||||
import * as importSeries from './importSeriesActions';
|
||||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
import * as oAuth from './oAuthActions';
|
||||
|
|
@ -20,7 +19,6 @@ export default [
|
|||
commands,
|
||||
episodes,
|
||||
episodeHistory,
|
||||
episodeSelection,
|
||||
importSeries,
|
||||
interactiveImportActions,
|
||||
oAuth,
|
||||
|
|
|
|||
|
|
@ -128,14 +128,14 @@ const sort = <T extends ModelBase, TFilter = null, TSort = null>(
|
|||
};
|
||||
|
||||
interface ClientSideFilterAndSortOptions<T extends ModelBase, TFilter, TSort> {
|
||||
selectedFilterKey: string | number;
|
||||
filters: Filter[];
|
||||
selectedFilterKey?: string | number;
|
||||
filters?: Filter[];
|
||||
filterPredicates?: Record<
|
||||
keyof TFilter,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(item: T, value: any, type: FilterType) => boolean
|
||||
>;
|
||||
customFilters: CustomFilter[];
|
||||
customFilters?: CustomFilter[];
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
secondarySortKey?: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue