From f1d54d2a9a01bbbe4f75cb1d05184e3849d7ed1d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 7 Dec 2024 19:52:26 -0800 Subject: [PATCH] Convert EpisodeHistory to TypeScript --- frontend/src/App/State/AppState.ts | 1 + .../Episode/EpisodeDetailsModalContent.tsx | 4 +- .../src/Episode/History/EpisodeHistory.js | 130 ------------- .../src/Episode/History/EpisodeHistory.tsx | 129 +++++++++++++ .../History/EpisodeHistoryConnector.js | 63 ------- .../src/Episode/History/EpisodeHistoryRow.js | 177 ------------------ .../src/Episode/History/EpisodeHistoryRow.tsx | 151 +++++++++++++++ .../Episode/SelectEpisodeModalContent.tsx | 3 - 8 files changed, 283 insertions(+), 375 deletions(-) delete mode 100644 frontend/src/Episode/History/EpisodeHistory.js create mode 100644 frontend/src/Episode/History/EpisodeHistory.tsx delete mode 100644 frontend/src/Episode/History/EpisodeHistoryConnector.js delete mode 100644 frontend/src/Episode/History/EpisodeHistoryRow.js create mode 100644 frontend/src/Episode/History/EpisodeHistoryRow.tsx 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,