diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts
index 8dfecab9e..36047cc4e 100644
--- a/frontend/src/App/State/AppState.ts
+++ b/frontend/src/App/State/AppState.ts
@@ -70,6 +70,7 @@ interface AppState {
captcha: CaptchaAppState;
commands: CommandAppState;
episodeFiles: EpisodeFilesAppState;
+ episodeHistory: HistoryAppState;
episodes: EpisodesAppState;
episodesSelection: EpisodesAppState;
history: HistoryAppState;
diff --git a/frontend/src/Episode/EpisodeDetailsModalContent.tsx b/frontend/src/Episode/EpisodeDetailsModalContent.tsx
index 75c8bef73..ec5a14116 100644
--- a/frontend/src/Episode/EpisodeDetailsModalContent.tsx
+++ b/frontend/src/Episode/EpisodeDetailsModalContent.tsx
@@ -19,7 +19,7 @@ import {
clearReleases,
} from 'Store/Actions/releaseActions';
import translate from 'Utilities/String/translate';
-import EpisodeHistoryConnector from './History/EpisodeHistoryConnector';
+import EpisodeHistory from './History/EpisodeHistory';
import EpisodeSearch from './Search/EpisodeSearch';
import SeasonEpisodeNumber from './SeasonEpisodeNumber';
import EpisodeSummary from './Summary/EpisodeSummary';
@@ -168,7 +168,7 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
-
+
diff --git a/frontend/src/Episode/History/EpisodeHistory.js b/frontend/src/Episode/History/EpisodeHistory.js
deleted file mode 100644
index 78f05a82d..000000000
--- a/frontend/src/Episode/History/EpisodeHistory.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Alert from 'Components/Alert';
-import Icon from 'Components/Icon';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import { icons, kinds } from 'Helpers/Props';
-import translate from 'Utilities/String/translate';
-import EpisodeHistoryRow from './EpisodeHistoryRow';
-
-const columns = [
- {
- name: 'eventType',
- isVisible: true
- },
- {
- name: 'sourceTitle',
- label: () => translate('SourceTitle'),
- isVisible: true
- },
- {
- name: 'languages',
- label: () => translate('Languages'),
- isVisible: true
- },
- {
- name: 'quality',
- label: () => translate('Quality'),
- isVisible: true
- },
- {
- name: 'customFormats',
- label: () => translate('CustomFormats'),
- isSortable: false,
- isVisible: true
- },
- {
- name: 'customFormatScore',
- label: React.createElement(Icon, {
- name: icons.SCORE,
- title: () => translate('CustomFormatScore')
- }),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'date',
- label: () => translate('Date'),
- isVisible: true
- },
- {
- name: 'actions',
- isVisible: true
- }
-];
-
-class EpisodeHistory extends Component {
-
- //
- // Render
-
- render() {
- const {
- isFetching,
- isPopulated,
- error,
- items,
- onMarkAsFailedPress
- } = this.props;
-
- const hasItems = !!items.length;
-
- if (isFetching) {
- return (
-
- );
- }
-
- if (!isFetching && !!error) {
- return (
- {translate('EpisodeHistoryLoadError')}
- );
- }
-
- if (isPopulated && !hasItems && !error) {
- return (
- {translate('NoEpisodeHistory')}
- );
- }
-
- if (isPopulated && hasItems && !error) {
- return (
-
-
- {
- items.map((item) => {
- return (
-
- );
- })
- }
-
-
- );
- }
-
- return null;
- }
-}
-
-EpisodeHistory.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- error: PropTypes.object,
- items: PropTypes.arrayOf(PropTypes.object).isRequired,
- onMarkAsFailedPress: PropTypes.func.isRequired
-};
-
-EpisodeHistory.defaultProps = {
- selectedTab: 'details'
-};
-
-export default EpisodeHistory;
diff --git a/frontend/src/Episode/History/EpisodeHistory.tsx b/frontend/src/Episode/History/EpisodeHistory.tsx
new file mode 100644
index 000000000..ea323ec60
--- /dev/null
+++ b/frontend/src/Episode/History/EpisodeHistory.tsx
@@ -0,0 +1,129 @@
+import React, { useCallback, useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import AppState from 'App/State/AppState';
+import Alert from 'Components/Alert';
+import Icon from 'Components/Icon';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import Column from 'Components/Table/Column';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import { icons, kinds } from 'Helpers/Props';
+import {
+ clearEpisodeHistory,
+ episodeHistoryMarkAsFailed,
+ fetchEpisodeHistory,
+} from 'Store/Actions/episodeHistoryActions';
+import translate from 'Utilities/String/translate';
+import EpisodeHistoryRow from './EpisodeHistoryRow';
+
+const columns: Column[] = [
+ {
+ name: 'eventType',
+ label: '',
+ isVisible: true,
+ },
+ {
+ name: 'sourceTitle',
+ label: () => translate('SourceTitle'),
+ isVisible: true,
+ },
+ {
+ name: 'languages',
+ label: () => translate('Languages'),
+ isVisible: true,
+ },
+ {
+ name: 'quality',
+ label: () => translate('Quality'),
+ isVisible: true,
+ },
+ {
+ name: 'customFormats',
+ label: () => translate('CustomFormats'),
+ isSortable: false,
+ isVisible: true,
+ },
+ {
+ name: 'customFormatScore',
+ label: React.createElement(Icon, {
+ name: icons.SCORE,
+ title: () => translate('CustomFormatScore'),
+ }),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'date',
+ label: () => translate('Date'),
+ isVisible: true,
+ },
+ {
+ name: 'actions',
+ label: '',
+ isVisible: true,
+ },
+];
+
+interface EpisodeHistoryProps {
+ episodeId: number;
+}
+
+function EpisodeHistory({ episodeId }: EpisodeHistoryProps) {
+ const dispatch = useDispatch();
+ const { items, isFetching, isPopulated, error } = useSelector(
+ (state: AppState) => state.episodeHistory
+ );
+
+ const handleMarkAsFailedPress = useCallback(
+ (historyId: number) => {
+ dispatch(episodeHistoryMarkAsFailed({ historyId, episodeId }));
+ },
+ [episodeId, dispatch]
+ );
+
+ const hasItems = !!items.length;
+
+ useEffect(() => {
+ dispatch(fetchEpisodeHistory({ episodeId }));
+
+ return () => {
+ dispatch(clearEpisodeHistory());
+ };
+ }, [episodeId, dispatch]);
+
+ if (isFetching) {
+ return ;
+ }
+
+ if (!isFetching && !!error) {
+ return (
+ {translate('EpisodeHistoryLoadError')}
+ );
+ }
+
+ if (isPopulated && !hasItems && !error) {
+ return {translate('NoEpisodeHistory')};
+ }
+
+ if (isPopulated && hasItems && !error) {
+ return (
+
+
+ {items.map((item) => {
+ return (
+
+ );
+ })}
+
+
+ );
+ }
+
+ return null;
+}
+
+export default EpisodeHistory;
diff --git a/frontend/src/Episode/History/EpisodeHistoryConnector.js b/frontend/src/Episode/History/EpisodeHistoryConnector.js
deleted file mode 100644
index 1e3414646..000000000
--- a/frontend/src/Episode/History/EpisodeHistoryConnector.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { clearEpisodeHistory, episodeHistoryMarkAsFailed, fetchEpisodeHistory } from 'Store/Actions/episodeHistoryActions';
-import EpisodeHistory from './EpisodeHistory';
-
-function createMapStateToProps() {
- return createSelector(
- (state) => state.episodeHistory,
- (episodeHistory) => {
- return episodeHistory;
- }
- );
-}
-
-const mapDispatchToProps = {
- fetchEpisodeHistory,
- clearEpisodeHistory,
- episodeHistoryMarkAsFailed
-};
-
-class EpisodeHistoryConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- this.props.fetchEpisodeHistory({ episodeId: this.props.episodeId });
- }
-
- componentWillUnmount() {
- this.props.clearEpisodeHistory();
- }
-
- //
- // Listeners
-
- onMarkAsFailedPress = (historyId) => {
- this.props.episodeHistoryMarkAsFailed({ historyId, episodeId: this.props.episodeId });
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-EpisodeHistoryConnector.propTypes = {
- episodeId: PropTypes.number.isRequired,
- fetchEpisodeHistory: PropTypes.func.isRequired,
- clearEpisodeHistory: PropTypes.func.isRequired,
- episodeHistoryMarkAsFailed: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeHistoryConnector);
diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.js b/frontend/src/Episode/History/EpisodeHistoryRow.js
deleted file mode 100644
index fd7fea827..000000000
--- a/frontend/src/Episode/History/EpisodeHistoryRow.js
+++ /dev/null
@@ -1,177 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-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 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';
-import EpisodeFormats from 'Episode/EpisodeFormats';
-import EpisodeLanguages from 'Episode/EpisodeLanguages';
-import EpisodeQuality from 'Episode/EpisodeQuality';
-import { icons, kinds, tooltipPositions } from 'Helpers/Props';
-import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
-import translate from 'Utilities/String/translate';
-import styles from './EpisodeHistoryRow.css';
-
-function getTitle(eventType) {
- switch (eventType) {
- case 'grabbed': return 'Grabbed';
- case 'seriesFolderImported': return 'Series Folder Imported';
- case 'downloadFolderImported': return 'Download Folder Imported';
- case 'downloadFailed': return 'Download Failed';
- case 'episodeFileDeleted': return 'Episode File Deleted';
- case 'episodeFileRenamed': return 'Episode File Renamed';
- default: return 'Unknown';
- }
-}
-
-class EpisodeHistoryRow extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- isMarkAsFailedModalOpen: false
- };
- }
-
- //
- // Listeners
-
- onMarkAsFailedPress = () => {
- this.setState({ isMarkAsFailedModalOpen: true });
- };
-
- onConfirmMarkAsFailed = () => {
- this.props.onMarkAsFailedPress(this.props.id);
- this.setState({ isMarkAsFailedModalOpen: false });
- };
-
- onMarkAsFailedModalClose = () => {
- this.setState({ isMarkAsFailedModalOpen: false });
- };
-
- //
- // Render
-
- render() {
- const {
- eventType,
- sourceTitle,
- languages,
- quality,
- qualityCutoffNotMet,
- customFormats,
- customFormatScore,
- date,
- data,
- downloadId
- } = this.props;
-
- const {
- isMarkAsFailedModalOpen
- } = this.state;
-
- return (
-
-
-
-
- {sourceTitle}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {formatCustomFormatScore(customFormatScore, customFormats.length)}
-
-
-
-
-
-
- }
- title={getTitle(eventType)}
- body={
-
- }
- position={tooltipPositions.LEFT}
- />
-
- {
- eventType === 'grabbed' &&
-
- }
-
-
-
-
- );
- }
-}
-
-EpisodeHistoryRow.propTypes = {
- id: PropTypes.number.isRequired,
- eventType: PropTypes.string.isRequired,
- sourceTitle: PropTypes.string.isRequired,
- languages: PropTypes.arrayOf(PropTypes.object).isRequired,
- quality: PropTypes.object.isRequired,
- qualityCutoffNotMet: PropTypes.bool.isRequired,
- customFormats: PropTypes.arrayOf(PropTypes.object),
- customFormatScore: PropTypes.number.isRequired,
- date: PropTypes.string.isRequired,
- data: PropTypes.object.isRequired,
- downloadId: PropTypes.string,
- onMarkAsFailedPress: PropTypes.func.isRequired
-};
-
-export default EpisodeHistoryRow;
diff --git a/frontend/src/Episode/History/EpisodeHistoryRow.tsx b/frontend/src/Episode/History/EpisodeHistoryRow.tsx
new file mode 100644
index 000000000..97b8cb479
--- /dev/null
+++ b/frontend/src/Episode/History/EpisodeHistoryRow.tsx
@@ -0,0 +1,151 @@
+import React, { useCallback, useState } from 'react';
+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 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';
+import EpisodeFormats from 'Episode/EpisodeFormats';
+import EpisodeLanguages from 'Episode/EpisodeLanguages';
+import EpisodeQuality from 'Episode/EpisodeQuality';
+import { icons, kinds, tooltipPositions } from 'Helpers/Props';
+import Language from 'Language/Language';
+import { QualityModel } from 'Quality/Quality';
+import CustomFormat from 'typings/CustomFormat';
+import { HistoryData, HistoryEventType } from 'typings/History';
+import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
+import translate from 'Utilities/String/translate';
+import styles from './EpisodeHistoryRow.css';
+
+function getTitle(eventType: HistoryEventType) {
+ switch (eventType) {
+ case 'grabbed':
+ return 'Grabbed';
+ case 'seriesFolderImported':
+ return 'Series Folder Imported';
+ case 'downloadFolderImported':
+ return 'Download Folder Imported';
+ case 'downloadFailed':
+ return 'Download Failed';
+ case 'episodeFileDeleted':
+ return 'Episode File Deleted';
+ case 'episodeFileRenamed':
+ return 'Episode File Renamed';
+ default:
+ return 'Unknown';
+ }
+}
+
+interface EpisodeHistoryRowProps {
+ id: number;
+ eventType: HistoryEventType;
+ sourceTitle: string;
+ languages: Language[];
+ quality: QualityModel;
+ qualityCutoffNotMet: boolean;
+ customFormats: CustomFormat[];
+ customFormatScore: number;
+ date: string;
+ data: HistoryData;
+ downloadId?: string;
+ onMarkAsFailedPress: (id: number) => void;
+}
+
+function EpisodeHistoryRow({
+ id,
+ eventType,
+ sourceTitle,
+ languages,
+ quality,
+ qualityCutoffNotMet,
+ customFormats,
+ customFormatScore,
+ date,
+ data,
+ downloadId,
+ onMarkAsFailedPress,
+}: EpisodeHistoryRowProps) {
+ const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false);
+
+ const handleMarkAsFailedPress = useCallback(() => {
+ setIsMarkAsFailedModalOpen(true);
+ }, []);
+
+ const handleConfirmMarkAsFailed = useCallback(() => {
+ onMarkAsFailedPress(id);
+ setIsMarkAsFailedModalOpen(false);
+ }, [id, onMarkAsFailedPress]);
+
+ const handleMarkAsFailedModalClose = useCallback(() => {
+ setIsMarkAsFailedModalOpen(false);
+ }, []);
+
+ return (
+
+
+
+ {sourceTitle}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatCustomFormatScore(customFormatScore, customFormats.length)}
+
+
+
+
+
+ }
+ title={getTitle(eventType)}
+ body={
+
+ }
+ position={tooltipPositions.LEFT}
+ />
+
+ {eventType === 'grabbed' && (
+
+ )}
+
+
+
+
+ );
+}
+
+export default EpisodeHistoryRow;
diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx
index 74473b5ed..1e0143b40 100644
--- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx
+++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx
@@ -74,9 +74,6 @@ interface SelectEpisodeModalContentProps {
onModalClose(): unknown;
}
-//
-// Render
-
function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
const {
selectedIds,