diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.tsx b/frontend/src/Activity/Blocklist/BlocklistRow.tsx
index 58d75b1dd..c7410320d 100644
--- a/frontend/src/Activity/Blocklist/BlocklistRow.tsx
+++ b/frontend/src/Activity/Blocklist/BlocklistRow.tsx
@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
@@ -119,7 +119,7 @@ function BlocklistRow(props: BlocklistRowProps) {
if (name === 'date') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
- return ;
+ return ;
}
if (name === 'indexer') {
diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js
deleted file mode 100644
index 862d8707e..000000000
--- a/frontend/src/Activity/History/Details/HistoryDetails.js
+++ /dev/null
@@ -1,354 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import DescriptionList from 'Components/DescriptionList/DescriptionList';
-import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
-import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
-import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
-import Link from 'Components/Link/Link';
-import formatDateTime from 'Utilities/Date/formatDateTime';
-import formatAge from 'Utilities/Number/formatAge';
-import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
-import translate from 'Utilities/String/translate';
-import styles from './HistoryDetails.css';
-
-function HistoryDetails(props) {
- const {
- eventType,
- sourceTitle,
- data,
- downloadId,
- shortDateFormat,
- timeFormat
- } = props;
-
- if (eventType === 'grabbed') {
- const {
- indexer,
- releaseGroup,
- seriesMatchType,
- customFormatScore,
- nzbInfoUrl,
- downloadClient,
- downloadClientName,
- age,
- ageHours,
- ageMinutes,
- publishedDate
- } = data;
-
- const downloadClientNameInfo = downloadClientName ?? downloadClient;
-
- return (
-
-
-
- {
- indexer ?
- :
- null
- }
-
- {
- releaseGroup ?
- :
- null
- }
-
- {
- customFormatScore && customFormatScore !== '0' ?
- :
- null
- }
-
- {
- seriesMatchType ?
- :
- null
- }
-
- {
- nzbInfoUrl ?
-
-
- {translate('InfoUrl')}
-
-
-
- {nzbInfoUrl}
-
- :
- null
- }
-
- {
- downloadClientNameInfo ?
- :
- null
- }
-
- {
- downloadId ?
- :
- null
- }
-
- {
- age || ageHours || ageMinutes ?
- :
- null
- }
-
- {
- publishedDate ?
- :
- null
- }
-
- );
- }
-
- if (eventType === 'downloadFailed') {
- const {
- message
- } = data;
-
- return (
-
-
-
- {
- downloadId ?
- :
- null
- }
-
- {
- message ?
- :
- null
- }
-
- );
- }
-
- if (eventType === 'downloadFolderImported') {
- const {
- customFormatScore,
- droppedPath,
- importedPath
- } = data;
-
- return (
-
-
-
- {
- droppedPath ?
- :
- null
- }
-
- {
- importedPath ?
- :
- null
- }
-
- {
- customFormatScore && customFormatScore !== '0' ?
- :
- null
- }
-
- );
- }
-
- if (eventType === 'episodeFileDeleted') {
- const {
- reason,
- customFormatScore
- } = data;
-
- let reasonMessage = '';
-
- switch (reason) {
- case 'Manual':
- reasonMessage = translate('DeletedReasonManual');
- break;
- case 'MissingFromDisk':
- reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk');
- break;
- case 'Upgrade':
- reasonMessage = translate('DeletedReasonUpgrade');
- break;
- default:
- reasonMessage = '';
- }
-
- return (
-
-
-
-
-
- {
- customFormatScore && customFormatScore !== '0' ?
- :
- null
- }
-
- );
- }
-
- if (eventType === 'episodeFileRenamed') {
- const {
- sourcePath,
- sourceRelativePath,
- path,
- relativePath
- } = data;
-
- return (
-
-
-
-
-
-
-
-
-
- );
- }
-
- if (eventType === 'downloadIgnored') {
- const {
- message
- } = data;
-
- return (
-
-
-
- {
- downloadId ?
- :
- null
- }
-
- {
- message ?
- :
- null
- }
-
- );
- }
-
- return (
-
-
-
- );
-}
-
-HistoryDetails.propTypes = {
- eventType: PropTypes.string.isRequired,
- sourceTitle: PropTypes.string.isRequired,
- data: PropTypes.object.isRequired,
- downloadId: PropTypes.string,
- shortDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired
-};
-
-export default HistoryDetails;
diff --git a/frontend/src/Activity/History/Details/HistoryDetails.tsx b/frontend/src/Activity/History/Details/HistoryDetails.tsx
new file mode 100644
index 000000000..d4c8f9f4f
--- /dev/null
+++ b/frontend/src/Activity/History/Details/HistoryDetails.tsx
@@ -0,0 +1,289 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import DescriptionList from 'Components/DescriptionList/DescriptionList';
+import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
+import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
+import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
+import Link from 'Components/Link/Link';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import {
+ DownloadFailedHistory,
+ DownloadFolderImportedHistory,
+ DownloadIgnoredHistory,
+ EpisodeFileDeletedHistory,
+ EpisodeFileRenamedHistory,
+ GrabbedHistoryData,
+ HistoryData,
+ HistoryEventType,
+} from 'typings/History';
+import formatDateTime from 'Utilities/Date/formatDateTime';
+import formatAge from 'Utilities/Number/formatAge';
+import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
+import translate from 'Utilities/String/translate';
+import styles from './HistoryDetails.css';
+
+interface HistoryDetailsProps {
+ eventType: HistoryEventType;
+ sourceTitle: string;
+ data: HistoryData;
+ downloadId?: string;
+ shortDateFormat: string;
+ timeFormat: string;
+}
+
+function HistoryDetails(props: HistoryDetailsProps) {
+ const { eventType, sourceTitle, data, downloadId } = props;
+
+ const { shortDateFormat, timeFormat } = useSelector(
+ createUISettingsSelector()
+ );
+
+ if (eventType === 'grabbed') {
+ const {
+ indexer,
+ releaseGroup,
+ seriesMatchType,
+ customFormatScore,
+ nzbInfoUrl,
+ downloadClient,
+ downloadClientName,
+ age,
+ ageHours,
+ ageMinutes,
+ publishedDate,
+ } = data as GrabbedHistoryData;
+
+ const downloadClientNameInfo = downloadClientName ?? downloadClient;
+
+ return (
+
+
+
+ {indexer ? (
+
+ ) : null}
+
+ {releaseGroup ? (
+
+ ) : null}
+
+ {customFormatScore && customFormatScore !== '0' ? (
+
+ ) : null}
+
+ {seriesMatchType ? (
+
+ ) : null}
+
+ {nzbInfoUrl ? (
+
+
+ {translate('InfoUrl')}
+
+
+
+ {nzbInfoUrl}
+
+
+ ) : null}
+
+ {downloadClientNameInfo ? (
+
+ ) : null}
+
+ {downloadId ? (
+
+ ) : null}
+
+ {age || ageHours || ageMinutes ? (
+
+ ) : null}
+
+ {publishedDate ? (
+
+ ) : null}
+
+ );
+ }
+
+ if (eventType === 'downloadFailed') {
+ const { message } = data as DownloadFailedHistory;
+
+ return (
+
+
+
+ {downloadId ? (
+
+ ) : null}
+
+ {message ? (
+
+ ) : null}
+
+ );
+ }
+
+ if (eventType === 'downloadFolderImported') {
+ const { customFormatScore, droppedPath, importedPath } =
+ data as DownloadFolderImportedHistory;
+
+ return (
+
+
+
+ {droppedPath ? (
+
+ ) : null}
+
+ {importedPath ? (
+
+ ) : null}
+
+ {customFormatScore && customFormatScore !== '0' ? (
+
+ ) : null}
+
+ );
+ }
+
+ if (eventType === 'episodeFileDeleted') {
+ const { reason, customFormatScore } = data as EpisodeFileDeletedHistory;
+
+ let reasonMessage = '';
+
+ switch (reason) {
+ case 'Manual':
+ reasonMessage = translate('DeletedReasonManual');
+ break;
+ case 'MissingFromDisk':
+ reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk');
+ break;
+ case 'Upgrade':
+ reasonMessage = translate('DeletedReasonUpgrade');
+ break;
+ default:
+ reasonMessage = '';
+ }
+
+ return (
+
+
+
+
+
+ {customFormatScore && customFormatScore !== '0' ? (
+
+ ) : null}
+
+ );
+ }
+
+ if (eventType === 'episodeFileRenamed') {
+ const { sourcePath, sourceRelativePath, path, relativePath } =
+ data as EpisodeFileRenamedHistory;
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (eventType === 'downloadIgnored') {
+ const { message } = data as DownloadIgnoredHistory;
+
+ return (
+
+
+
+ {downloadId ? (
+
+ ) : null}
+
+ {message ? (
+
+ ) : null}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default HistoryDetails;
diff --git a/frontend/src/Activity/History/Details/HistoryDetailsConnector.js b/frontend/src/Activity/History/Details/HistoryDetailsConnector.js
deleted file mode 100644
index 0848c7905..000000000
--- a/frontend/src/Activity/History/Details/HistoryDetailsConnector.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import _ from 'lodash';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import HistoryDetails from './HistoryDetails';
-
-function createMapStateToProps() {
- return createSelector(
- createUISettingsSelector(),
- (uiSettings) => {
- return _.pick(uiSettings, [
- 'shortDateFormat',
- 'timeFormat'
- ]);
- }
- );
-}
-
-export default connect(createMapStateToProps)(HistoryDetails);
diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.js b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx
similarity index 58%
rename from frontend/src/Activity/History/Details/HistoryDetailsModal.js
rename to frontend/src/Activity/History/Details/HistoryDetailsModal.tsx
index ddeea5b78..a833bca5b 100644
--- a/frontend/src/Activity/History/Details/HistoryDetailsModal.js
+++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.tsx
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
+import { HistoryData, HistoryEventType } from 'typings/History';
import translate from 'Utilities/String/translate';
import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css';
-function getHeaderTitle(eventType) {
+function getHeaderTitle(eventType: HistoryEventType) {
switch (eventType) {
case 'grabbed':
return translate('Grabbed');
@@ -31,7 +31,20 @@ function getHeaderTitle(eventType) {
}
}
-function HistoryDetailsModal(props) {
+interface HistoryDetailsModalProps {
+ isOpen: boolean;
+ eventType: HistoryEventType;
+ sourceTitle: string;
+ data: HistoryData;
+ downloadId?: string;
+ isMarkingAsFailed: boolean;
+ shortDateFormat: string;
+ timeFormat: string;
+ onMarkAsFailedPress: () => void;
+ onModalClose: () => void;
+}
+
+function HistoryDetailsModal(props: HistoryDetailsModalProps) {
const {
isOpen,
eventType,
@@ -42,18 +55,13 @@ function HistoryDetailsModal(props) {
shortDateFormat,
timeFormat,
onMarkAsFailedPress,
- onModalClose
+ onModalClose,
} = props;
return (
-
+
-
- {getHeaderTitle(eventType)}
-
+ {getHeaderTitle(eventType)}
- {
- eventType === 'grabbed' &&
-
- {translate('MarkAsFailed')}
-
- }
+ {eventType === 'grabbed' && (
+
+ {translate('MarkAsFailed')}
+
+ )}
-
+
);
}
-HistoryDetailsModal.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- eventType: PropTypes.string.isRequired,
- sourceTitle: PropTypes.string.isRequired,
- data: PropTypes.object.isRequired,
- downloadId: PropTypes.string,
- isMarkingAsFailed: PropTypes.bool.isRequired,
- shortDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired,
- onMarkAsFailedPress: PropTypes.func.isRequired,
- onModalClose: PropTypes.func.isRequired
-};
-
HistoryDetailsModal.defaultProps = {
- isMarkingAsFailed: false
+ isMarkingAsFailed: false,
};
export default HistoryDetailsModal;
diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js
deleted file mode 100644
index e5cc31ecd..000000000
--- a/frontend/src/Activity/History/History.js
+++ /dev/null
@@ -1,180 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Alert from 'Components/Alert';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import FilterMenu from 'Components/Menu/FilterMenu';
-import PageContent from 'Components/Page/PageContent';
-import PageContentBody from 'Components/Page/PageContentBody';
-import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
-import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
-import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
-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 { align, icons, kinds } from 'Helpers/Props';
-import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
-import translate from 'Utilities/String/translate';
-import HistoryFilterModal from './HistoryFilterModal';
-import HistoryRowConnector from './HistoryRowConnector';
-
-class History extends Component {
-
- //
- // Lifecycle
-
- shouldComponentUpdate(nextProps) {
- // Don't update when fetching has completed if items have changed,
- // before episodes start fetching or when episodes start fetching.
-
- if (
- (
- this.props.isFetching &&
- nextProps.isPopulated &&
- hasDifferentItems(this.props.items, nextProps.items)
- ) ||
- (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
- ) {
- return false;
- }
-
- return true;
- }
-
- //
- // Render
-
- render() {
- const {
- isFetching,
- isPopulated,
- error,
- items,
- columns,
- selectedFilterKey,
- filters,
- customFilters,
- totalRecords,
- isEpisodesFetching,
- isEpisodesPopulated,
- episodesError,
- onFilterSelect,
- onFirstPagePress,
- ...otherProps
- } = this.props;
-
- const isFetchingAny = isFetching || isEpisodesFetching;
- const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
- const hasError = error || episodesError;
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- isFetchingAny && !isAllPopulated &&
-
- }
-
- {
- !isFetchingAny && hasError &&
-
- {translate('HistoryLoadError')}
-
- }
-
- {
- // 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 &&
-
- {translate('NoHistoryFound')}
-
- }
-
- {
- isAllPopulated && !hasError && !!items.length &&
-
-
-
- {
- items.map((item) => {
- return (
-
- );
- })
- }
-
-
-
-
-
- }
-
-
- );
- }
-}
-
-History.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- error: PropTypes.object,
- items: PropTypes.arrayOf(PropTypes.object).isRequired,
- columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
- filters: PropTypes.arrayOf(PropTypes.object).isRequired,
- customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
- totalRecords: PropTypes.number,
- isEpisodesFetching: PropTypes.bool.isRequired,
- isEpisodesPopulated: PropTypes.bool.isRequired,
- episodesError: PropTypes.object,
- onFilterSelect: PropTypes.func.isRequired,
- onFirstPagePress: PropTypes.func.isRequired
-};
-
-export default History;
diff --git a/frontend/src/Activity/History/History.tsx b/frontend/src/Activity/History/History.tsx
new file mode 100644
index 000000000..1020d90ea
--- /dev/null
+++ b/frontend/src/Activity/History/History.tsx
@@ -0,0 +1,228 @@
+import React, { useCallback, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import AppState from 'App/State/AppState';
+import Alert from 'Components/Alert';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import FilterMenu from 'Components/Menu/FilterMenu';
+import PageContent from 'Components/Page/PageContent';
+import PageContentBody from 'Components/Page/PageContentBody';
+import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
+import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
+import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
+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';
+import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
+import {
+ registerPagePopulator,
+ unregisterPagePopulator,
+} from 'Utilities/pagePopulator';
+import translate from 'Utilities/String/translate';
+import HistoryFilterModal from './HistoryFilterModal';
+import HistoryRow from './HistoryRow';
+
+function History() {
+ const requestCurrentPage = useCurrentPage();
+
+ const {
+ isFetching,
+ isPopulated,
+ error,
+ items,
+ columns,
+ selectedFilterKey,
+ filters,
+ sortKey,
+ sortDirection,
+ page,
+ totalPages,
+ totalRecords,
+ } = useSelector((state: AppState) => state.history);
+
+ 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 hasError = error || episodesError;
+
+ const {
+ handleFirstPagePress,
+ handlePreviousPagePress,
+ handleNextPagePress,
+ handleLastPagePress,
+ handlePageSelect,
+ } = usePaging({
+ page,
+ totalPages,
+ gotoPage: gotoHistoryPage,
+ });
+
+ const handleFilterSelect = useCallback(
+ (selectedFilterKey: string) => {
+ dispatch(setHistoryFilter({ selectedFilterKey }));
+ },
+ [dispatch]
+ );
+
+ const handleSortPress = useCallback(
+ (sortKey: string) => {
+ dispatch(setHistorySort({ sortKey }));
+ },
+ [dispatch]
+ );
+
+ const handleTableOptionChange = useCallback(
+ (payload: TableOptionsChangePayload) => {
+ dispatch(setHistoryTableOption(payload));
+
+ if (payload.pageSize) {
+ dispatch(gotoHistoryPage({ page: 1 }));
+ }
+ },
+ [dispatch]
+ );
+
+ useEffect(() => {
+ if (requestCurrentPage) {
+ dispatch(fetchHistory());
+ } else {
+ dispatch(gotoHistoryPage({ page: 1 }));
+ }
+
+ return () => {
+ dispatch(clearHistory());
+ dispatch(clearEpisodes());
+ dispatch(clearEpisodeFiles());
+ };
+ }, [requestCurrentPage, dispatch]);
+
+ useEffect(() => {
+ const episodeIds = selectUniqueIds(items, 'episodeId');
+
+ if (episodeIds.length) {
+ dispatch(fetchEpisodes({ episodeIds }));
+ } else {
+ dispatch(clearEpisodes());
+ }
+ }, [items, dispatch]);
+
+ useEffect(() => {
+ const repopulate = () => {
+ dispatch(fetchHistory());
+ };
+
+ registerPagePopulator(repopulate);
+
+ return () => {
+ unregisterPagePopulator(repopulate);
+ };
+ }, [dispatch]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isFetchingAny && !isAllPopulated ? : null}
+
+ {!isFetchingAny && hasError ? (
+ {translate('HistoryLoadError')}
+ ) : null}
+
+ {
+ // 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 ? (
+ {translate('NoHistoryFound')}
+ ) : null
+ }
+
+ {isAllPopulated && !hasError && items.length ? (
+
+
+
+ {items.map((item) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+ ) : null}
+
+
+ );
+}
+
+export default History;
diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js
deleted file mode 100644
index b407960bd..000000000
--- a/frontend/src/Activity/History/HistoryConnector.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import withCurrentPage from 'Components/withCurrentPage';
-import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
-import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
-import * as historyActions from 'Store/Actions/historyActions';
-import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
-import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
-import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
-import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
-import History from './History';
-
-function createMapStateToProps() {
- return createSelector(
- (state) => state.history,
- (state) => state.episodes,
- createCustomFiltersSelector('history'),
- (history, episodes, customFilters) => {
- return {
- isEpisodesFetching: episodes.isFetching,
- isEpisodesPopulated: episodes.isPopulated,
- episodesError: episodes.error,
- customFilters,
- ...history
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- ...historyActions,
- fetchEpisodes,
- clearEpisodes,
- clearEpisodeFiles
-};
-
-class HistoryConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- const {
- useCurrentPage,
- fetchHistory,
- gotoHistoryFirstPage
- } = this.props;
-
- registerPagePopulator(this.repopulate);
-
- if (useCurrentPage) {
- fetchHistory();
- } else {
- gotoHistoryFirstPage();
- }
- }
-
- componentDidUpdate(prevProps) {
- if (hasDifferentItems(prevProps.items, this.props.items)) {
- const episodeIds = selectUniqueIds(this.props.items, 'episodeId');
-
- if (episodeIds.length) {
- this.props.fetchEpisodes({ episodeIds });
- } else {
- this.props.clearEpisodes();
- }
- }
- }
-
- componentWillUnmount() {
- unregisterPagePopulator(this.repopulate);
- this.props.clearHistory();
- this.props.clearEpisodes();
- this.props.clearEpisodeFiles();
- }
-
- //
- // Control
-
- repopulate = () => {
- this.props.fetchHistory();
- };
-
- //
- // Listeners
-
- onFirstPagePress = () => {
- this.props.gotoHistoryFirstPage();
- };
-
- onPreviousPagePress = () => {
- this.props.gotoHistoryPreviousPage();
- };
-
- onNextPagePress = () => {
- this.props.gotoHistoryNextPage();
- };
-
- onLastPagePress = () => {
- this.props.gotoHistoryLastPage();
- };
-
- onPageSelect = (page) => {
- this.props.gotoHistoryPage({ page });
- };
-
- onSortPress = (sortKey) => {
- this.props.setHistorySort({ sortKey });
- };
-
- onFilterSelect = (selectedFilterKey) => {
- this.props.setHistoryFilter({ selectedFilterKey });
- };
-
- onTableOptionChange = (payload) => {
- this.props.setHistoryTableOption(payload);
-
- if (payload.pageSize) {
- this.props.gotoHistoryFirstPage();
- }
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-HistoryConnector.propTypes = {
- useCurrentPage: PropTypes.bool.isRequired,
- items: PropTypes.arrayOf(PropTypes.object).isRequired,
- fetchHistory: PropTypes.func.isRequired,
- gotoHistoryFirstPage: PropTypes.func.isRequired,
- gotoHistoryPreviousPage: PropTypes.func.isRequired,
- gotoHistoryNextPage: PropTypes.func.isRequired,
- gotoHistoryLastPage: PropTypes.func.isRequired,
- gotoHistoryPage: PropTypes.func.isRequired,
- setHistorySort: PropTypes.func.isRequired,
- setHistoryFilter: PropTypes.func.isRequired,
- setHistoryTableOption: PropTypes.func.isRequired,
- clearHistory: PropTypes.func.isRequired,
- fetchEpisodes: PropTypes.func.isRequired,
- clearEpisodes: PropTypes.func.isRequired,
- clearEpisodeFiles: PropTypes.func.isRequired
-};
-
-export default withCurrentPage(
- connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
-);
diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.js b/frontend/src/Activity/History/HistoryEventTypeCell.tsx
similarity index 60%
rename from frontend/src/Activity/History/HistoryEventTypeCell.js
rename to frontend/src/Activity/History/HistoryEventTypeCell.tsx
index 2f5ef6ee1..adedf08c0 100644
--- a/frontend/src/Activity/History/HistoryEventTypeCell.js
+++ b/frontend/src/Activity/History/HistoryEventTypeCell.tsx
@@ -1,12 +1,17 @@
-import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
+import {
+ EpisodeFileDeletedHistory,
+ GrabbedHistoryData,
+ HistoryData,
+ HistoryEventType,
+} from 'typings/History';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
-function getIconName(eventType, data) {
+function getIconName(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
@@ -17,7 +22,9 @@ function getIconName(eventType, data) {
case 'downloadFailed':
return icons.DOWNLOADING;
case 'episodeFileDeleted':
- return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
+ return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk'
+ ? icons.FILE_MISSING
+ : icons.DELETE;
case 'episodeFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
@@ -27,7 +34,7 @@ function getIconName(eventType, data) {
}
}
-function getIconKind(eventType) {
+function getIconKind(eventType: HistoryEventType) {
switch (eventType) {
case 'downloadFailed':
return kinds.DANGER;
@@ -36,10 +43,13 @@ function getIconKind(eventType) {
}
}
-function getTooltip(eventType, data) {
+function getTooltip(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
- return translate('EpisodeGrabbedTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
+ return translate('EpisodeGrabbedTooltip', {
+ indexer: (data as GrabbedHistoryData).indexer,
+ downloadClient: (data as GrabbedHistoryData).downloadClient,
+ });
case 'seriesFolderImported':
return translate('SeriesFolderImportedTooltip');
case 'downloadFolderImported':
@@ -47,7 +57,9 @@ function getTooltip(eventType, data) {
case 'downloadFailed':
return translate('DownloadFailedEpisodeTooltip');
case 'episodeFileDeleted':
- return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip');
+ return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk'
+ ? translate('EpisodeFileMissingTooltip')
+ : translate('EpisodeFileDeletedTooltip');
case 'episodeFileRenamed':
return translate('EpisodeFileRenamedTooltip');
case 'downloadIgnored':
@@ -57,31 +69,21 @@ function getTooltip(eventType, data) {
}
}
-function HistoryEventTypeCell({ eventType, data }) {
+interface HistoryEventTypeCellProps {
+ eventType: HistoryEventType;
+ data: HistoryData;
+}
+
+function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
const iconName = getIconName(eventType, data);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);
return (
-
-
+
+
);
}
-HistoryEventTypeCell.propTypes = {
- eventType: PropTypes.string.isRequired,
- data: PropTypes.object
-};
-
-HistoryEventTypeCell.defaultProps = {
- data: {}
-};
-
export default HistoryEventTypeCell;
diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js
deleted file mode 100644
index 507fdc2d7..000000000
--- a/frontend/src/Activity/History/HistoryRow.js
+++ /dev/null
@@ -1,312 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import IconButton from 'Components/Link/IconButton';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import TableRow from 'Components/Table/TableRow';
-import Tooltip from 'Components/Tooltip/Tooltip';
-import episodeEntities from 'Episode/episodeEntities';
-import EpisodeFormats from 'Episode/EpisodeFormats';
-import EpisodeLanguages from 'Episode/EpisodeLanguages';
-import EpisodeQuality from 'Episode/EpisodeQuality';
-import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
-import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
-import { icons, tooltipPositions } from 'Helpers/Props';
-import SeriesTitleLink from 'Series/SeriesTitleLink';
-import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
-import HistoryDetailsModal from './Details/HistoryDetailsModal';
-import HistoryEventTypeCell from './HistoryEventTypeCell';
-import styles from './HistoryRow.css';
-
-class HistoryRow extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- isDetailsModalOpen: false
- };
- }
-
- componentDidUpdate(prevProps) {
- if (
- prevProps.isMarkingAsFailed &&
- !this.props.isMarkingAsFailed &&
- !this.props.markAsFailedError
- ) {
- this.setState({ isDetailsModalOpen: false });
- }
- }
-
- //
- // Listeners
-
- onDetailsPress = () => {
- this.setState({ isDetailsModalOpen: true });
- };
-
- onDetailsModalClose = () => {
- this.setState({ isDetailsModalOpen: false });
- };
-
- //
- // Render
-
- render() {
- const {
- episodeId,
- series,
- episode,
- languages,
- quality,
- customFormats,
- customFormatScore,
- qualityCutoffNotMet,
- eventType,
- sourceTitle,
- date,
- data,
- downloadId,
- isMarkingAsFailed,
- columns,
- shortDateFormat,
- timeFormat,
- onMarkAsFailedPress
- } = this.props;
-
- if (!series || !episode) {
- return null;
- }
-
- return (
-
- {
- columns.map((column) => {
- const {
- name,
- isVisible
- } = column;
-
- if (!isVisible) {
- return null;
- }
-
- if (name === 'eventType') {
- return (
-
- );
- }
-
- if (name === 'series.sortTitle') {
- return (
-
-
-
- );
- }
-
- if (name === 'episode') {
- return (
-
-
-
- );
- }
-
- if (name === 'episodes.title') {
- return (
-
-
-
- );
- }
-
- if (name === 'languages') {
- return (
-
-
-
- );
- }
-
- if (name === 'quality') {
- return (
-
-
-
- );
- }
-
- if (name === 'customFormats') {
- return (
-
-
-
- );
- }
-
- if (name === 'date') {
- return (
-
- );
- }
-
- if (name === 'downloadClient') {
- return (
-
- {data.downloadClient}
-
- );
- }
-
- if (name === 'indexer') {
- return (
-
- {data.indexer}
-
- );
- }
-
- if (name === 'customFormatScore') {
- return (
-
- }
- position={tooltipPositions.BOTTOM}
- />
-
- );
- }
-
- if (name === 'releaseGroup') {
- return (
-
- {data.releaseGroup}
-
- );
- }
-
- if (name === 'sourceTitle') {
- return (
-
- {sourceTitle}
-
- );
- }
-
- if (name === 'details') {
- return (
-
-
-
-
-
- );
- }
-
- return null;
- })
- }
-
-
-
- );
- }
-
-}
-
-HistoryRow.propTypes = {
- episodeId: PropTypes.number,
- series: PropTypes.object.isRequired,
- episode: PropTypes.object,
- languages: PropTypes.arrayOf(PropTypes.object).isRequired,
- quality: PropTypes.object.isRequired,
- customFormats: PropTypes.arrayOf(PropTypes.object),
- customFormatScore: PropTypes.number.isRequired,
- qualityCutoffNotMet: PropTypes.bool.isRequired,
- eventType: PropTypes.string.isRequired,
- sourceTitle: PropTypes.string.isRequired,
- date: PropTypes.string.isRequired,
- data: PropTypes.object.isRequired,
- downloadId: PropTypes.string,
- isMarkingAsFailed: PropTypes.bool,
- markAsFailedError: PropTypes.object,
- columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- shortDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired,
- onMarkAsFailedPress: PropTypes.func.isRequired
-};
-
-HistoryRow.defaultProps = {
- customFormats: []
-};
-
-export default HistoryRow;
diff --git a/frontend/src/Activity/History/HistoryRow.tsx b/frontend/src/Activity/History/HistoryRow.tsx
new file mode 100644
index 000000000..bcd84f606
--- /dev/null
+++ b/frontend/src/Activity/History/HistoryRow.tsx
@@ -0,0 +1,275 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import IconButton from 'Components/Link/IconButton';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import Column from 'Components/Table/Column';
+import TableRow from 'Components/Table/TableRow';
+import Tooltip from 'Components/Tooltip/Tooltip';
+import episodeEntities from 'Episode/episodeEntities';
+import EpisodeFormats from 'Episode/EpisodeFormats';
+import EpisodeLanguages from 'Episode/EpisodeLanguages';
+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 { QualityModel } from 'Quality/Quality';
+import SeriesTitleLink from 'Series/SeriesTitleLink';
+import useSeries from 'Series/useSeries';
+import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import CustomFormat from 'typings/CustomFormat';
+import { HistoryData, HistoryEventType } from 'typings/History';
+import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
+import HistoryDetailsModal from './Details/HistoryDetailsModal';
+import HistoryEventTypeCell from './HistoryEventTypeCell';
+import styles from './HistoryRow.css';
+
+interface HistoryRowProps {
+ id: number;
+ episodeId: number;
+ seriesId: number;
+ languages: object[];
+ quality: QualityModel;
+ customFormats?: CustomFormat[];
+ customFormatScore: number;
+ qualityCutoffNotMet: boolean;
+ eventType: HistoryEventType;
+ sourceTitle: string;
+ date: string;
+ data: HistoryData;
+ downloadId?: string;
+ isMarkingAsFailed?: boolean;
+ markAsFailedError?: object;
+ columns: Column[];
+}
+
+function HistoryRow(props: HistoryRowProps) {
+ const {
+ id,
+ episodeId,
+ seriesId,
+ languages,
+ quality,
+ customFormats = [],
+ customFormatScore,
+ qualityCutoffNotMet,
+ eventType,
+ sourceTitle,
+ date,
+ data,
+ downloadId,
+ isMarkingAsFailed,
+ markAsFailedError,
+ columns,
+ } = props;
+
+ const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
+ const dispatch = useDispatch();
+ const series = useSeries(seriesId);
+ const episode = useEpisode(episodeId, 'episodes');
+
+ const { shortDateFormat, timeFormat } = useSelector(
+ createUISettingsSelector()
+ );
+
+ const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
+
+ const handleDetailsPress = useCallback(() => {
+ setIsDetailsModalOpen(true);
+ }, [setIsDetailsModalOpen]);
+
+ const handleDetailsModalClose = useCallback(() => {
+ 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;
+ }
+
+ return (
+
+ {columns.map((column) => {
+ const { name, isVisible } = column;
+
+ if (!isVisible) {
+ return null;
+ }
+
+ if (name === 'eventType') {
+ return (
+
+ );
+ }
+
+ if (name === 'series.sortTitle') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'episode') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'episodes.title') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'languages') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'quality') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'customFormats') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'date') {
+ return ;
+ }
+
+ if (name === 'downloadClient') {
+ return (
+
+ {'downloadClient' in data ? data.downloadClient : ''}
+
+ );
+ }
+
+ if (name === 'indexer') {
+ return (
+
+ {'indexer' in data ? data.indexer : ''}
+
+ );
+ }
+
+ if (name === 'customFormatScore') {
+ return (
+
+ }
+ position={tooltipPositions.BOTTOM}
+ />
+
+ );
+ }
+
+ if (name === 'releaseGroup') {
+ return (
+
+ {'releaseGroup' in data ? data.releaseGroup : ''}
+
+ );
+ }
+
+ if (name === 'sourceTitle') {
+ return {sourceTitle};
+ }
+
+ if (name === 'details') {
+ return (
+
+
+
+ );
+ }
+
+ return null;
+ })}
+
+
+
+ );
+}
+
+HistoryRow.defaultProps = {
+ customFormats: [],
+};
+
+export default HistoryRow;
diff --git a/frontend/src/Activity/History/HistoryRowConnector.js b/frontend/src/Activity/History/HistoryRowConnector.js
deleted file mode 100644
index b5d6223f6..000000000
--- a/frontend/src/Activity/History/HistoryRowConnector.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
-import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
-import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import HistoryRow from './HistoryRow';
-
-function createMapStateToProps() {
- return createSelector(
- createSeriesSelector(),
- createEpisodeSelector(),
- createUISettingsSelector(),
- (series, episode, uiSettings) => {
- return {
- series,
- episode,
- shortDateFormat: uiSettings.shortDateFormat,
- timeFormat: uiSettings.timeFormat
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- fetchHistory,
- markAsFailed
-};
-
-class HistoryRowConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidUpdate(prevProps) {
- if (
- prevProps.isMarkingAsFailed &&
- !this.props.isMarkingAsFailed &&
- !this.props.markAsFailedError
- ) {
- this.props.fetchHistory();
- }
- }
-
- //
- // Listeners
-
- onMarkAsFailedPress = () => {
- this.props.markAsFailed({ id: this.props.id });
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-
-}
-
-HistoryRowConnector.propTypes = {
- id: PropTypes.number.isRequired,
- isMarkingAsFailed: PropTypes.bool,
- markAsFailedError: PropTypes.object,
- fetchHistory: PropTypes.func.isRequired,
- markAsFailed: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);
diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js
index f143ace3f..523147078 100644
--- a/frontend/src/Activity/Queue/QueueRow.js
+++ b/frontend/src/Activity/Queue/QueueRow.js
@@ -4,7 +4,7 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
@@ -217,7 +217,7 @@ class QueueRow extends Component {
if (name === 'episodes.airDateUtc') {
if (episode) {
return (
-
@@ -366,7 +366,7 @@ class QueueRow extends Component {
if (name === 'added') {
return (
-
diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js
index e7f8a37ff..4a8330a6c 100644
--- a/frontend/src/App/AppRoutes.js
+++ b/frontend/src/App/AppRoutes.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import Blocklist from 'Activity/Blocklist/Blocklist';
-import HistoryConnector from 'Activity/History/HistoryConnector';
+import History from 'Activity/History/History';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
@@ -125,7 +125,7 @@ function AppRoutes(props) {
,
- AppSectionFilterState {}
+ AppSectionFilterState,
+ PagedAppSectionState,
+ TableAppSectionState {}
export default HistoryAppState;
diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js
deleted file mode 100644
index 37d23e8f9..000000000
--- a/frontend/src/Components/Table/Cells/RelativeDateCell.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { PureComponent } from 'react';
-import formatDateTime from 'Utilities/Date/formatDateTime';
-import getRelativeDate from 'Utilities/Date/getRelativeDate';
-import TableRowCell from './TableRowCell';
-import styles from './RelativeDateCell.css';
-
-class RelativeDateCell extends PureComponent {
-
- //
- // Render
-
- render() {
- const {
- className,
- date,
- includeSeconds,
- includeTime,
- showRelativeDates,
- shortDateFormat,
- longDateFormat,
- timeFormat,
- component: Component,
- dispatch,
- ...otherProps
- } = this.props;
-
- if (!date) {
- return (
-
- );
- }
-
- return (
-
- {getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })}
-
- );
- }
-}
-
-RelativeDateCell.propTypes = {
- className: PropTypes.string.isRequired,
- date: PropTypes.string,
- includeSeconds: PropTypes.bool.isRequired,
- includeTime: PropTypes.bool.isRequired,
- showRelativeDates: PropTypes.bool.isRequired,
- shortDateFormat: PropTypes.string.isRequired,
- longDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired,
- component: PropTypes.elementType,
- dispatch: PropTypes.func
-};
-
-RelativeDateCell.defaultProps = {
- className: styles.cell,
- includeSeconds: false,
- includeTime: false,
- component: TableRowCell
-};
-
-export default RelativeDateCell;
diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.tsx b/frontend/src/Components/Table/Cells/RelativeDateCell.tsx
new file mode 100644
index 000000000..1c5be48be
--- /dev/null
+++ b/frontend/src/Components/Table/Cells/RelativeDateCell.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import formatDateTime from 'Utilities/Date/formatDateTime';
+import getRelativeDate from 'Utilities/Date/getRelativeDate';
+import TableRowCell from './TableRowCell';
+import styles from './RelativeDateCell.css';
+
+interface RelativeDateCellProps {
+ className?: string;
+ date?: string;
+ includeSeconds?: boolean;
+ includeTime?: boolean;
+ component?: React.ElementType;
+}
+
+function RelativeDateCell(props: RelativeDateCellProps) {
+ const {
+ className = styles.cell,
+ date,
+ includeSeconds = false,
+ includeTime = false,
+
+ component: Component = TableRowCell,
+ ...otherProps
+ } = props;
+
+ const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
+ useSelector(createUISettingsSelector());
+
+ if (!date) {
+ return ;
+ }
+
+ return (
+
+ {getRelativeDate({
+ date,
+ shortDateFormat,
+ showRelativeDates,
+ timeFormat,
+ includeSeconds,
+ includeTime,
+ timeForToday: true,
+ })}
+
+ );
+}
+
+export default RelativeDateCell;
diff --git a/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js b/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js
deleted file mode 100644
index ed996abbe..000000000
--- a/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import _ from 'lodash';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import RelativeDateCell from './RelativeDateCell';
-
-function createMapStateToProps() {
- return createSelector(
- createUISettingsSelector(),
- (uiSettings) => {
- return _.pick(uiSettings, [
- 'showRelativeDates',
- 'shortDateFormat',
- 'longDateFormat',
- 'timeFormat'
- ]);
- }
- );
-}
-
-export default connect(createMapStateToProps, null)(RelativeDateCell);
diff --git a/frontend/src/Episode/EpisodeNumber.js b/frontend/src/Episode/EpisodeNumber.js
deleted file mode 100644
index 7f5c448f1..000000000
--- a/frontend/src/Episode/EpisodeNumber.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Fragment } from 'react';
-import Icon from 'Components/Icon';
-import Popover from 'Components/Tooltip/Popover';
-import { icons, kinds, tooltipPositions } from 'Helpers/Props';
-import padNumber from 'Utilities/Number/padNumber';
-import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
-import translate from 'Utilities/String/translate';
-import SceneInfo from './SceneInfo';
-import styles from './EpisodeNumber.css';
-
-function getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisodeNumber) {
- const messages = [];
-
- if (unverifiedSceneNumbering) {
- messages.push(translate('SceneNumberNotVerified'));
- }
-
- if (seriesType === 'anime' && !absoluteEpisodeNumber) {
- messages.push(translate('EpisodeMissingAbsoluteNumber'));
- }
-
- return messages.join('\n');
-}
-
-function EpisodeNumber(props) {
- const {
- seasonNumber,
- episodeNumber,
- absoluteEpisodeNumber,
- sceneSeasonNumber,
- sceneEpisodeNumber,
- sceneAbsoluteEpisodeNumber,
- useSceneNumbering,
- unverifiedSceneNumbering,
- alternateTitles: seriesAlternateTitles,
- seriesType,
- showSeasonNumber
- } = props;
-
- const alternateTitles = filterAlternateTitles(seriesAlternateTitles, null, useSceneNumbering, seasonNumber, sceneSeasonNumber);
-
- const hasSceneInformation = sceneSeasonNumber !== undefined ||
- sceneEpisodeNumber !== undefined ||
- (seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined) ||
- !!alternateTitles.length;
-
- const warningMessage = getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisodeNumber);
-
- return (
-
- {
- hasSceneInformation ?
-
- {
- showSeasonNumber && seasonNumber != null &&
-
- {seasonNumber}x
-
- }
-
- {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
-
- {
- seriesType === 'anime' && !!absoluteEpisodeNumber &&
-
- ({absoluteEpisodeNumber})
-
- }
-
- }
- title={translate('SceneInformation')}
- body={
-
- }
- position={tooltipPositions.RIGHT}
- /> :
-
- {
- showSeasonNumber && seasonNumber != null &&
-
- {seasonNumber}x
-
- }
-
- {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
-
- {
- seriesType === 'anime' && !!absoluteEpisodeNumber &&
-
- ({absoluteEpisodeNumber})
-
- }
-
- }
-
- {
- warningMessage ?
- :
- null
- }
-
-
- );
-}
-
-EpisodeNumber.propTypes = {
- seasonNumber: PropTypes.number.isRequired,
- episodeNumber: PropTypes.number.isRequired,
- absoluteEpisodeNumber: PropTypes.number,
- sceneSeasonNumber: PropTypes.number,
- sceneEpisodeNumber: PropTypes.number,
- sceneAbsoluteEpisodeNumber: PropTypes.number,
- useSceneNumbering: PropTypes.bool.isRequired,
- unverifiedSceneNumbering: PropTypes.bool.isRequired,
- alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
- seriesType: PropTypes.string,
- showSeasonNumber: PropTypes.bool.isRequired
-};
-
-EpisodeNumber.defaultProps = {
- useSceneNumbering: false,
- unverifiedSceneNumbering: false,
- alternateTitles: [],
- showSeasonNumber: false
-};
-
-export default EpisodeNumber;
diff --git a/frontend/src/Episode/EpisodeNumber.tsx b/frontend/src/Episode/EpisodeNumber.tsx
new file mode 100644
index 000000000..596174499
--- /dev/null
+++ b/frontend/src/Episode/EpisodeNumber.tsx
@@ -0,0 +1,140 @@
+import React, { Fragment } from 'react';
+import Icon from 'Components/Icon';
+import Popover from 'Components/Tooltip/Popover';
+import { icons, kinds, tooltipPositions } from 'Helpers/Props';
+import { AlternateTitle, SeriesType } from 'Series/Series';
+import padNumber from 'Utilities/Number/padNumber';
+import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
+import translate from 'Utilities/String/translate';
+import SceneInfo from './SceneInfo';
+import styles from './EpisodeNumber.css';
+
+function getWarningMessage(
+ unverifiedSceneNumbering: boolean,
+ seriesType: SeriesType | undefined,
+ absoluteEpisodeNumber: number | undefined
+) {
+ const messages = [];
+
+ if (unverifiedSceneNumbering) {
+ messages.push(translate('SceneNumberNotVerified'));
+ }
+
+ if (seriesType === 'anime' && !absoluteEpisodeNumber) {
+ messages.push(translate('EpisodeMissingAbsoluteNumber'));
+ }
+
+ return messages.join('\n');
+}
+
+export interface EpisodeNumberProps {
+ seasonNumber: number;
+ episodeNumber: number;
+ absoluteEpisodeNumber?: number;
+ sceneSeasonNumber?: number;
+ sceneEpisodeNumber?: number;
+ sceneAbsoluteEpisodeNumber?: number;
+ useSceneNumbering?: boolean;
+ unverifiedSceneNumbering?: boolean;
+ alternateTitles?: AlternateTitle[];
+ seriesType?: SeriesType;
+ showSeasonNumber?: boolean;
+}
+
+function EpisodeNumber(props: EpisodeNumberProps) {
+ const {
+ seasonNumber,
+ episodeNumber,
+ absoluteEpisodeNumber,
+ sceneSeasonNumber,
+ sceneEpisodeNumber,
+ sceneAbsoluteEpisodeNumber,
+ useSceneNumbering = false,
+ unverifiedSceneNumbering = false,
+ alternateTitles: seriesAlternateTitles = [],
+ seriesType,
+ showSeasonNumber = false,
+ } = props;
+
+ const alternateTitles = filterAlternateTitles(
+ seriesAlternateTitles,
+ null,
+ useSceneNumbering,
+ seasonNumber,
+ sceneSeasonNumber
+ );
+
+ const hasSceneInformation =
+ sceneSeasonNumber !== undefined ||
+ sceneEpisodeNumber !== undefined ||
+ (seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined) ||
+ !!alternateTitles.length;
+
+ const warningMessage = getWarningMessage(
+ unverifiedSceneNumbering,
+ seriesType,
+ absoluteEpisodeNumber
+ );
+
+ return (
+
+ {hasSceneInformation ? (
+
+ {showSeasonNumber && seasonNumber != null && (
+ {seasonNumber}x
+ )}
+
+ {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
+
+ {seriesType === 'anime' && !!absoluteEpisodeNumber && (
+
+ ({absoluteEpisodeNumber})
+
+ )}
+
+ }
+ title={translate('SceneInformation')}
+ body={
+
+ }
+ position={tooltipPositions.RIGHT}
+ />
+ ) : (
+
+ {showSeasonNumber && seasonNumber != null && (
+ {seasonNumber}x
+ )}
+
+ {showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
+
+ {seriesType === 'anime' && !!absoluteEpisodeNumber && (
+
+ ({absoluteEpisodeNumber})
+
+ )}
+
+ )}
+
+ {warningMessage ? (
+
+ ) : null}
+
+ );
+}
+
+export default EpisodeNumber;
diff --git a/frontend/src/Episode/EpisodeQuality.js b/frontend/src/Episode/EpisodeQuality.tsx
similarity index 69%
rename from frontend/src/Episode/EpisodeQuality.js
rename to frontend/src/Episode/EpisodeQuality.tsx
index 5ca64224a..4de00b0b0 100644
--- a/frontend/src/Episode/EpisodeQuality.js
+++ b/frontend/src/Episode/EpisodeQuality.tsx
@@ -1,11 +1,15 @@
-import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
+import { QualityModel } from 'Quality/Quality';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
-function getTooltip(title, quality, size) {
+function getTooltip(
+ title: string,
+ quality: QualityModel,
+ size: number | undefined
+) {
if (!title) {
return;
}
@@ -27,7 +31,11 @@ function getTooltip(title, quality, size) {
return title;
}
-function revisionLabel(className, quality, showRevision) {
+function revisionLabel(
+ className: string | undefined,
+ quality: QualityModel,
+ showRevision: boolean
+) {
if (!showRevision) {
return;
}
@@ -55,16 +63,27 @@ function revisionLabel(className, quality, showRevision) {
);
}
+
+ return null;
}
-function EpisodeQuality(props) {
+interface EpisodeQualityProps {
+ className?: string;
+ title?: string;
+ quality: QualityModel;
+ size?: number;
+ isCutoffNotMet?: boolean;
+ showRevision?: boolean;
+}
+
+function EpisodeQuality(props: EpisodeQualityProps) {
const {
className,
- title,
+ title = '',
quality,
size,
isCutoffNotMet,
- showRevision
+ showRevision = false,
} = props;
if (!quality) {
@@ -79,23 +98,10 @@ function EpisodeQuality(props) {
title={getTooltip(title, quality, size)}
>
{quality.quality.name}
- {revisionLabel(className, quality, showRevision)}
+
+ {revisionLabel(className, quality, showRevision)}
);
}
-EpisodeQuality.propTypes = {
- className: PropTypes.string,
- title: PropTypes.string,
- quality: PropTypes.object.isRequired,
- size: PropTypes.number,
- isCutoffNotMet: PropTypes.bool,
- showRevision: PropTypes.bool
-};
-
-EpisodeQuality.defaultProps = {
- title: '',
- showRevision: false
-};
-
export default EpisodeQuality;
diff --git a/frontend/src/Episode/EpisodeTitleLink.tsx b/frontend/src/Episode/EpisodeTitleLink.tsx
index f683820e0..e7455312d 100644
--- a/frontend/src/Episode/EpisodeTitleLink.tsx
+++ b/frontend/src/Episode/EpisodeTitleLink.tsx
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
import React, { useCallback, useState } from 'react';
import Link from 'Components/Link/Link';
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
@@ -6,8 +5,12 @@ import FinaleType from './FinaleType';
import styles from './EpisodeTitleLink.css';
interface EpisodeTitleLinkProps {
+ episodeId: number;
+ seriesId: number;
+ episodeEntity: string;
episodeTitle: string;
finaleType?: string;
+ showOpenSeriesButton: boolean;
}
function EpisodeTitleLink(props: EpisodeTitleLinkProps) {
@@ -38,9 +41,4 @@ function EpisodeTitleLink(props: EpisodeTitleLinkProps) {
);
}
-EpisodeTitleLink.propTypes = {
- episodeTitle: PropTypes.string.isRequired,
- finaleType: PropTypes.string,
-};
-
export default EpisodeTitleLink;
diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.js b/frontend/src/Episode/History/EpisodeHistoryRow.js
index 93cdb7c26..fd7fea827 100644
--- a/frontend/src/Episode/History/EpisodeHistoryRow.js
+++ b/frontend/src/Episode/History/EpisodeHistoryRow.js
@@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
+import HistoryDetails from 'Activity/History/Details/HistoryDetails';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
@@ -109,7 +109,7 @@ class EpisodeHistoryRow extends Component {
{formatCustomFormatScore(customFormatScore, customFormats.length)}
- {airDate}
- );
- }
-
- return (
-
- );
-}
-
-SeasonEpisodeNumber.propTypes = {
- airDate: PropTypes.string,
- seriesType: PropTypes.string
-};
-
-export default SeasonEpisodeNumber;
diff --git a/frontend/src/Episode/SeasonEpisodeNumber.tsx b/frontend/src/Episode/SeasonEpisodeNumber.tsx
new file mode 100644
index 000000000..c0a0afa51
--- /dev/null
+++ b/frontend/src/Episode/SeasonEpisodeNumber.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { SeriesType } from 'Series/Series';
+import EpisodeNumber, { EpisodeNumberProps } from './EpisodeNumber';
+
+interface SeasonEpisodeNumberProps extends EpisodeNumberProps {
+ airDate?: string;
+ seriesType?: SeriesType;
+}
+
+function SeasonEpisodeNumber(props: SeasonEpisodeNumberProps) {
+ const { airDate, seriesType, ...otherProps } = props;
+
+ if (seriesType === 'daily' && airDate) {
+ return {airDate};
+ }
+
+ return (
+
+ );
+}
+
+export default SeasonEpisodeNumber;
diff --git a/frontend/src/Episode/createEpisodesFetchingSelector.ts b/frontend/src/Episode/createEpisodesFetchingSelector.ts
new file mode 100644
index 000000000..c1ed3bbc6
--- /dev/null
+++ b/frontend/src/Episode/createEpisodesFetchingSelector.ts
@@ -0,0 +1,17 @@
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+
+function createEpisodesFetchingSelector() {
+ return createSelector(
+ (state: AppState) => state.episodes,
+ (episodes) => {
+ return {
+ isEpisodesFetching: episodes.isFetching,
+ isEpisodesPopulated: episodes.isPopulated,
+ episodesError: episodes.error,
+ };
+ }
+ );
+}
+
+export default createEpisodesFetchingSelector;
diff --git a/frontend/src/Episode/useEpisode.ts b/frontend/src/Episode/useEpisode.ts
new file mode 100644
index 000000000..4f32ffe7f
--- /dev/null
+++ b/frontend/src/Episode/useEpisode.ts
@@ -0,0 +1,44 @@
+import { useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+
+export type EpisodeEntities =
+ | 'calendar'
+ | 'episodes'
+ | 'interactiveImport'
+ | 'cutoffUnmet'
+ | 'missing';
+
+function createEpisodeSelector(episodeId: number) {
+ return createSelector(
+ (state: AppState) => state.episodes.items,
+ (episodes) => {
+ return episodes.find((e) => e.id === episodeId);
+ }
+ );
+}
+
+function createCalendarEpisodeSelector(episodeId: number) {
+ return createSelector(
+ (state: AppState) => state.calendar.items,
+ (episodes) => {
+ return episodes.find((e) => e.id === episodeId);
+ }
+ );
+}
+
+function useEpisode(episodeId: number, episodeEntity: EpisodeEntities) {
+ let selector = createEpisodeSelector;
+
+ switch (episodeEntity) {
+ case 'calendar':
+ selector = createCalendarEpisodeSelector;
+ break;
+ default:
+ break;
+ }
+
+ return useSelector(selector(episodeId));
+}
+
+export default useEpisode;
diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js
index 3903f0d71..83c7493c4 100644
--- a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js
+++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
@@ -41,7 +41,7 @@ class RecentFolderRow extends Component {
{folder}
-
+
diff --git a/frontend/src/Series/History/SeriesHistoryRow.js b/frontend/src/Series/History/SeriesHistoryRow.js
index 743c8c869..19ec358ee 100644
--- a/frontend/src/Series/History/SeriesHistoryRow.js
+++ b/frontend/src/Series/History/SeriesHistoryRow.js
@@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
+import HistoryDetails from 'Activity/History/Details/HistoryDetails';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
@@ -133,7 +133,7 @@ class SeriesHistoryRow extends Component {
{formatCustomFormatScore(customFormatScore, customFormats.length)}
-
-
diff --git a/frontend/src/System/Events/LogsTableRow.js b/frontend/src/System/Events/LogsTableRow.js
index 2de54a189..2c38ea10c 100644
--- a/frontend/src/System/Events/LogsTableRow.js
+++ b/frontend/src/System/Events/LogsTableRow.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
@@ -98,7 +98,7 @@ class LogsTableRow extends Component {
if (name === 'time') {
return (
-
diff --git a/frontend/src/System/Logs/Files/LogFilesTableRow.js b/frontend/src/System/Logs/Files/LogFilesTableRow.js
index ba0339b84..1e5ad552d 100644
--- a/frontend/src/System/Logs/Files/LogFilesTableRow.js
+++ b/frontend/src/System/Logs/Files/LogFilesTableRow.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import translate from 'Utilities/String/translate';
@@ -23,7 +23,7 @@ class LogFilesTableRow extends Component {
{filename}
-
diff --git a/frontend/src/Utilities/Object/selectUniqueIds.js b/frontend/src/Utilities/Object/selectUniqueIds.js
deleted file mode 100644
index c2c0c17e3..000000000
--- a/frontend/src/Utilities/Object/selectUniqueIds.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import _ from 'lodash';
-
-function selectUniqueIds(items, idProp) {
- const ids = _.reduce(items, (result, item) => {
- if (item[idProp]) {
- result.push(item[idProp]);
- }
-
- return result;
- }, []);
-
- return _.uniq(ids);
-}
-
-export default selectUniqueIds;
diff --git a/frontend/src/Utilities/Object/selectUniqueIds.ts b/frontend/src/Utilities/Object/selectUniqueIds.ts
new file mode 100644
index 000000000..847613c83
--- /dev/null
+++ b/frontend/src/Utilities/Object/selectUniqueIds.ts
@@ -0,0 +1,13 @@
+import KeysMatching from 'typings/Helpers/KeysMatching';
+
+function selectUniqueIds(items: T[], idProp: KeysMatching) {
+ return items.reduce((acc: K[], item) => {
+ if (item[idProp] && acc.indexOf(item[idProp] as K) === -1) {
+ acc.push(item[idProp] as K);
+ }
+
+ return acc;
+ }, []);
+}
+
+export default selectUniqueIds;
diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js
index 7d98eaee3..76fe0e0dd 100644
--- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js
+++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
@@ -99,7 +99,7 @@ function CutoffUnmetRow(props) {
if (name === 'episodes.airDateUtc') {
return (
-
diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js
index 92ecd451e..7064d9a9a 100644
--- a/frontend/src/Wanted/Missing/MissingRow.js
+++ b/frontend/src/Wanted/Missing/MissingRow.js
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
@@ -102,7 +102,7 @@ function MissingRow(props) {
if (name === 'episodes.airDateUtc') {
return (
-
diff --git a/frontend/src/typings/Helpers/KeysMatching.ts b/frontend/src/typings/Helpers/KeysMatching.ts
index 0e20206ef..107e0904f 100644
--- a/frontend/src/typings/Helpers/KeysMatching.ts
+++ b/frontend/src/typings/Helpers/KeysMatching.ts
@@ -1,4 +1,4 @@
-type KeysMatching = {
+export type KeysMatching = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];
diff --git a/frontend/src/typings/History.ts b/frontend/src/typings/History.ts
index 99fabe275..d20895f37 100644
--- a/frontend/src/typings/History.ts
+++ b/frontend/src/typings/History.ts
@@ -11,6 +11,63 @@ export type HistoryEventType =
| 'episodeFileRenamed'
| 'downloadIgnored';
+export interface GrabbedHistoryData {
+ indexer: string;
+ nzbInfoUrl: string;
+ releaseGroup: string;
+ age: string;
+ ageHours: string;
+ ageMinutes: string;
+ publishedDate: string;
+ downloadClient: string;
+ downloadClientName: string;
+ size: string;
+ downloadUrl: string;
+ guid: string;
+ tvdbId: string;
+ tvRageId: string;
+ protocol: string;
+ customFormatScore?: string;
+ seriesMatchType: string;
+ releaseSource: string;
+ indexerFlags: string;
+ releaseType: string;
+}
+
+export interface DownloadFailedHistory {
+ message: string;
+}
+
+export interface DownloadFolderImportedHistory {
+ customFormatScore?: string;
+ droppedPath: string;
+ importedPath: string;
+}
+
+export interface EpisodeFileDeletedHistory {
+ customFormatScore?: string;
+ reason: 'Manual' | 'MissingFromDisk' | 'Upgrade';
+}
+
+export interface EpisodeFileRenamedHistory {
+ sourcePath: string;
+ sourceRelativePath: string;
+ path: string;
+ relativePath: string;
+}
+
+export interface DownloadIgnoredHistory {
+ message: string;
+}
+
+export type HistoryData =
+ | GrabbedHistoryData
+ | DownloadFailedHistory
+ | DownloadFolderImportedHistory
+ | EpisodeFileDeletedHistory
+ | EpisodeFileRenamedHistory
+ | DownloadIgnoredHistory;
+
export default interface History {
episodeId: number;
seriesId: number;
@@ -23,6 +80,6 @@ export default interface History {
date: string;
downloadId: string;
eventType: HistoryEventType;
- data: unknown;
+ data: HistoryData;
id: number;
}