mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-11-28 08:58:41 +02:00
Convert History to TypeScript
This commit is contained in:
parent
ee80564dd4
commit
824ed0a369
@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
@ -119,7 +119,7 @@ function BlocklistRow(props: BlocklistRowProps) {
|
|||||||
if (name === 'date') {
|
if (name === 'date') {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore ts(2739)
|
// @ts-ignore ts(2739)
|
||||||
return <RelativeDateCellConnector key={name} date={date} />;
|
return <RelativeDateCell key={name} date={date} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'indexer') {
|
if (name === 'indexer') {
|
||||||
|
@ -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 (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
indexer ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
releaseGroup ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ReleaseGroup')}
|
|
||||||
data={releaseGroup}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
seriesMatchType ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('SeriesMatchType')}
|
|
||||||
data={seriesMatchType}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
nzbInfoUrl ?
|
|
||||||
<span>
|
|
||||||
<DescriptionListItemTitle>
|
|
||||||
{translate('InfoUrl')}
|
|
||||||
</DescriptionListItemTitle>
|
|
||||||
|
|
||||||
<DescriptionListItemDescription>
|
|
||||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadClientNameInfo ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DownloadClient')}
|
|
||||||
data={downloadClientNameInfo}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
age || ageHours || ageMinutes ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('AgeWhenGrabbed')}
|
|
||||||
data={formatAge(age, ageHours, ageMinutes)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
publishedDate ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('PublishedDate')}
|
|
||||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFolderImported') {
|
|
||||||
const {
|
|
||||||
customFormatScore,
|
|
||||||
droppedPath,
|
|
||||||
importedPath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
droppedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Source')}
|
|
||||||
data={droppedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
importedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ImportedTo')}
|
|
||||||
data={importedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Reason')}
|
|
||||||
data={reasonMessage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'episodeFileRenamed') {
|
|
||||||
const {
|
|
||||||
sourcePath,
|
|
||||||
sourceRelativePath,
|
|
||||||
path,
|
|
||||||
relativePath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourcePath')}
|
|
||||||
data={sourcePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourceRelativePath')}
|
|
||||||
data={sourceRelativePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationPath')}
|
|
||||||
data={path}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationRelativePath')}
|
|
||||||
data={relativePath}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadIgnored') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
289
frontend/src/Activity/History/Details/HistoryDetails.tsx
Normal file
289
frontend/src/Activity/History/Details/HistoryDetails.tsx
Normal file
@ -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 (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{indexer ? (
|
||||||
|
<DescriptionListItem title={translate('Indexer')} data={indexer} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseGroup ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseGroup')}
|
||||||
|
data={releaseGroup}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{seriesMatchType ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('SeriesMatchType')}
|
||||||
|
data={seriesMatchType}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{nzbInfoUrl ? (
|
||||||
|
<span>
|
||||||
|
<DescriptionListItemTitle>
|
||||||
|
{translate('InfoUrl')}
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription>
|
||||||
|
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadClientNameInfo ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={downloadClientNameInfo}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{age || ageHours || ageMinutes ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('AgeWhenGrabbed')}
|
||||||
|
data={formatAge(age, ageHours, ageMinutes)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{publishedDate ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('PublishedDate')}
|
||||||
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFailed') {
|
||||||
|
const { message } = data as DownloadFailedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFolderImported') {
|
||||||
|
const { customFormatScore, droppedPath, importedPath } =
|
||||||
|
data as DownloadFolderImportedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{droppedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Source')}
|
||||||
|
data={droppedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{importedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ImportedTo')}
|
||||||
|
data={importedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('Reason')} data={reasonMessage} />
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'episodeFileRenamed') {
|
||||||
|
const { sourcePath, sourceRelativePath, path, relativePath } =
|
||||||
|
data as EpisodeFileRenamedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourcePath')}
|
||||||
|
data={sourcePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourceRelativePath')}
|
||||||
|
data={sourceRelativePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('DestinationPath')} data={path} />
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DestinationRelativePath')}
|
||||||
|
data={relativePath}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadIgnored') {
|
||||||
|
const { message } = data as DownloadIgnoredHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryDetails;
|
@ -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);
|
|
@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import HistoryDetails from './HistoryDetails';
|
import HistoryDetails from './HistoryDetails';
|
||||||
import styles from './HistoryDetailsModal.css';
|
import styles from './HistoryDetailsModal.css';
|
||||||
|
|
||||||
function getHeaderTitle(eventType) {
|
function getHeaderTitle(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('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 {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
eventType,
|
eventType,
|
||||||
@ -42,18 +55,13 @@ function HistoryDetailsModal(props) {
|
|||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
onMarkAsFailedPress,
|
onMarkAsFailedPress,
|
||||||
onModalClose
|
onModalClose,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>{getHeaderTitle(eventType)}</ModalHeader>
|
||||||
{getHeaderTitle(eventType)}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<HistoryDetails
|
<HistoryDetails
|
||||||
@ -67,8 +75,7 @@ function HistoryDetailsModal(props) {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{
|
{eventType === 'grabbed' && (
|
||||||
eventType === 'grabbed' &&
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
className={styles.markAsFailedButton}
|
className={styles.markAsFailedButton}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
@ -77,34 +84,17 @@ function HistoryDetailsModal(props) {
|
|||||||
>
|
>
|
||||||
{translate('MarkAsFailed')}
|
{translate('MarkAsFailed')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {
|
HistoryDetailsModal.defaultProps = {
|
||||||
isMarkingAsFailed: false
|
isMarkingAsFailed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HistoryDetailsModal;
|
export default HistoryDetailsModal;
|
@ -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 (
|
|
||||||
<PageContent title={translate('History')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Refresh')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isFetching}
|
|
||||||
onPress={onFirstPagePress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
{...otherProps}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={HistoryFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isFetchingAny && !isAllPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetchingAny && hasError &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('HistoryLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// 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 &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('NoHistoryFound')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !!items.length &&
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<HistoryRowConnector
|
|
||||||
key={item.id}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetchingAny}
|
|
||||||
onFirstPagePress={onFirstPagePress}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
228
frontend/src/Activity/History/History.tsx
Normal file
228
frontend/src/Activity/History/History.tsx
Normal file
@ -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<HistoryItem, number>(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 (
|
||||||
|
<PageContent title={translate('History')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Refresh')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
onPress={handleFirstPagePress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{isFetchingAny && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetchingAny && hasError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||||
|
) : 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 ? (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<HistoryRow key={item.id} columns={columns} {...item} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
@ -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 (
|
|
||||||
<History
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
@ -1,12 +1,17 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
EpisodeFileDeletedHistory,
|
||||||
|
GrabbedHistoryData,
|
||||||
|
HistoryData,
|
||||||
|
HistoryEventType,
|
||||||
|
} from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType, data) {
|
function getIconName(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@ -17,7 +22,9 @@ function getIconName(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? icons.FILE_MISSING
|
||||||
|
: icons.DELETE;
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@ -27,7 +34,7 @@ function getIconName(eventType, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconKind(eventType) {
|
function getIconKind(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return kinds.DANGER;
|
return kinds.DANGER;
|
||||||
@ -36,10 +43,13 @@ function getIconKind(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTooltip(eventType, data) {
|
function getTooltip(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
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':
|
case 'seriesFolderImported':
|
||||||
return translate('SeriesFolderImportedTooltip');
|
return translate('SeriesFolderImportedTooltip');
|
||||||
case 'downloadFolderImported':
|
case 'downloadFolderImported':
|
||||||
@ -47,7 +57,9 @@ function getTooltip(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('DownloadFailedEpisodeTooltip');
|
return translate('DownloadFailedEpisodeTooltip');
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip');
|
return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? translate('EpisodeFileMissingTooltip')
|
||||||
|
: translate('EpisodeFileDeletedTooltip');
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return translate('EpisodeFileRenamedTooltip');
|
return translate('EpisodeFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
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 iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.cell} title={tooltip}>
|
||||||
className={styles.cell}
|
<Icon name={iconName} kind={iconKind} />
|
||||||
title={tooltip}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryEventTypeCell.propTypes = {
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryEventTypeCell.defaultProps = {
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryEventTypeCell;
|
export default HistoryEventTypeCell;
|
@ -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 (
|
|
||||||
<TableRow>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'eventType') {
|
|
||||||
return (
|
|
||||||
<HistoryEventTypeCell
|
|
||||||
key={name}
|
|
||||||
eventType={eventType}
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'series.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<SeriesTitleLink
|
|
||||||
titleSlug={series.titleSlug}
|
|
||||||
title={series.title}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episode') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<SeasonEpisodeNumber
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episodes.title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeTitleLink
|
|
||||||
episodeId={episodeId}
|
|
||||||
episodeEntity={episodeEntities.EPISODES}
|
|
||||||
seriesId={series.id}
|
|
||||||
episodeTitle={episode.title}
|
|
||||||
showOpenSeriesButton={true}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeLanguages languages={languages} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeQuality
|
|
||||||
quality={quality}
|
|
||||||
isCutoffMet={qualityCutoffNotMet}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'date') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={date}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.downloadClient}
|
|
||||||
>
|
|
||||||
{data.downloadClient}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexer}
|
|
||||||
>
|
|
||||||
{data.indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormatScore') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.customFormatScore}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'releaseGroup') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.releaseGroup}
|
|
||||||
>
|
|
||||||
{data.releaseGroup}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sourceTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
>
|
|
||||||
{sourceTitle}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'details') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.details}
|
|
||||||
>
|
|
||||||
<div className={styles.actionContents}>
|
|
||||||
<IconButton
|
|
||||||
name={icons.INFO}
|
|
||||||
onPress={this.onDetailsPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<HistoryDetailsModal
|
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
|
||||||
eventType={eventType}
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
data={data}
|
|
||||||
downloadId={downloadId}
|
|
||||||
isMarkingAsFailed={isMarkingAsFailed}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
275
frontend/src/Activity/History/HistoryRow.tsx
Normal file
275
frontend/src/Activity/History/HistoryRow.tsx
Normal file
@ -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 (
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'eventType') {
|
||||||
|
return (
|
||||||
|
<HistoryEventTypeCell
|
||||||
|
key={name}
|
||||||
|
eventType={eventType}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeasonEpisodeNumber
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodes.title') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeTitleLink
|
||||||
|
episodeId={episodeId}
|
||||||
|
episodeEntity={episodeEntities.EPISODES}
|
||||||
|
seriesId={series.id}
|
||||||
|
episodeTitle={episode.title}
|
||||||
|
showOpenSeriesButton={true}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeQuality
|
||||||
|
quality={quality}
|
||||||
|
isCutoffNotMet={qualityCutoffNotMet}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
return <RelativeDateCell key={name} date={date} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.downloadClient}>
|
||||||
|
{'downloadClient' in data ? data.downloadClient : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.indexer}>
|
||||||
|
{'indexer' in data ? data.indexer : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'releaseGroup') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.releaseGroup}>
|
||||||
|
{'releaseGroup' in data ? data.releaseGroup : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'details') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.details}>
|
||||||
|
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<HistoryDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
eventType={eventType}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
data={data}
|
||||||
|
downloadId={downloadId}
|
||||||
|
isMarkingAsFailed={isMarkingAsFailed}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryRow.defaultProps = {
|
||||||
|
customFormats: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryRow;
|
@ -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 (
|
|
||||||
<HistoryRow
|
|
||||||
{...this.props}
|
|
||||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
@ -4,7 +4,7 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
|||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
@ -217,7 +217,7 @@ class QueueRow extends Component {
|
|||||||
if (name === 'episodes.airDateUtc') {
|
if (name === 'episodes.airDateUtc') {
|
||||||
if (episode) {
|
if (episode) {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={episode.airDateUtc}
|
date={episode.airDateUtc}
|
||||||
/>
|
/>
|
||||||
@ -366,7 +366,7 @@ class QueueRow extends Component {
|
|||||||
|
|
||||||
if (name === 'added') {
|
if (name === 'added') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={added}
|
date={added}
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
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 QueueConnector from 'Activity/Queue/QueueConnector';
|
||||||
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
|
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
|
||||||
import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
|
import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
|
||||||
@ -125,7 +125,7 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/activity/history"
|
path="/activity/history"
|
||||||
component={HistoryConnector}
|
component={History}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
@ -59,6 +59,7 @@ interface AppState {
|
|||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
|
episodes: EpisodesAppState;
|
||||||
episodeFiles: EpisodeFilesAppState;
|
episodeFiles: EpisodeFilesAppState;
|
||||||
episodesSelection: EpisodesAppState;
|
episodesSelection: EpisodesAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import History from 'typings/History';
|
import History from 'typings/History';
|
||||||
|
|
||||||
interface HistoryAppState
|
interface HistoryAppState
|
||||||
extends AppSectionState<History>,
|
extends AppSectionState<History>,
|
||||||
AppSectionFilterState<History> {}
|
AppSectionFilterState<History>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
export default HistoryAppState;
|
export default HistoryAppState;
|
||||||
|
@ -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 (
|
|
||||||
<Component
|
|
||||||
className={className}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
className={className}
|
|
||||||
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })}
|
|
||||||
</Component>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
57
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
57
frontend/src/Components/Table/Cells/RelativeDateCell.tsx
Normal file
@ -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 <Component className={className} {...otherProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={className}
|
||||||
|
title={formatDateTime(date, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds,
|
||||||
|
includeRelativeDay: !showRelativeDates,
|
||||||
|
})}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{getRelativeDate({
|
||||||
|
date,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
timeFormat,
|
||||||
|
includeSeconds,
|
||||||
|
includeTime,
|
||||||
|
timeForToday: true,
|
||||||
|
})}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RelativeDateCell;
|
@ -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);
|
|
@ -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 (
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
hasSceneInformation ?
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
showSeasonNumber && seasonNumber != null &&
|
|
||||||
<Fragment>
|
|
||||||
{seasonNumber}x
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
|
|
||||||
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
|
||||||
|
|
||||||
{
|
|
||||||
seriesType === 'anime' && !!absoluteEpisodeNumber &&
|
|
||||||
<span className={styles.absoluteEpisodeNumber}>
|
|
||||||
({absoluteEpisodeNumber})
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
title={translate('SceneInformation')}
|
|
||||||
body={
|
|
||||||
<SceneInfo
|
|
||||||
seasonNumber={seasonNumber}
|
|
||||||
episodeNumber={episodeNumber}
|
|
||||||
sceneSeasonNumber={sceneSeasonNumber}
|
|
||||||
sceneEpisodeNumber={sceneEpisodeNumber}
|
|
||||||
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
|
|
||||||
alternateTitles={alternateTitles}
|
|
||||||
seriesType={seriesType}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/> :
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
showSeasonNumber && seasonNumber != null &&
|
|
||||||
<Fragment>
|
|
||||||
{seasonNumber}x
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
|
|
||||||
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
|
||||||
|
|
||||||
{
|
|
||||||
seriesType === 'anime' && !!absoluteEpisodeNumber &&
|
|
||||||
<span className={styles.absoluteEpisodeNumber}>
|
|
||||||
({absoluteEpisodeNumber})
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
warningMessage ?
|
|
||||||
<Icon
|
|
||||||
className={styles.warning}
|
|
||||||
name={icons.WARNING}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={warningMessage}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
140
frontend/src/Episode/EpisodeNumber.tsx
Normal file
140
frontend/src/Episode/EpisodeNumber.tsx
Normal file
@ -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 (
|
||||||
|
<span>
|
||||||
|
{hasSceneInformation ? (
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<span>
|
||||||
|
{showSeasonNumber && seasonNumber != null && (
|
||||||
|
<Fragment>{seasonNumber}x</Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
||||||
|
|
||||||
|
{seriesType === 'anime' && !!absoluteEpisodeNumber && (
|
||||||
|
<span className={styles.absoluteEpisodeNumber}>
|
||||||
|
({absoluteEpisodeNumber})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
title={translate('SceneInformation')}
|
||||||
|
body={
|
||||||
|
<SceneInfo
|
||||||
|
seasonNumber={seasonNumber}
|
||||||
|
episodeNumber={episodeNumber}
|
||||||
|
sceneSeasonNumber={sceneSeasonNumber}
|
||||||
|
sceneEpisodeNumber={sceneEpisodeNumber}
|
||||||
|
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
|
||||||
|
alternateTitles={alternateTitles}
|
||||||
|
seriesType={seriesType}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{showSeasonNumber && seasonNumber != null && (
|
||||||
|
<Fragment>{seasonNumber}x</Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSeasonNumber ? padNumber(episodeNumber, 2) : episodeNumber}
|
||||||
|
|
||||||
|
{seriesType === 'anime' && !!absoluteEpisodeNumber && (
|
||||||
|
<span className={styles.absoluteEpisodeNumber}>
|
||||||
|
({absoluteEpisodeNumber})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{warningMessage ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.warning}
|
||||||
|
name={icons.WARNING}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={warningMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EpisodeNumber;
|
@ -1,11 +1,15 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function getTooltip(title, quality, size) {
|
function getTooltip(
|
||||||
|
title: string,
|
||||||
|
quality: QualityModel,
|
||||||
|
size: number | undefined
|
||||||
|
) {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -27,7 +31,11 @@ function getTooltip(title, quality, size) {
|
|||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
function revisionLabel(className, quality, showRevision) {
|
function revisionLabel(
|
||||||
|
className: string | undefined,
|
||||||
|
quality: QualityModel,
|
||||||
|
showRevision: boolean
|
||||||
|
) {
|
||||||
if (!showRevision) {
|
if (!showRevision) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -55,16 +63,27 @@ function revisionLabel(className, quality, showRevision) {
|
|||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EpisodeQuality(props) {
|
interface EpisodeQualityProps {
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
quality: QualityModel;
|
||||||
|
size?: number;
|
||||||
|
isCutoffNotMet?: boolean;
|
||||||
|
showRevision?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EpisodeQuality(props: EpisodeQualityProps) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
title,
|
title = '',
|
||||||
quality,
|
quality,
|
||||||
size,
|
size,
|
||||||
isCutoffNotMet,
|
isCutoffNotMet,
|
||||||
showRevision
|
showRevision = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (!quality) {
|
if (!quality) {
|
||||||
@ -79,23 +98,10 @@ function EpisodeQuality(props) {
|
|||||||
title={getTooltip(title, quality, size)}
|
title={getTooltip(title, quality, size)}
|
||||||
>
|
>
|
||||||
{quality.quality.name}
|
{quality.quality.name}
|
||||||
</Label>{revisionLabel(className, quality, showRevision)}
|
</Label>
|
||||||
|
{revisionLabel(className, quality, showRevision)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
export default EpisodeQuality;
|
@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||||
@ -6,8 +5,12 @@ import FinaleType from './FinaleType';
|
|||||||
import styles from './EpisodeTitleLink.css';
|
import styles from './EpisodeTitleLink.css';
|
||||||
|
|
||||||
interface EpisodeTitleLinkProps {
|
interface EpisodeTitleLinkProps {
|
||||||
|
episodeId: number;
|
||||||
|
seriesId: number;
|
||||||
|
episodeEntity: string;
|
||||||
episodeTitle: string;
|
episodeTitle: string;
|
||||||
finaleType?: string;
|
finaleType?: string;
|
||||||
|
showOpenSeriesButton: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EpisodeTitleLink(props: EpisodeTitleLinkProps) {
|
function EpisodeTitleLink(props: EpisodeTitleLinkProps) {
|
||||||
@ -38,9 +41,4 @@ function EpisodeTitleLink(props: EpisodeTitleLinkProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpisodeTitleLink.propTypes = {
|
|
||||||
episodeTitle: PropTypes.string.isRequired,
|
|
||||||
finaleType: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EpisodeTitleLink;
|
export default EpisodeTitleLink;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
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 HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
@ -109,7 +109,7 @@ class EpisodeHistoryRow extends Component {
|
|||||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
date={date}
|
date={date}
|
||||||
includeSeconds={true}
|
includeSeconds={true}
|
||||||
includeTime={true}
|
includeTime={true}
|
||||||
@ -124,7 +124,7 @@ class EpisodeHistoryRow extends Component {
|
|||||||
}
|
}
|
||||||
title={getTitle(eventType)}
|
title={getTitle(eventType)}
|
||||||
body={
|
body={
|
||||||
<HistoryDetailsConnector
|
<HistoryDetails
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
sourceTitle={sourceTitle}
|
sourceTitle={sourceTitle}
|
||||||
data={data}
|
data={data}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import EpisodeNumber from './EpisodeNumber';
|
|
||||||
|
|
||||||
function SeasonEpisodeNumber(props) {
|
|
||||||
const {
|
|
||||||
airDate,
|
|
||||||
seriesType,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (seriesType === 'daily' && airDate) {
|
|
||||||
return (
|
|
||||||
<span>{airDate}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EpisodeNumber
|
|
||||||
seriesType={seriesType}
|
|
||||||
showSeasonNumber={true}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SeasonEpisodeNumber.propTypes = {
|
|
||||||
airDate: PropTypes.string,
|
|
||||||
seriesType: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SeasonEpisodeNumber;
|
|
26
frontend/src/Episode/SeasonEpisodeNumber.tsx
Normal file
26
frontend/src/Episode/SeasonEpisodeNumber.tsx
Normal file
@ -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 <span>{airDate}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EpisodeNumber
|
||||||
|
seriesType={seriesType}
|
||||||
|
showSeasonNumber={true}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SeasonEpisodeNumber;
|
17
frontend/src/Episode/createEpisodesFetchingSelector.ts
Normal file
17
frontend/src/Episode/createEpisodesFetchingSelector.ts
Normal file
@ -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;
|
44
frontend/src/Episode/useEpisode.ts
Normal file
44
frontend/src/Episode/useEpisode.ts
Normal file
@ -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;
|
@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRowButton from 'Components/Table/TableRowButton';
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@ -41,7 +41,7 @@ class RecentFolderRow extends Component {
|
|||||||
<TableRowButton onPress={this.onPress}>
|
<TableRowButton onPress={this.onPress}>
|
||||||
<TableRowCell>{folder}</TableRowCell>
|
<TableRowCell>{folder}</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector date={lastUsed} />
|
<RelativeDateCell date={lastUsed} />
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
<TableRowCell className={styles.actions}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
@ -176,7 +176,7 @@ class EpisodeRow extends Component {
|
|||||||
|
|
||||||
if (name === 'airDateUtc') {
|
if (name === 'airDateUtc') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={airDateUtc}
|
date={airDateUtc}
|
||||||
/>
|
/>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
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 HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
@ -133,7 +133,7 @@ class SeriesHistoryRow extends Component {
|
|||||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
date={date}
|
date={date}
|
||||||
includeSeconds={true}
|
includeSeconds={true}
|
||||||
includeTime={true}
|
includeTime={true}
|
||||||
@ -148,7 +148,7 @@ class SeriesHistoryRow extends Component {
|
|||||||
}
|
}
|
||||||
title={getTitle(eventType)}
|
title={getTitle(eventType)}
|
||||||
body={
|
body={
|
||||||
<HistoryDetailsConnector
|
<HistoryDetails
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
sourceTitle={sourceTitle}
|
sourceTitle={sourceTitle}
|
||||||
data={data}
|
data={data}
|
||||||
|
@ -8,7 +8,7 @@ import HeartRating from 'Components/HeartRating';
|
|||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
@ -252,7 +252,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore ts(2739)
|
// @ts-ignore ts(2739)
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
date={nextAiring}
|
date={nextAiring}
|
||||||
@ -265,7 +265,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore ts(2739)
|
// @ts-ignore ts(2739)
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
date={previousAiring}
|
date={previousAiring}
|
||||||
@ -278,7 +278,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore ts(2739)
|
// @ts-ignore ts(2739)
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
date={added}
|
date={added}
|
||||||
|
@ -4,7 +4,7 @@ import Icon from 'Components/Icon';
|
|||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
@ -110,7 +110,7 @@ class BackupRow extends Component {
|
|||||||
{formatBytes(size)}
|
{formatBytes(size)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
date={time}
|
date={time}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRowButton from 'Components/Table/TableRowButton';
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@ -98,7 +98,7 @@ class LogsTableRow extends Component {
|
|||||||
|
|
||||||
if (name === 'time') {
|
if (name === 'time') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={time}
|
date={time}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@ -23,7 +23,7 @@ class LogFilesTableRow extends Component {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableRowCell>{filename}</TableRowCell>
|
<TableRowCell>{filename}</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
date={lastWriteTime}
|
date={lastWriteTime}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -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;
|
|
13
frontend/src/Utilities/Object/selectUniqueIds.ts
Normal file
13
frontend/src/Utilities/Object/selectUniqueIds.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import KeysMatching from 'typings/Helpers/KeysMatching';
|
||||||
|
|
||||||
|
function selectUniqueIds<T, K>(items: T[], idProp: KeysMatching<T, K>) {
|
||||||
|
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;
|
@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
@ -99,7 +99,7 @@ function CutoffUnmetRow(props) {
|
|||||||
|
|
||||||
if (name === 'episodes.airDateUtc') {
|
if (name === 'episodes.airDateUtc') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={airDateUtc}
|
date={airDateUtc}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
@ -102,7 +102,7 @@ function MissingRow(props) {
|
|||||||
|
|
||||||
if (name === 'episodes.airDateUtc') {
|
if (name === 'episodes.airDateUtc') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={airDateUtc}
|
date={airDateUtc}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
type KeysMatching<T, V> = {
|
export type KeysMatching<T, V> = {
|
||||||
[K in keyof T]-?: T[K] extends V ? K : never;
|
[K in keyof T]-?: T[K] extends V ? K : never;
|
||||||
}[keyof T];
|
}[keyof T];
|
||||||
|
|
||||||
|
@ -11,6 +11,63 @@ export type HistoryEventType =
|
|||||||
| 'episodeFileRenamed'
|
| 'episodeFileRenamed'
|
||||||
| 'downloadIgnored';
|
| '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 {
|
export default interface History {
|
||||||
episodeId: number;
|
episodeId: number;
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
@ -23,6 +80,6 @@ export default interface History {
|
|||||||
date: string;
|
date: string;
|
||||||
downloadId: string;
|
downloadId: string;
|
||||||
eventType: HistoryEventType;
|
eventType: HistoryEventType;
|
||||||
data: unknown;
|
data: HistoryData;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user