1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Convert History to TypeScript

(cherry picked from commit 824ed0a36931ce7aae9aa544a7baf0738dae568c)

Closes #10230
Closes #10390
Closes #10247
This commit is contained in:
Mark McDowall
2024-07-19 20:42:59 -07:00
committed by Bogdan
parent 13f10906f1
commit 6747b74271
47 changed files with 923 additions and 1178 deletions

View File

@@ -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,
movieMatchType,
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
}
{
movieMatchType ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieMatchType')}
data={movieMatchType}
/> :
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
}
{
indexer ?
<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 === 'movieFileDeleted') {
const {
reason,
customFormatScore
} = data;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('DeletedReasonMissingFromDisk');
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 === 'movieFileRenamed') {
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;

View File

@@ -0,0 +1,287 @@
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,
GrabbedHistoryData,
HistoryData,
HistoryEventType,
MovieFileDeletedHistory,
MovieFileRenamedHistory,
} 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;
}
function HistoryDetails(props: HistoryDetailsProps) {
const { eventType, sourceTitle, data, downloadId } = props;
const { shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
if (eventType === 'grabbed') {
const {
indexer,
releaseGroup,
movieMatchType,
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}
{movieMatchType ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieMatchType')}
data={movieMatchType}
/>
) : 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 === 'movieFileDeleted') {
const { reason, customFormatScore } = data as MovieFileDeletedHistory;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('DeletedReasonMovieMissingFromDisk');
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 === 'movieFileRenamed') {
const { sourcePath, sourceRelativePath, path, relativePath } =
data as MovieFileRenamedHistory;
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;

View File

@@ -1,18 +0,0 @@
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 {
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
export default connect(createMapStateToProps)(HistoryDetails);

View File

@@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { HistoryData, HistoryEventType } from 'typings/History';
import translate from 'Utilities/String/translate';
import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
function getHeaderTitle(eventType: HistoryEventType) {
switch (eventType) {
case 'grabbed':
return translate('Grabbed');
@@ -31,29 +31,33 @@ function getHeaderTitle(eventType) {
}
}
function HistoryDetailsModal(props) {
interface HistoryDetailsModalProps {
isOpen: boolean;
eventType: HistoryEventType;
sourceTitle: string;
data: HistoryData;
downloadId?: string;
isMarkingAsFailed: boolean;
onMarkAsFailedPress: () => void;
onModalClose: () => void;
}
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
const {
isOpen,
eventType,
sourceTitle,
data,
downloadId,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
isMarkingAsFailed = false,
onMarkAsFailedPress,
onModalClose
onModalClose,
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{getHeaderTitle(eventType)}
</ModalHeader>
<ModalHeader>{getHeaderTitle(eventType)}</ModalHeader>
<ModalBody>
<HistoryDetails
@@ -61,50 +65,26 @@ function HistoryDetailsModal(props) {
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
</ModalBody>
<ModalFooter>
{
eventType === 'grabbed' &&
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
{translate('MarkAsFailed')}
</SpinnerButton>
}
{eventType === 'grabbed' && (
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
{translate('MarkAsFailed')}
</SpinnerButton>
)}
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
</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 = {
isMarkingAsFailed: false
};
export default HistoryDetailsModal;

View File

@@ -1,158 +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 translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
isMoviesFetching,
isMoviesPopulated,
moviesError,
items,
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
onFilterSelect,
onFirstPagePress,
...otherProps
} = this.props;
const isFetchingAny = isFetching || isMoviesFetching;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
const hasError = error || moviesError;
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={isFetching}
onFirstPagePress={onFirstPagePress}
{...otherProps}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
History.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: 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,
onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired
};
export default History;

View File

@@ -0,0 +1,216 @@
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 useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import { align, icons, kinds } from 'Helpers/Props';
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
import {
clearHistory,
fetchHistory,
gotoHistoryPage,
setHistoryFilter,
setHistorySort,
setHistoryTableOption,
} from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { TableOptionsChangePayload } from 'typings/Table';
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,
pageSize,
totalPages,
totalRecords,
} = useSelector((state: AppState) => state.history);
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
createMoviesFetchingSelector()
);
const customFilters = useSelector(createCustomFiltersSelector('history'));
const dispatch = useDispatch();
const isFetchingAny = isFetching || isMoviesFetching;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
const hasError = error || moviesError;
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());
};
}, [requestCurrentPage, 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}
pageSize={pageSize}
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 movies 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}
pageSize={pageSize}
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;

View File

@@ -1,141 +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 * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.movies,
createCustomFiltersSelector('history'),
(history, movies, customFilters) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
customFilters,
...history
};
}
);
}
const mapDispatchToProps = {
...historyActions
};
class HistoryConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchHistory,
gotoHistoryFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchHistory();
} else {
gotoHistoryFirstPage();
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearHistory();
}
//
// 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
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
);

View File

@@ -1,12 +1,17 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import {
GrabbedHistoryData,
HistoryData,
HistoryEventType,
MovieFileDeletedHistory,
} from 'typings/History';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
function getIconName(eventType, data) {
function getIconName(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
@@ -17,7 +22,9 @@ function getIconName(eventType, data) {
case 'downloadFailed':
return icons.DOWNLOADING;
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
? icons.FILE_MISSING
: icons.DELETE;
case 'movieFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
@@ -27,7 +34,7 @@ function getIconName(eventType, data) {
}
}
function getIconKind(eventType) {
function getIconKind(eventType: HistoryEventType) {
switch (eventType) {
case 'downloadFailed':
return kinds.DANGER;
@@ -36,52 +43,47 @@ function getIconKind(eventType) {
}
}
function getTooltip(eventType, data) {
function getTooltip(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
return translate('MovieGrabbedTooltip', {
indexer: (data as GrabbedHistoryData).indexer,
downloadClient: (data as GrabbedHistoryData).downloadClient,
});
case 'movieFolderImported':
return translate('MovieFolderImportedTooltip');
case 'downloadFolderImported':
return translate('MovieImportedTooltip');
case 'downloadFailed':
return translate('MovieDownloadFailedTooltip');
return translate('DownloadFailedMovieTooltip');
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
? translate('MovieFileMissingTooltip')
: translate('MovieFileDeletedTooltip');
case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip');
case 'downloadIgnored':
return translate('MovieDownloadIgnoredTooltip');
return translate('DownloadIgnoredMovieTooltip');
default:
return translate('UnknownEventTooltip');
}
}
function HistoryEventTypeCell({ eventType, data }) {
interface HistoryEventTypeCellProps {
eventType: HistoryEventType;
data: HistoryData;
}
function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
const iconName = getIconName(eventType, data);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);
return (
<TableRowCell
className={styles.cell}
title={tooltip}
>
<Icon
name={iconName}
kind={iconKind}
/>
<TableRowCell className={styles.cell} title={tooltip}>
<Icon name={iconName} kind={iconKind} />
</TableRowCell>
);
}
HistoryEventTypeCell.propTypes = {
eventType: PropTypes.string.isRequired,
data: PropTypes.object
};
HistoryEventTypeCell.defaultProps = {
data: {}
};
export default HistoryEventTypeCell;

View File

@@ -1,277 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
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 {
movie,
quality,
customFormats,
customFormatScore,
languages,
qualityCutoffNotMet,
eventType,
sourceTitle,
date,
data,
downloadId,
isMarkingAsFailed,
columns,
shortDateFormat,
timeFormat,
onMarkAsFailedPress
} = this.props;
if (!movie) {
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 === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/>
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
<MovieQuality
quality={quality}
isCutoffMet={qualityCutoffNotMet}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCell
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={<MovieFormats 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 = {
movieId: PropTypes.number,
movie: PropTypes.object.isRequired,
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;

View File

@@ -0,0 +1,224 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch } 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 usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import useMovie from 'Movie/useMovie';
import { QualityModel } from 'Quality/Quality';
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
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;
movieId: number;
languages: Language[];
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,
movieId,
languages,
quality,
customFormats = [],
customFormatScore,
qualityCutoffNotMet,
eventType,
sourceTitle,
date,
data,
downloadId,
isMarkingAsFailed = false,
markAsFailedError,
columns,
} = props;
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
const dispatch = useDispatch();
const movie = useMovie(movieId);
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 (!movie) {
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 === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages languages={languages} />
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
<MovieQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats 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={<MovieFormats 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}
onMarkAsFailedPress={handleMarkAsFailedPress}
onModalClose={handleDetailsModalClose}
/>
</TableRow>
);
}
export default HistoryRow;

View File

@@ -1,73 +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 createMovieSelector from 'Store/Selectors/createMovieSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import HistoryRow from './HistoryRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
return {
movie,
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);