diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index f8b1593c6..7ce330f7f 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -9,7 +9,7 @@ import CommandAppState from './CommandAppState'; import CustomFiltersAppState from './CustomFiltersAppState'; import EpisodeFilesAppState from './EpisodeFilesAppState'; import EpisodesAppState from './EpisodesAppState'; -import HistoryAppState from './HistoryAppState'; +import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState'; import InteractiveImportAppState from './InteractiveImportAppState'; import MessagesAppState from './MessagesAppState'; import OAuthAppState from './OAuthAppState'; @@ -102,6 +102,7 @@ interface AppState { releases: ReleasesAppState; rootFolders: RootFolderAppState; series: SeriesAppState; + seriesHistory: SeriesHistoryAppState; seriesIndex: SeriesIndexAppState; settings: SettingsAppState; system: SystemAppState; diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts index 632b82179..0ffa92e0e 100644 --- a/frontend/src/App/State/HistoryAppState.ts +++ b/frontend/src/App/State/HistoryAppState.ts @@ -5,6 +5,8 @@ import AppSectionState, { } from 'App/State/AppSectionState'; import History from 'typings/History'; +export type SeriesHistoryAppState = AppSectionState; + interface HistoryAppState extends AppSectionState, AppSectionFilterState, diff --git a/frontend/src/Series/History/SeriesHistoryModal.js b/frontend/src/Series/History/SeriesHistoryModal.js deleted file mode 100644 index 0cd7ef9d0..000000000 --- a/frontend/src/Series/History/SeriesHistoryModal.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import SeriesHistoryModalContentConnector from './SeriesHistoryModalContentConnector'; - -function SeriesHistoryModal(props) { - const { - isOpen, - onModalClose, - ...otherProps - } = props; - - return ( - - - - ); -} - -SeriesHistoryModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SeriesHistoryModal; diff --git a/frontend/src/Series/History/SeriesHistoryModal.tsx b/frontend/src/Series/History/SeriesHistoryModal.tsx new file mode 100644 index 000000000..9e464859e --- /dev/null +++ b/frontend/src/Series/History/SeriesHistoryModal.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import SeriesHistoryModalContent, { + SeriesHistoryModalContentProps, +} from './SeriesHistoryModalContent'; + +interface SeriesHistoryModalProps extends SeriesHistoryModalContentProps { + isOpen: boolean; +} + +function SeriesHistoryModal({ + isOpen, + onModalClose, + ...otherProps +}: SeriesHistoryModalProps) { + return ( + + + + ); +} + +export default SeriesHistoryModal; diff --git a/frontend/src/Series/History/SeriesHistoryModalContent.js b/frontend/src/Series/History/SeriesHistoryModalContent.js deleted file mode 100644 index 72ddc5dc4..000000000 --- a/frontend/src/Series/History/SeriesHistoryModalContent.js +++ /dev/null @@ -1,154 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import { icons, kinds } from 'Helpers/Props'; -import formatSeason from 'Season/formatSeason'; -import translate from 'Utilities/String/translate'; -import SeriesHistoryRowConnector from './SeriesHistoryRowConnector'; - -const columns = [ - { - name: 'eventType', - isVisible: true - }, - { - name: 'episode', - label: () => translate('Episode'), - 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 SeriesHistoryModalContent extends Component { - - // - // Render - - render() { - const { - seasonNumber, - isFetching, - isPopulated, - error, - items, - onMarkAsFailedPress, - onModalClose - } = this.props; - - const fullSeries = seasonNumber == null; - const hasItems = !!items.length; - - return ( - - - {seasonNumber == null ? - translate('History') : - translate('HistoryModalHeaderSeason', { season: formatSeason(seasonNumber) }) - } - - - - { - isFetching && - - } - - { - !isFetching && !!error && - {translate('HistoryLoadError')} - } - - { - isPopulated && !hasItems && !error && -
{translate('NoHistory')}
- } - - { - isPopulated && hasItems && !error && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } -
- - - - -
- ); - } -} - -SeriesHistoryModalContent.propTypes = { - seasonNumber: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onMarkAsFailedPress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SeriesHistoryModalContent; diff --git a/frontend/src/Series/History/SeriesHistoryModalContent.tsx b/frontend/src/Series/History/SeriesHistoryModalContent.tsx new file mode 100644 index 000000000..779292ea4 --- /dev/null +++ b/frontend/src/Series/History/SeriesHistoryModalContent.tsx @@ -0,0 +1,170 @@ +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 Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +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 formatSeason from 'Season/formatSeason'; +import { + clearSeriesHistory, + fetchSeriesHistory, + seriesHistoryMarkAsFailed, +} from 'Store/Actions/seriesHistoryActions'; +import translate from 'Utilities/String/translate'; +import SeriesHistoryRow from './SeriesHistoryRow'; + +const columns: Column[] = [ + { + name: 'eventType', + label: '', + isVisible: true, + }, + { + name: 'episode', + label: () => translate('Episode'), + 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, + }, +]; + +export interface SeriesHistoryModalContentProps { + seriesId: number; + seasonNumber?: number; + onModalClose: () => void; +} + +function SeriesHistoryModalContent({ + seriesId, + seasonNumber, + onModalClose, +}: SeriesHistoryModalContentProps) { + const dispatch = useDispatch(); + + const { isFetching, isPopulated, error, items } = useSelector( + (state: AppState) => state.seriesHistory + ); + + const fullSeries = seasonNumber == null; + const hasItems = !!items.length; + + const handleMarkAsFailedPress = useCallback( + (historyId: number) => { + dispatch( + seriesHistoryMarkAsFailed({ + historyId, + seriesId, + seasonNumber, + }) + ); + }, + [seriesId, seasonNumber, dispatch] + ); + + useEffect(() => { + dispatch( + fetchSeriesHistory({ + seriesId, + seasonNumber, + }) + ); + + return () => { + dispatch(clearSeriesHistory()); + }; + }, [seriesId, seasonNumber, dispatch]); + + return ( + + + {seasonNumber == null + ? translate('History') + : translate('HistoryModalHeaderSeason', { + season: formatSeason(seasonNumber)!, + })} + + + + {isFetching && !isPopulated ? : null} + + {!isFetching && !!error ? ( + {translate('HistoryLoadError')} + ) : null} + + {isPopulated && !hasItems && !error ? ( +
{translate('NoHistory')}
+ ) : null} + + {isPopulated && hasItems && !error ? ( + + + {items.map((item) => { + return ( + + ); + })} + +
+ ) : null} +
+ + + + +
+ ); +} + +export default SeriesHistoryModalContent; diff --git a/frontend/src/Series/History/SeriesHistoryModalContentConnector.js b/frontend/src/Series/History/SeriesHistoryModalContentConnector.js deleted file mode 100644 index 5d78423eb..000000000 --- a/frontend/src/Series/History/SeriesHistoryModalContentConnector.js +++ /dev/null @@ -1,81 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearSeriesHistory, fetchSeriesHistory, seriesHistoryMarkAsFailed } from 'Store/Actions/seriesHistoryActions'; -import SeriesHistoryModalContent from './SeriesHistoryModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.seriesHistory, - (seriesHistory) => { - return seriesHistory; - } - ); -} - -const mapDispatchToProps = { - fetchSeriesHistory, - clearSeriesHistory, - seriesHistoryMarkAsFailed -}; - -class SeriesHistoryModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - seriesId, - seasonNumber - } = this.props; - - this.props.fetchSeriesHistory({ - seriesId, - seasonNumber - }); - } - - componentWillUnmount() { - this.props.clearSeriesHistory(); - } - - // - // Listeners - - onMarkAsFailedPress = (historyId) => { - const { - seriesId, - seasonNumber - } = this.props; - - this.props.seriesHistoryMarkAsFailed({ - historyId, - seriesId, - seasonNumber - }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -SeriesHistoryModalContentConnector.propTypes = { - seriesId: PropTypes.number.isRequired, - seasonNumber: PropTypes.number, - fetchSeriesHistory: PropTypes.func.isRequired, - clearSeriesHistory: PropTypes.func.isRequired, - seriesHistoryMarkAsFailed: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(SeriesHistoryModalContentConnector); diff --git a/frontend/src/Series/History/SeriesHistoryRow.js b/frontend/src/Series/History/SeriesHistoryRow.js deleted file mode 100644 index 19ec358ee..000000000 --- a/frontend/src/Series/History/SeriesHistoryRow.js +++ /dev/null @@ -1,204 +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 EpisodeNumber from 'Episode/EpisodeNumber'; -import EpisodeQuality from 'Episode/EpisodeQuality'; -import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; -import { icons, kinds, tooltipPositions } from 'Helpers/Props'; -import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; -import translate from 'Utilities/String/translate'; -import styles from './SeriesHistoryRow.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 SeriesHistoryRow 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, - date, - data, - downloadId, - fullSeries, - series, - episode, - customFormatScore - } = this.props; - - const { - isMarkAsFailedModalOpen - } = this.state; - - const EpisodeComponent = fullSeries ? SeasonEpisodeNumber : EpisodeNumber; - - if (!series || !episode) { - return null; - } - - return ( - - - - - - - - - {sourceTitle} - - - - - - - - - - - - - - - - {formatCustomFormatScore(customFormatScore, customFormats.length)} - - - - - - - } - title={getTitle(eventType)} - body={ - - } - position={tooltipPositions.LEFT} - /> - - { - eventType === 'grabbed' && - - } - - - - - ); - } -} - -SeriesHistoryRow.propTypes = { - id: PropTypes.number.isRequired, - eventType: PropTypes.string.isRequired, - sourceTitle: PropTypes.string.isRequired, - languages: PropTypes.arrayOf(PropTypes.object), - quality: PropTypes.object.isRequired, - qualityCutoffNotMet: PropTypes.bool.isRequired, - customFormats: PropTypes.arrayOf(PropTypes.object), - date: PropTypes.string.isRequired, - data: PropTypes.object.isRequired, - downloadId: PropTypes.string, - fullSeries: PropTypes.bool.isRequired, - series: PropTypes.object.isRequired, - episode: PropTypes.object.isRequired, - customFormatScore: PropTypes.number.isRequired, - onMarkAsFailedPress: PropTypes.func.isRequired -}; - -export default SeriesHistoryRow; diff --git a/frontend/src/Series/History/SeriesHistoryRow.tsx b/frontend/src/Series/History/SeriesHistoryRow.tsx new file mode 100644 index 000000000..adee7ce7a --- /dev/null +++ b/frontend/src/Series/History/SeriesHistoryRow.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useMemo, 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 EpisodeNumber from 'Episode/EpisodeNumber'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; +import useEpisode from 'Episode/useEpisode'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Language from 'Language/Language'; +import { QualityModel } from 'Quality/Quality'; +import useSeries from 'Series/useSeries'; +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 './SeriesHistoryRow.css'; + +interface SeriesHistoryRowProps { + id: number; + seriesId: number; + episodeId: number; + eventType: HistoryEventType; + sourceTitle: string; + languages?: Language[]; + quality: QualityModel; + qualityCutoffNotMet: boolean; + customFormats?: CustomFormat[]; + date: string; + data: HistoryData; + downloadId?: string; + fullSeries: boolean; + customFormatScore: number; + onMarkAsFailedPress: (historyId: number) => void; +} + +function SeriesHistoryRow({ + id, + seriesId, + episodeId, + eventType, + sourceTitle, + languages = [], + quality, + qualityCutoffNotMet, + customFormats = [], + date, + data, + downloadId, + fullSeries, + customFormatScore, + onMarkAsFailedPress, +}: SeriesHistoryRowProps) { + const series = useSeries(seriesId); + const episode = useEpisode(episodeId, 'episodes'); + + const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false); + + const EpisodeComponent = fullSeries ? SeasonEpisodeNumber : EpisodeNumber; + + const title = useMemo(() => { + 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'; + } + }, [eventType]); + + const handleMarkAsFailedPress = useCallback(() => { + setIsMarkAsFailedModalOpen(true); + }, []); + + const handleConfirmMarkAsFailed = useCallback(() => { + onMarkAsFailedPress(id); + setIsMarkAsFailedModalOpen(false); + }, [id, onMarkAsFailedPress]); + + const handleMarkAsFailedModalClose = useCallback(() => { + setIsMarkAsFailedModalOpen(false); + }, []); + + if (!series || !episode) { + return null; + } + + return ( + + + + + + + + {sourceTitle} + + + + + + + + + + + + + + + {formatCustomFormatScore(customFormatScore, customFormats.length)} + + + + + + } + title={title} + body={ + + } + position={tooltipPositions.LEFT} + /> + + {eventType === 'grabbed' ? ( + + ) : null} + + + + + ); +} + +export default SeriesHistoryRow; diff --git a/frontend/src/Series/History/SeriesHistoryRowConnector.js b/frontend/src/Series/History/SeriesHistoryRowConnector.js deleted file mode 100644 index 4d301f192..000000000 --- a/frontend/src/Series/History/SeriesHistoryRowConnector.js +++ /dev/null @@ -1,26 +0,0 @@ -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 SeriesHistoryRow from './SeriesHistoryRow'; - -function createMapStateToProps() { - return createSelector( - createSeriesSelector(), - createEpisodeSelector(), - (series, episode) => { - return { - series, - episode - }; - } - ); -} - -const mapDispatchToProps = { - fetchHistory, - markAsFailed -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(SeriesHistoryRow);