mirror of
https://github.com/Sonarr/Sonarr
synced 2025-10-05 15:42:51 +02:00
Use react-query for History UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
@@ -9,6 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { useMarkAsFailed } from '../useHistory';
|
||||
import HistoryDetails from './HistoryDetails';
|
||||
import styles from './HistoryDetailsModal.css';
|
||||
|
||||
@@ -33,26 +34,32 @@ function getHeaderTitle(eventType: HistoryEventType) {
|
||||
|
||||
interface HistoryDetailsModalProps {
|
||||
isOpen: boolean;
|
||||
id: number;
|
||||
eventType: HistoryEventType;
|
||||
sourceTitle: string;
|
||||
data: HistoryData;
|
||||
downloadId?: string;
|
||||
isMarkingAsFailed: boolean;
|
||||
onMarkAsFailedPress: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed = false,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
const { isOpen, id, eventType, sourceTitle, data, downloadId, onModalClose } =
|
||||
props;
|
||||
|
||||
const { markAsFailed, isMarkingAsFailed, markAsFailedError } =
|
||||
useMarkAsFailed(id);
|
||||
|
||||
const wasMarkingAsFailed = useRef(isMarkingAsFailed);
|
||||
|
||||
const handleMarkAsFailedPress = useCallback(() => {
|
||||
markAsFailed();
|
||||
}, [markAsFailed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [wasMarkingAsFailed, isMarkingAsFailed, markAsFailedError, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
@@ -74,7 +81,7 @@ function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||
className={styles.markAsFailedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isMarkingAsFailed}
|
||||
onPress={onMarkAsFailedPress}
|
||||
onPress={handleMarkAsFailedPress}
|
||||
>
|
||||
{translate('MarkAsFailed')}
|
||||
</SpinnerButton>
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import {
|
||||
setQueueOption,
|
||||
setQueueOptions,
|
||||
} from 'Activity/Queue/queueOptionsStore';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
@@ -13,20 +16,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 createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import {
|
||||
clearHistory,
|
||||
fetchHistory,
|
||||
gotoHistoryPage,
|
||||
setHistoryFilter,
|
||||
setHistorySort,
|
||||
setHistoryTableOption,
|
||||
} from 'Store/Actions/historyActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import HistoryItem from 'typings/History';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
@@ -37,100 +31,90 @@ import {
|
||||
} from 'Utilities/pagePopulator';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryFilterModal from './HistoryFilterModal';
|
||||
import { useHistoryOptions } from './historyOptionsStore';
|
||||
import HistoryRow from './HistoryRow';
|
||||
import useHistory, { useFilters } from './useHistory';
|
||||
|
||||
function History() {
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
page,
|
||||
pageSize,
|
||||
records,
|
||||
totalPages,
|
||||
totalRecords,
|
||||
} = useSelector((state: AppState) => state.history);
|
||||
error,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isLoading,
|
||||
page,
|
||||
goToPage,
|
||||
refetch,
|
||||
} = useHistory();
|
||||
|
||||
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
||||
useHistoryOptions();
|
||||
|
||||
const filters = useFilters();
|
||||
|
||||
const requestCurrentPage = useCurrentPage();
|
||||
|
||||
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
||||
useSelector(createEpisodesFetchingSelector());
|
||||
const customFilters = useSelector(createCustomFiltersSelector('history'));
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isFetchingAny = isFetching || isEpisodesFetching;
|
||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
||||
const isFetchingAny = isLoading || isEpisodesFetching;
|
||||
const isAllPopulated = isFetched && (isEpisodesPopulated || !records.length);
|
||||
const hasError = error || episodesError;
|
||||
|
||||
const {
|
||||
handleFirstPagePress,
|
||||
handlePreviousPagePress,
|
||||
handleNextPagePress,
|
||||
handleLastPagePress,
|
||||
handlePageSelect,
|
||||
} = usePaging({
|
||||
page,
|
||||
totalPages,
|
||||
gotoPage: gotoHistoryPage,
|
||||
});
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string | number) => {
|
||||
dispatch(setHistoryFilter({ selectedFilterKey }));
|
||||
setQueueOption('selectedFilterKey', selectedFilterKey);
|
||||
},
|
||||
[dispatch]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string) => {
|
||||
dispatch(setHistorySort({ sortKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
const handleSortPress = useCallback((sortKey: string) => {
|
||||
setQueueOption('sortKey', sortKey);
|
||||
}, []);
|
||||
|
||||
const handleTableOptionChange = useCallback(
|
||||
(payload: TableOptionsChangePayload) => {
|
||||
dispatch(setHistoryTableOption(payload));
|
||||
setQueueOptions(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
dispatch(gotoHistoryPage({ page: 1 }));
|
||||
goToPage(1);
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
[goToPage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (requestCurrentPage) {
|
||||
dispatch(fetchHistory());
|
||||
} else {
|
||||
dispatch(gotoHistoryPage({ page: 1 }));
|
||||
}
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
goToPage(1);
|
||||
refetch();
|
||||
}, [goToPage, refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(clearHistory());
|
||||
dispatch(clearEpisodes());
|
||||
dispatch(clearEpisodeFiles());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const episodeIds = selectUniqueIds<HistoryItem, number>(items, 'episodeId');
|
||||
const episodeIds = selectUniqueIds<HistoryItem, number>(
|
||||
records,
|
||||
'episodeId'
|
||||
);
|
||||
|
||||
if (episodeIds.length) {
|
||||
dispatch(fetchEpisodes({ episodeIds }));
|
||||
} else {
|
||||
dispatch(clearEpisodes());
|
||||
}
|
||||
}, [items, dispatch]);
|
||||
}, [records, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repopulate = () => {
|
||||
dispatch(fetchHistory());
|
||||
refetch();
|
||||
};
|
||||
|
||||
registerPagePopulator(repopulate);
|
||||
@@ -138,7 +122,7 @@ function History() {
|
||||
return () => {
|
||||
unregisterPagePopulator(repopulate);
|
||||
};
|
||||
}, [dispatch]);
|
||||
}, [refetch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('History')}>
|
||||
@@ -148,7 +132,7 @@ function History() {
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={handleFirstPagePress}
|
||||
onPress={handleRefreshPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
@@ -186,12 +170,12 @@ function History() {
|
||||
// If history isPopulated and it's empty show no history found and don't
|
||||
// wait for the episodes to populate because they are never coming.
|
||||
|
||||
isPopulated && !hasError && !items.length ? (
|
||||
isFetched && !hasError && !records.length ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert>
|
||||
) : null
|
||||
}
|
||||
|
||||
{isAllPopulated && !hasError && items.length ? (
|
||||
{isAllPopulated && !hasError && records.length ? (
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
@@ -202,7 +186,7 @@ function History() {
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
{records.map((item) => {
|
||||
return (
|
||||
<HistoryRow key={item.id} columns={columns} {...item} />
|
||||
);
|
||||
@@ -215,11 +199,7 @@ function History() {
|
||||
totalPages={totalPages}
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={handleFirstPagePress}
|
||||
onPreviousPagePress={handlePreviousPagePress}
|
||||
onNextPagePress={handleNextPagePress}
|
||||
onLastPagePress={handleLastPagePress}
|
||||
onPageSelect={handlePageSelect}
|
||||
onPageSelect={goToPage}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
@@ -1,48 +1,25 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
|
||||
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
import { setHistoryOption } from './historyOptionsStore';
|
||||
import useHistory, { FILTER_BUILDER } from './useHistory';
|
||||
|
||||
type HistoryFilterModalProps = FilterModalProps<History>;
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { records } = useHistory();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: { selectedFilterKey: string | number }) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
({ selectedFilterKey }: { selectedFilterKey: string | number }) => {
|
||||
setHistoryOption('selectedFilterKey', selectedFilterKey);
|
||||
},
|
||||
[dispatch]
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
sectionItems={records}
|
||||
filterBuilderProps={FILTER_BUILDER}
|
||||
customFilterType="history"
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
@@ -13,13 +12,11 @@ import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||
import useEpisode from 'Episode/useEpisode';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
@@ -61,13 +58,9 @@ function HistoryRow(props: HistoryRowProps) {
|
||||
date,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed = false,
|
||||
markAsFailedError,
|
||||
columns,
|
||||
} = props;
|
||||
|
||||
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
|
||||
const dispatch = useDispatch();
|
||||
const series = useSeries(seriesId);
|
||||
const episode = useEpisode(episodeId, 'episodes');
|
||||
|
||||
@@ -81,23 +74,6 @@ function HistoryRow(props: HistoryRowProps) {
|
||||
setIsDetailsModalOpen(false);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
const handleMarkAsFailedPress = useCallback(() => {
|
||||
dispatch(markAsFailed({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) {
|
||||
setIsDetailsModalOpen(false);
|
||||
dispatch(fetchHistory());
|
||||
}
|
||||
}, [
|
||||
wasMarkingAsFailed,
|
||||
isMarkingAsFailed,
|
||||
markAsFailedError,
|
||||
setIsDetailsModalOpen,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
if (!series || !episode) {
|
||||
return null;
|
||||
}
|
||||
@@ -254,13 +230,12 @@ function HistoryRow(props: HistoryRowProps) {
|
||||
})}
|
||||
|
||||
<HistoryDetailsModal
|
||||
id={id}
|
||||
isOpen={isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||
onModalClose={handleDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
|
109
frontend/src/Activity/History/historyOptionsStore.ts
Normal file
109
frontend/src/Activity/History/historyOptionsStore.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import {
|
||||
createOptionsStore,
|
||||
PageableOptions,
|
||||
} from 'Helpers/Hooks/useOptionsStore';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export type HistoryOptions = PageableOptions;
|
||||
|
||||
const { useOptions, useOption, setOptions, setOption } =
|
||||
createOptionsStore<HistoryOptions>('history_options', () => {
|
||||
return {
|
||||
includeUnknownSeriesItems: true,
|
||||
pageSize: 20,
|
||||
selectedFilterKey: 'all',
|
||||
sortKey: 'time',
|
||||
sortDirection: 'descending',
|
||||
columns: [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: '',
|
||||
columnLabel: () => translate('EventType'),
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
{
|
||||
name: 'series.sortTitle',
|
||||
label: () => translate('Series'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'episode',
|
||||
label: () => translate('Episode'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'episodes.title',
|
||||
label: () => translate('EpisodeTitle'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('Formats'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'downloadClient',
|
||||
label: () => translate('DownloadClient'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: () => translate('ReleaseGroup'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: () => translate('CustomFormatScore'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isVisible: false,
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
label: '',
|
||||
columnLabel: () => translate('Details'),
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export const useHistoryOptions = useOptions;
|
||||
export const setHistoryOptions = setOptions;
|
||||
export const useHistoryOption = useOption;
|
||||
export const setHistoryOption = setOption;
|
186
frontend/src/Activity/History/useHistory.ts
Normal file
186
frontend/src/Activity/History/useHistory.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CustomFilter, Filter, FilterBuilderProp } from 'App/State/AppState';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import usePage from 'Helpers/Hooks/usePage';
|
||||
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
|
||||
import { filterBuilderValueTypes } from 'Helpers/Props';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import History from 'typings/History';
|
||||
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { useHistoryOptions } from './historyOptionsStore';
|
||||
|
||||
export const FILTERS: Filter[] = [
|
||||
{
|
||||
key: 'all',
|
||||
label: () => translate('All'),
|
||||
filters: [],
|
||||
},
|
||||
{
|
||||
key: 'grabbed',
|
||||
label: () => translate('Grabbed'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '1',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'imported',
|
||||
label: () => translate('Imported'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '3',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
label: () => translate('Failed'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '4',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'deleted',
|
||||
label: () => translate('Deleted'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '5',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'renamed',
|
||||
label: () => translate('Renamed'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '6',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'ignored',
|
||||
label: () => translate('Ignored'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '7',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const FILTER_BUILDER: FilterBuilderProp<History>[] = [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: () => translate('EventType'),
|
||||
type: 'equal',
|
||||
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE,
|
||||
},
|
||||
{
|
||||
name: 'seriesIds',
|
||||
label: () => translate('Series'),
|
||||
type: 'equal',
|
||||
valueType: filterBuilderValueTypes.SERIES,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
type: 'equal',
|
||||
valueType: filterBuilderValueTypes.QUALITY,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
type: 'contains',
|
||||
valueType: filterBuilderValueTypes.LANGUAGE,
|
||||
},
|
||||
];
|
||||
|
||||
const useHistory = () => {
|
||||
const { page, goToPage } = usePage('history');
|
||||
const { pageSize, selectedFilterKey, sortKey, sortDirection } =
|
||||
useHistoryOptions();
|
||||
const customFilters = useSelector(
|
||||
createCustomFiltersSelector('history')
|
||||
) as CustomFilter[];
|
||||
|
||||
const filters = useMemo(() => {
|
||||
return findSelectedFilters(selectedFilterKey, FILTERS, customFilters);
|
||||
}, [selectedFilterKey, customFilters]);
|
||||
|
||||
const { refetch, ...query } = usePagedApiQuery<History>({
|
||||
path: '/history',
|
||||
page,
|
||||
pageSize,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
queryOptions: {
|
||||
placeholderData: keepPreviousData,
|
||||
},
|
||||
});
|
||||
|
||||
const handleGoToPage = useCallback(
|
||||
(page: number) => {
|
||||
goToPage(page);
|
||||
},
|
||||
[goToPage]
|
||||
);
|
||||
|
||||
return {
|
||||
...query,
|
||||
goToPage: handleGoToPage,
|
||||
page,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHistory;
|
||||
|
||||
export const useFilters = () => {
|
||||
return FILTERS;
|
||||
};
|
||||
|
||||
export const useMarkAsFailed = (id: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { mutate, isPending } = useApiMutation<unknown, void>({
|
||||
path: `/history/failed/${id}`,
|
||||
method: 'POST',
|
||||
mutationOptions: {
|
||||
onMutate: () => {
|
||||
setError(null);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/history'] });
|
||||
},
|
||||
onError: () => {
|
||||
setError('Error marking history item as failed');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
markAsFailed: mutate,
|
||||
isMarkingAsFailed: isPending,
|
||||
markAsFailedError: error,
|
||||
};
|
||||
};
|
@@ -90,7 +90,6 @@ interface AppState {
|
||||
episodeHistory: HistoryAppState;
|
||||
episodes: EpisodesAppState;
|
||||
episodesSelection: EpisodesAppState;
|
||||
history: HistoryAppState;
|
||||
importSeries: ImportSeriesAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
oAuth: OAuthAppState;
|
||||
|
@@ -5,12 +5,14 @@ import { create } from 'zustand';
|
||||
interface PageStore {
|
||||
blocklist: number;
|
||||
events: number;
|
||||
history: number;
|
||||
queue: number;
|
||||
}
|
||||
|
||||
const pageStore = create<PageStore>(() => ({
|
||||
blocklist: 1,
|
||||
events: 1,
|
||||
history: 1,
|
||||
queue: 1,
|
||||
}));
|
||||
|
||||
|
@@ -1,327 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createAction } from 'redux-actions';
|
||||
import Icon from 'Components/Icon';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { updateItem } from './baseActions';
|
||||
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 = 'history';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
pageSize: 20,
|
||||
sortKey: 'date',
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
items: [],
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'eventType',
|
||||
columnLabel: () => translate('EventType'),
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'series.sortTitle',
|
||||
label: () => translate('Series'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'episode',
|
||||
label: () => translate('Episode'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'episodes.title',
|
||||
label: () => translate('EpisodeTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('Formats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'downloadClient',
|
||||
label: () => translate('DownloadClient'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: () => translate('ReleaseGroup'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: () => translate('CustomFormatScore'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
columnLabel: () => translate('Details'),
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
}
|
||||
],
|
||||
|
||||
selectedFilterKey: 'all',
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: () => translate('All'),
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'grabbed',
|
||||
label: () => translate('Grabbed'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '1',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'imported',
|
||||
label: () => translate('Imported'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '3',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
label: () => translate('Failed'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '4',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'deleted',
|
||||
label: () => translate('Deleted'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '5',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'renamed',
|
||||
label: () => translate('Renamed'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '6',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'ignored',
|
||||
label: () => translate('Ignored'),
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '7',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: () => translate('EventType'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
|
||||
},
|
||||
{
|
||||
name: 'seriesIds',
|
||||
label: () => translate('Series'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.SERIES
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.QUALITY
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
type: filterBuilderTypes.CONTAINS,
|
||||
valueType: filterBuilderValueTypes.LANGUAGE
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'history.pageSize',
|
||||
'history.sortKey',
|
||||
'history.sortDirection',
|
||||
'history.selectedFilterKey',
|
||||
'history.columns'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_HISTORY = 'history/fetchHistory';
|
||||
export const GOTO_FIRST_HISTORY_PAGE = 'history/gotoHistoryFirstPage';
|
||||
export const GOTO_PREVIOUS_HISTORY_PAGE = 'history/gotoHistoryPreviousPage';
|
||||
export const GOTO_NEXT_HISTORY_PAGE = 'history/gotoHistoryNextPage';
|
||||
export const GOTO_LAST_HISTORY_PAGE = 'history/gotoHistoryLastPage';
|
||||
export const GOTO_HISTORY_PAGE = 'history/gotoHistoryPage';
|
||||
export const SET_HISTORY_SORT = 'history/setHistorySort';
|
||||
export const SET_HISTORY_FILTER = 'history/setHistoryFilter';
|
||||
export const SET_HISTORY_TABLE_OPTION = 'history/setHistoryTableOption';
|
||||
export const CLEAR_HISTORY = 'history/clearHistory';
|
||||
export const MARK_AS_FAILED = 'history/markAsFailed';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchHistory = createThunk(FETCH_HISTORY);
|
||||
export const gotoHistoryFirstPage = createThunk(GOTO_FIRST_HISTORY_PAGE);
|
||||
export const gotoHistoryPreviousPage = createThunk(GOTO_PREVIOUS_HISTORY_PAGE);
|
||||
export const gotoHistoryNextPage = createThunk(GOTO_NEXT_HISTORY_PAGE);
|
||||
export const gotoHistoryLastPage = createThunk(GOTO_LAST_HISTORY_PAGE);
|
||||
export const gotoHistoryPage = createThunk(GOTO_HISTORY_PAGE);
|
||||
export const setHistorySort = createThunk(SET_HISTORY_SORT);
|
||||
export const setHistoryFilter = createThunk(SET_HISTORY_FILTER);
|
||||
export const setHistoryTableOption = createAction(SET_HISTORY_TABLE_OPTION);
|
||||
export const clearHistory = createAction(CLEAR_HISTORY);
|
||||
export const markAsFailed = createThunk(MARK_AS_FAILED);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
...createServerSideCollectionHandlers(
|
||||
section,
|
||||
'/history',
|
||||
fetchHistory,
|
||||
{
|
||||
[serverSideCollectionHandlers.FETCH]: FETCH_HISTORY,
|
||||
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_HISTORY_PAGE,
|
||||
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_HISTORY_PAGE,
|
||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_HISTORY_PAGE,
|
||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_HISTORY_PAGE,
|
||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_HISTORY_PAGE,
|
||||
[serverSideCollectionHandlers.SORT]: SET_HISTORY_SORT,
|
||||
[serverSideCollectionHandlers.FILTER]: SET_HISTORY_FILTER
|
||||
}),
|
||||
|
||||
[MARK_AS_FAILED]: function(getState, payload, dispatch) {
|
||||
const id = payload.id;
|
||||
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id,
|
||||
isMarkingAsFailed: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/history/failed/${id}`,
|
||||
method: 'POST',
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id,
|
||||
isMarkingAsFailed: false,
|
||||
markAsFailedError: null
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id,
|
||||
isMarkingAsFailed: false,
|
||||
markAsFailedError: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_HISTORY_TABLE_OPTION]: createSetTableOptionReducer(section),
|
||||
|
||||
[CLEAR_HISTORY]: createClearReducer(section, {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: [],
|
||||
totalPages: 0,
|
||||
totalRecords: 0
|
||||
})
|
||||
|
||||
}, defaultState, section);
|
@@ -7,7 +7,6 @@ import * as episodes from './episodeActions';
|
||||
import * as episodeFiles from './episodeFileActions';
|
||||
import * as episodeHistory from './episodeHistoryActions';
|
||||
import * as episodeSelection from './episodeSelectionActions';
|
||||
import * as history from './historyActions';
|
||||
import * as importSeries from './importSeriesActions';
|
||||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
import * as oAuth from './oAuthActions';
|
||||
@@ -35,7 +34,6 @@ export default [
|
||||
episodeFiles,
|
||||
episodeHistory,
|
||||
episodeSelection,
|
||||
history,
|
||||
importSeries,
|
||||
interactiveImportActions,
|
||||
oAuth,
|
||||
|
Reference in New Issue
Block a user