Convert Series History to TypeScript

pull/7605/head
Mark McDowall 1 month ago
parent 17aab235a5
commit 0ff3101511
No known key found for this signature in database

@ -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;

@ -5,6 +5,8 @@ import AppSectionState, {
} from 'App/State/AppSectionState';
import History from 'typings/History';
export type SeriesHistoryAppState = AppSectionState<History>;
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History>,

@ -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 (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_EXTRA_LARGE}
onModalClose={onModalClose}
>
<SeriesHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
SeriesHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SeriesHistoryModal;

@ -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 (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_EXTRA_LARGE}
onModalClose={onModalClose}
>
<SeriesHistoryModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default SeriesHistoryModal;

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{seasonNumber == null ?
translate('History') :
translate('HistoryModalHeaderSeason', { season: formatSeason(seasonNumber) })
}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
}
{
isPopulated && !hasItems && !error &&
<div>{translate('NoHistory')}</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<SeriesHistoryRowConnector
key={item.id}
fullSeries={fullSeries}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
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;

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{seasonNumber == null
? translate('History')
: translate('HistoryModalHeaderSeason', {
season: formatSeason(seasonNumber)!,
})}
</ModalHeader>
<ModalBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
) : null}
{isPopulated && !hasItems && !error ? (
<div>{translate('NoHistory')}</div>
) : null}
{isPopulated && hasItems && !error ? (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return (
<SeriesHistoryRow
key={item.id}
fullSeries={fullSeries}
{...item}
onMarkAsFailedPress={handleMarkAsFailedPress}
/>
);
})}
</TableBody>
</Table>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default SeriesHistoryModalContent;

@ -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 (
<SeriesHistoryModalContent
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
);
}
}
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);

@ -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 (
<TableRow>
<HistoryEventTypeCell
eventType={eventType}
data={data}
/>
<TableRowCell key={name}>
<EpisodeComponent
seasonNumber={episode.seasonNumber}
episodeNumber={episode.episodeNumber}
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
seriesType={series.seriesType}
alternateTitles={series.alternateTitles}
sceneSeasonNumber={episode.sceneSeasonNumber}
sceneEpisodeNumber={episode.sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
/>
</TableRowCell>
<TableRowCell className={styles.sourceTitle}>
{sourceTitle}
</TableRowCell>
<TableRowCell>
<EpisodeLanguages languages={languages} />
</TableRowCell>
<TableRowCell>
<EpisodeQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
<TableRowCell>
<EpisodeFormats formats={customFormats} />
</TableRowCell>
<TableRowCell>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCell
date={date}
includeSeconds={true}
includeTime={true}
/>
<TableRowCell className={styles.actions}>
<Popover
anchor={
<Icon
name={icons.INFO}
/>
}
title={getTitle(eventType)}
body={
<HistoryDetails
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
/>
}
position={tooltipPositions.LEFT}
/>
{
eventType === 'grabbed' &&
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
confirmLabel={translate('MarkAsFailed')}
onConfirm={this.onConfirmMarkAsFailed}
onCancel={this.onMarkAsFailedModalClose}
/>
</TableRow>
);
}
}
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;

@ -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 (
<TableRow>
<HistoryEventTypeCell eventType={eventType} data={data} />
<TableRowCell>
<EpisodeComponent
seasonNumber={episode.seasonNumber}
episodeNumber={episode.episodeNumber}
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
seriesType={series.seriesType}
alternateTitles={series.alternateTitles}
sceneSeasonNumber={episode.sceneSeasonNumber}
sceneEpisodeNumber={episode.sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
/>
</TableRowCell>
<TableRowCell className={styles.sourceTitle}>{sourceTitle}</TableRowCell>
<TableRowCell>
<EpisodeLanguages languages={languages} />
</TableRowCell>
<TableRowCell>
<EpisodeQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
<TableRowCell>
<EpisodeFormats formats={customFormats} />
</TableRowCell>
<TableRowCell>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCell date={date} includeSeconds={true} includeTime={true} />
<TableRowCell className={styles.actions}>
<Popover
anchor={<Icon name={icons.INFO} />}
title={title}
body={
<HistoryDetails
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
/>
}
position={tooltipPositions.LEFT}
/>
{eventType === 'grabbed' ? (
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={handleMarkAsFailedPress}
/>
) : null}
</TableRowCell>
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
confirmLabel={translate('MarkAsFailed')}
onConfirm={handleConfirmMarkAsFailed}
onCancel={handleMarkAsFailedModalClose}
/>
</TableRow>
);
}
export default SeriesHistoryRow;

@ -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);
Loading…
Cancel
Save