Convert Cutoff Unmet to TypeScript

pull/7640/head
Mark McDowall 2 months ago
parent 4c6d6b726e
commit 45c53bea86
No known key found for this signature in database

@ -31,7 +31,7 @@ import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import Updates from 'System/Updates/Updates';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import CutoffUnmet from 'Wanted/CutoffUnmet/CutoffUnmet';
import MissingConnector from 'Wanted/Missing/MissingConnector';
function RedirectWithUrlBase() {
@ -90,7 +90,7 @@ function AppRoutes() {
<Route path="/wanted/missing" component={MissingConnector} />
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
<Route path="/wanted/cutoffunmet" component={CutoffUnmet} />
{/*
Settings

@ -1,9 +1,25 @@
import AppSectionState from 'App/State/AppSectionState';
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from 'App/State/AppSectionState';
import Episode from 'Episode/Episode';
type WantedCutoffUnmetAppState = AppSectionState<Episode>;
interface WantedEpisode extends Episode {
isSaving?: boolean;
}
interface WantedCutoffUnmetAppState
extends AppSectionState<WantedEpisode>,
AppSectionFilterState<WantedEpisode>,
PagedAppSectionState,
TableAppSectionState {}
type WantedMissingAppState = AppSectionState<Episode>;
interface WantedMissingAppState
extends AppSectionState<WantedEpisode>,
AppSectionFilterState<WantedEpisode>,
PagedAppSectionState,
TableAppSectionState {}
interface WantedAppState {
cutoffUnmet: WantedCutoffUnmetAppState;

@ -1,14 +1,14 @@
import React, { useCallback, useState } from 'react';
import Modal from 'Components/Modal/Modal';
import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab';
import { EpisodeEntities } from 'Episode/useEpisode';
import { EpisodeEntity } from 'Episode/useEpisode';
import { sizes } from 'Helpers/Props';
import EpisodeDetailsModalContent from './EpisodeDetailsModalContent';
interface EpisodeDetailsModalProps {
isOpen: boolean;
episodeId: number;
episodeEntity: EpisodeEntities;
episodeEntity: EpisodeEntity;
seriesId: number;
episodeTitle: string;
isSaving?: boolean;

@ -10,7 +10,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import Episode from 'Episode/Episode';
import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab';
import episodeEntities from 'Episode/episodeEntities';
import useEpisode, { EpisodeEntities } from 'Episode/useEpisode';
import useEpisode, { EpisodeEntity } from 'Episode/useEpisode';
import Series from 'Series/Series';
import useSeries from 'Series/useSeries';
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
@ -29,7 +29,7 @@ const TABS: EpisodeDetailsTab[] = ['details', 'history', 'search'];
export interface EpisodeDetailsModalContentProps {
episodeId: number;
episodeEntity: EpisodeEntities;
episodeEntity: EpisodeEntity;
seriesId: number;
episodeTitle: string;
isSaving?: boolean;

@ -4,7 +4,7 @@ import { EPISODE_SEARCH } from 'Commands/commandNames';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { EpisodeEntities } from 'Episode/useEpisode';
import { EpisodeEntity } from 'Episode/useEpisode';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
@ -15,14 +15,19 @@ import styles from './EpisodeSearchCell.css';
interface EpisodeSearchCellProps {
episodeId: number;
episodeEntity: EpisodeEntities;
episodeEntity: EpisodeEntity;
seriesId: number;
episodeTitle: string;
showOpenSeriesButton: boolean;
}
function EpisodeSearchCell(props: EpisodeSearchCellProps) {
const { episodeId, episodeEntity, seriesId, episodeTitle } = props;
function EpisodeSearchCell({
episodeId,
episodeEntity,
seriesId,
episodeTitle,
showOpenSeriesButton,
}: EpisodeSearchCellProps) {
const executingCommands = useSelector(createExecutingCommandsSelector());
const isSearching = executingCommands.some(({ name, body }) => {
const { episodeIds = [] } = body;
@ -66,6 +71,7 @@ function EpisodeSearchCell(props: EpisodeSearchCellProps) {
episodeTitle={episodeTitle}
selectedTab="search"
startInteractiveSearch={true}
showOpenSeriesButton={showOpenSeriesButton}
onModalClose={setDetailsModalClosed}
/>
</TableRowCell>

@ -4,7 +4,7 @@ import QueueDetails from 'Activity/Queue/QueueDetails';
import Icon from 'Components/Icon';
import ProgressBar from 'Components/ProgressBar';
import Episode from 'Episode/Episode';
import useEpisode, { EpisodeEntities } from 'Episode/useEpisode';
import useEpisode, { EpisodeEntity } from 'Episode/useEpisode';
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
import { icons, kinds, sizes } from 'Helpers/Props';
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
@ -15,13 +15,15 @@ import styles from './EpisodeStatus.css';
interface EpisodeStatusProps {
episodeId: number;
episodeEntity?: EpisodeEntities;
episodeEntity?: EpisodeEntity;
episodeFileId: number;
}
function EpisodeStatus(props: EpisodeStatusProps) {
const { episodeId, episodeEntity = 'episodes', episodeFileId } = props;
function EpisodeStatus({
episodeId,
episodeEntity = 'episodes',
episodeFileId,
}: EpisodeStatusProps) {
const {
airDateUtc,
monitored,

@ -1,14 +1,14 @@
import React, { useCallback, useState } from 'react';
import Link from 'Components/Link/Link';
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
import { EpisodeEntities } from 'Episode/useEpisode';
import { EpisodeEntity } from 'Episode/useEpisode';
import FinaleType from './FinaleType';
import styles from './EpisodeTitleLink.css';
interface EpisodeTitleLinkProps {
episodeId: number;
seriesId: number;
episodeEntity: EpisodeEntities;
episodeEntity: EpisodeEntity;
episodeTitle: string;
finaleType?: string;
showOpenSeriesButton: boolean;

@ -6,7 +6,7 @@ import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import Episode from 'Episode/Episode';
import useEpisode, { EpisodeEntities } from 'Episode/useEpisode';
import useEpisode, { EpisodeEntity } from 'Episode/useEpisode';
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
import { icons, kinds, sizes } from 'Helpers/Props';
import Series from 'Series/Series';
@ -72,7 +72,7 @@ const COLUMNS: Column[] = [
interface EpisodeSummaryProps {
seriesId: number;
episodeId: number;
episodeEntity: EpisodeEntities;
episodeEntity: EpisodeEntity;
episodeFileId?: number;
}

@ -3,7 +3,7 @@ import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Episode from './Episode';
export type EpisodeEntities =
export type EpisodeEntity =
| 'calendar'
| 'episodes'
| 'interactiveImport.episodes'
@ -48,7 +48,7 @@ function createWantedMissingEpisodeSelector(episodeId?: number) {
function useEpisode(
episodeId: number | undefined,
episodeEntity: EpisodeEntities
episodeEntity: EpisodeEntity
) {
let selector = createEpisodeSelector;

@ -1,10 +1,10 @@
import { Filter } from 'App/State/AppState';
export default function getFilterValue(
export default function getFilterValue<T>(
filters: Filter[],
filterKey: string | number,
filterValueKey: string,
defaultValue: string | number | boolean
defaultValue: T
) {
const filter = filters.find((f) => f.key === filterKey);

@ -1,293 +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 ConfirmModal from 'Components/Modal/ConfirmModal';
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 PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
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 getFilterValue from 'Utilities/Filter/getFilterValue';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import CutoffUnmetRowConnector from './CutoffUnmetRowConnector';
function getMonitoredValue(props) {
const {
filters,
selectedFilterKey
} = props;
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
}
class CutoffUnmet extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmSearchAllCutoffUnmetModalOpen: false,
isInteractiveImportModalOpen: false
};
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
});
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onFilterMenuItemPress = (filterKey, filterValue) => {
this.props.onFilterSelect(filterKey, filterValue);
};
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onSearchSelectedPress = () => {
const selected = this.getSelectedIds();
this.props.onSearchSelectedPress(selected);
};
onToggleSelectedPress = () => {
const episodeIds = this.getSelectedIds();
this.props.batchToggleCutoffUnmetEpisodes({
episodeIds,
monitored: !getMonitoredValue(this.props)
});
};
onSearchAllCutoffUnmetPress = () => {
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true });
};
onSearchAllCutoffUnmetConfirmed = () => {
const {
selectedFilterKey,
onSearchAllCutoffUnmetPress
} = this.props;
// TODO: Custom filters will need to check whether there is a monitored
// filter once implemented.
onSearchAllCutoffUnmetPress(selectedFilterKey === 'monitored');
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
};
onConfirmSearchAllCutoffUnmetModalClose = () => {
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
selectedFilterKey,
filters,
columns,
totalRecords,
isSearchingForCutoffUnmetEpisodes,
isSaving,
onFilterSelect,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmSearchAllCutoffUnmetModalOpen
} = this.state;
const itemsSelected = !!this.getSelectedIds().length;
const isShowingMonitored = getMonitoredValue(this.props);
return (
<PageContent title={translate('CutoffUnmet')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
iconName={icons.SEARCH}
isDisabled={isSearchingForCutoffUnmetEpisodes}
isSpinning={isSearchingForCutoffUnmetEpisodes}
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllCutoffUnmetPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={this.onToggleSelectedPress}
/>
</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={[]}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('CutoffUnmetLoadError')}
</Alert>
}
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{translate('CutoffUnmetNoItems')}
</Alert>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<CutoffUnmetRowConnector
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
<ConfirmModal
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
kind={kinds.DANGER}
title={translate('SearchForCutoffUnmetEpisodes')}
message={
<div>
<div>
{translate('SearchForCutoffUnmetEpisodesConfirmationCount', { totalRecords })}
</div>
<div>
{translate('MassSearchCancelWarning')}
</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={this.onSearchAllCutoffUnmetConfirmed}
onCancel={this.onConfirmSearchAllCutoffUnmetModalClose}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
CutoffUnmet.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isSearchingForCutoffUnmetEpisodes: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSearchSelectedPress: PropTypes.func.isRequired,
batchToggleCutoffUnmetEpisodes: PropTypes.func.isRequired,
onSearchAllCutoffUnmetPress: PropTypes.func.isRequired
};
export default CutoffUnmet;

@ -0,0 +1,355 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState, { Filter } from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
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 PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
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 useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import {
batchToggleCutoffUnmetEpisodes,
clearCutoffUnmet,
fetchCutoffUnmet,
gotoCutoffUnmetPage,
setCutoffUnmetFilter,
setCutoffUnmetSort,
setCutoffUnmetTableOption,
} from 'Store/Actions/wantedActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import getFilterValue from 'Utilities/Filter/getFilterValue';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import CutoffUnmetRow from './CutoffUnmetRow';
function getMonitoredValue(
filters: Filter[],
selectedFilterKey: string
): boolean {
return !!getFilterValue(filters, selectedFilterKey, 'monitored', false);
}
function CutoffUnmet() {
const dispatch = useDispatch();
const requestCurrentPage = useCurrentPage();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords = 0,
} = useSelector((state: AppState) => state.wanted.cutoffUnmet);
const isSearchingForAllEpisodes = useSelector(
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH)
);
const isSearchingForSelectedEpisodes = useSelector(
createCommandExecutingSelector(commandNames.EPISODE_SEARCH)
);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] =
useState(false);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoCutoffUnmetPage,
});
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const isSaving = useMemo(() => {
return items.filter((m) => m.isSaving).length > 1;
}, [items]);
const itemsSelected = !!selectedIds.length;
const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey);
const isSearchingForEpisodes =
isSearchingForAllEpisodes || isSearchingForSelectedEpisodes;
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleSearchSelectedPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: selectedIds,
commandFinished: () => {
dispatch(fetchCutoffUnmet());
},
})
);
}, [selectedIds, dispatch]);
const handleSearchAllPress = useCallback(() => {
setIsConfirmSearchAllModalOpen(true);
}, []);
const handleConfirmSearchAllCutoffUnmetModalClose = useCallback(() => {
setIsConfirmSearchAllModalOpen(false);
}, []);
const handleSearchAllCutoffUnmetConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH,
commandFinished: () => {
dispatch(fetchCutoffUnmet());
},
})
);
setIsConfirmSearchAllModalOpen(false);
}, [dispatch]);
const handleToggleSelectedPress = useCallback(() => {
dispatch(
batchToggleCutoffUnmetEpisodes({
episodeIds: selectedIds,
monitored: !isShowingMonitored,
})
);
}, [isShowingMonitored, selectedIds, dispatch]);
const handleFilterSelect = useCallback(
(filterKey: number | string) => {
dispatch(setCutoffUnmetFilter({ selectedFilterKey: filterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setCutoffUnmetSort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setCutoffUnmetTableOption(payload));
if (payload.pageSize) {
dispatch(gotoCutoffUnmetPage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchCutoffUnmet());
} else {
dispatch(gotoCutoffUnmetPage({ page: 1 }));
}
return () => {
dispatch(clearCutoffUnmet());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchCutoffUnmet());
};
registerPagePopulator(repopulate);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
return (
<PageContent title={translate('CutoffUnmet')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={
itemsSelected
? translate('SearchSelected')
: translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForEpisodes}
isSpinning={isSearchingForEpisodes}
onPress={
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={
isShowingMonitored
? translate('UnmonitorSelected')
: translate('MonitorSelected')
}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={handleToggleSelectedPress}
/>
</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={[]}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('CutoffUnmetLoadError')}</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('CutoffUnmetNoItems')}</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<CutoffUnmetRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForCutoffUnmetEpisodes')}
message={
<div>
<div>
{translate(
'SearchForCutoffUnmetEpisodesConfirmationCount',
{ totalRecords }
)}
</div>
<div>{translate('MassSearchCancelWarning')}</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={handleSearchAllCutoffUnmetConfirmed}
onCancel={handleConfirmSearchAllCutoffUnmetModalClose}
/>
</div>
) : null}
</PageContentBody>
</PageContent>
);
}
export default CutoffUnmet;

@ -1,188 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearEpisodeFiles, fetchEpisodeFiles } from 'Store/Actions/episodeFileActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import * as wantedActions from 'Store/Actions/wantedActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import CutoffUnmet from './CutoffUnmet';
function createMapStateToProps() {
return createSelector(
(state) => state.wanted.cutoffUnmet,
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH),
createCommandExecutingSelector(commandNames.EPISODE_SEARCH),
(cutoffUnmet, isSearchingForAllCutoffUnmetEpisodes, isSearchingForSelectedCutoffUnmetEpisodes) => {
return {
isSearchingForCutoffUnmetEpisodes: isSearchingForAllCutoffUnmetEpisodes || isSearchingForSelectedCutoffUnmetEpisodes,
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
...cutoffUnmet
};
}
);
}
const mapDispatchToProps = {
...wantedActions,
executeCommand,
fetchQueueDetails,
clearQueueDetails,
fetchEpisodeFiles,
clearEpisodeFiles
};
class CutoffUnmetConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchCutoffUnmet,
gotoCutoffUnmetFirstPage
} = this.props;
registerPagePopulator(this.repopulate, ['seriesUpdated', 'episodeFileUpdated', 'episodeFileDeleted']);
if (useCurrentPage) {
fetchCutoffUnmet();
} else {
gotoCutoffUnmetFirstPage();
}
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const episodeIds = selectUniqueIds(this.props.items, 'id');
const episodeFileIds = selectUniqueIds(this.props.items, 'episodeFileId');
this.props.fetchQueueDetails({ episodeIds });
if (episodeFileIds.length) {
this.props.fetchEpisodeFiles({ episodeFileIds });
}
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearCutoffUnmet();
this.props.clearQueueDetails();
this.props.clearEpisodeFiles();
}
//
// Control
repopulate = () => {
this.props.fetchCutoffUnmet();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoCutoffUnmetFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoCutoffUnmetPreviousPage();
};
onNextPagePress = () => {
this.props.gotoCutoffUnmetNextPage();
};
onLastPagePress = () => {
this.props.gotoCutoffUnmetLastPage();
};
onPageSelect = (page) => {
this.props.gotoCutoffUnmetPage({ page });
};
onSortPress = (sortKey) => {
this.props.setCutoffUnmetSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setCutoffUnmetFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setCutoffUnmetTableOption(payload);
if (payload.pageSize) {
this.props.gotoCutoffUnmetFirstPage();
}
};
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: selected,
commandFinished: this.repopulate
});
};
onSearchAllCutoffUnmetPress = (monitored) => {
this.props.executeCommand({
name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH,
monitored,
commandFinished: this.repopulate
});
};
//
// Render
render() {
return (
<CutoffUnmet
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onSearchSelectedPress={this.onSearchSelectedPress}
onToggleSelectedPress={this.onToggleSelectedPress}
onSearchAllCutoffUnmetPress={this.onSearchAllCutoffUnmetPress}
{...this.props}
/>
);
}
}
CutoffUnmetConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchCutoffUnmet: PropTypes.func.isRequired,
gotoCutoffUnmetFirstPage: PropTypes.func.isRequired,
gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired,
gotoCutoffUnmetNextPage: PropTypes.func.isRequired,
gotoCutoffUnmetLastPage: PropTypes.func.isRequired,
gotoCutoffUnmetPage: PropTypes.func.isRequired,
setCutoffUnmetSort: PropTypes.func.isRequired,
setCutoffUnmetFilter: PropTypes.func.isRequired,
setCutoffUnmetTableOption: PropTypes.func.isRequired,
clearCutoffUnmet: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired,
fetchEpisodeFiles: PropTypes.func.isRequired,
clearEpisodeFiles: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector)
);

@ -1,187 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import episodeEntities from 'Episode/episodeEntities';
import EpisodeSearchCell from 'Episode/EpisodeSearchCell';
import EpisodeStatus from 'Episode/EpisodeStatus';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import styles from './CutoffUnmetRow.css';
function CutoffUnmetRow(props) {
const {
id,
episodeFileId,
series,
seasonNumber,
episodeNumber,
absoluteEpisodeNumber,
sceneSeasonNumber,
sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber,
unverifiedSceneNumbering,
airDateUtc,
lastSearchTime,
title,
isSelected,
columns,
onSelectedChange
} = props;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'series.sortTitle') {
return (
<TableRowCell key={name}>
<SeriesTitleLink
titleSlug={series.titleSlug}
title={series.title}
/>
</TableRowCell>
);
}
if (name === 'episode') {
return (
<TableRowCell
key={name}
className={styles.episode}
>
<SeasonEpisodeNumber
seasonNumber={seasonNumber}
episodeNumber={episodeNumber}
absoluteEpisodeNumber={absoluteEpisodeNumber}
seriesType={series.seriesType}
alternateTitles={series.alternateTitles}
sceneSeasonNumber={sceneSeasonNumber}
sceneEpisodeNumber={sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
unverifiedSceneNumbering={unverifiedSceneNumbering}
/>
</TableRowCell>
);
}
if (name === 'episodes.title') {
return (
<TableRowCell key={name}>
<EpisodeTitleLink
episodeId={id}
seriesId={series.id}
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
episodeTitle={title}
showOpenSeriesButton={true}
/>
</TableRowCell>
);
}
if (name === 'episodes.airDateUtc') {
return (
<RelativeDateCell
key={name}
date={airDateUtc}
/>
);
}
if (name === 'episodes.lastSearchTime') {
return (
<RelativeDateCell
key={name}
date={lastSearchTime}
includeSeconds={true}
/>
);
}
if (name === 'languages') {
return (
<TableRowCell
key={name}
className={styles.languages}
>
<EpisodeFileLanguages
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<EpisodeStatus
episodeId={id}
episodeFileId={episodeFileId}
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<EpisodeSearchCell
key={name}
episodeId={id}
seriesId={series.id}
episodeTitle={title}
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
showOpenSeriesButton={true}
/>
);
}
return null;
})
}
</TableRow>
);
}
CutoffUnmetRow.propTypes = {
id: PropTypes.number.isRequired,
episodeFileId: PropTypes.number,
series: PropTypes.object.isRequired,
seasonNumber: PropTypes.number.isRequired,
episodeNumber: PropTypes.number.isRequired,
absoluteEpisodeNumber: PropTypes.number,
sceneSeasonNumber: PropTypes.number,
sceneEpisodeNumber: PropTypes.number,
sceneAbsoluteEpisodeNumber: PropTypes.number,
unverifiedSceneNumbering: PropTypes.bool.isRequired,
airDateUtc: PropTypes.string.isRequired,
lastSearchTime: PropTypes.string,
title: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default CutoffUnmetRow;

@ -0,0 +1,171 @@
import React from 'react';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import EpisodeSearchCell from 'Episode/EpisodeSearchCell';
import EpisodeStatus from 'Episode/EpisodeStatus';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import useSeries from 'Series/useSeries';
import { SelectStateInputProps } from 'typings/props';
import styles from './CutoffUnmetRow.css';
interface CutoffUnmetRowProps {
id: number;
seriesId: number;
episodeFileId?: number;
seasonNumber: number;
episodeNumber: number;
absoluteEpisodeNumber?: number;
sceneSeasonNumber?: number;
sceneEpisodeNumber?: number;
sceneAbsoluteEpisodeNumber?: number;
unverifiedSceneNumbering: boolean;
airDateUtc?: string;
lastSearchTime?: string;
title: string;
isSelected?: boolean;
columns: Column[];
onSelectedChange: (options: SelectStateInputProps) => void;
}
function CutoffUnmetRow({
id,
seriesId,
episodeFileId,
seasonNumber,
episodeNumber,
absoluteEpisodeNumber,
sceneSeasonNumber,
sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber,
unverifiedSceneNumbering,
airDateUtc,
lastSearchTime,
title,
isSelected,
columns,
onSelectedChange,
}: CutoffUnmetRowProps) {
const series = useSeries(seriesId);
if (!series || !episodeFileId) {
return null;
}
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'series.sortTitle') {
return (
<TableRowCell key={name}>
<SeriesTitleLink
titleSlug={series.titleSlug}
title={series.title}
/>
</TableRowCell>
);
}
if (name === 'episode') {
return (
<TableRowCell key={name} className={styles.episode}>
<SeasonEpisodeNumber
seasonNumber={seasonNumber}
episodeNumber={episodeNumber}
absoluteEpisodeNumber={absoluteEpisodeNumber}
seriesType={series.seriesType}
alternateTitles={series.alternateTitles}
sceneSeasonNumber={sceneSeasonNumber}
sceneEpisodeNumber={sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
unverifiedSceneNumbering={unverifiedSceneNumbering}
/>
</TableRowCell>
);
}
if (name === 'episodes.title') {
return (
<TableRowCell key={name}>
<EpisodeTitleLink
episodeId={id}
seriesId={series.id}
episodeEntity="wanted.cutoffUnmet"
episodeTitle={title}
showOpenSeriesButton={true}
/>
</TableRowCell>
);
}
if (name === 'episodes.airDateUtc') {
return <RelativeDateCell key={name} date={airDateUtc} />;
}
if (name === 'episodes.lastSearchTime') {
return (
<RelativeDateCell
key={name}
date={lastSearchTime}
includeSeconds={true}
/>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name} className={styles.languages}>
<EpisodeFileLanguages episodeFileId={episodeFileId} />
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell key={name} className={styles.status}>
<EpisodeStatus
episodeId={id}
episodeFileId={episodeFileId}
episodeEntity="wanted.cutoffUnmet"
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<EpisodeSearchCell
key={name}
episodeId={id}
seriesId={series.id}
episodeTitle={title}
episodeEntity="wanted.cutoffUnmet"
showOpenSeriesButton={true}
/>
);
}
return null;
})}
</TableRow>
);
}
export default CutoffUnmetRow;

@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import CutoffUnmetRow from './CutoffUnmetRow';
function createMapStateToProps() {
return createSelector(
createSeriesSelector(),
(series) => {
return {
series
};
}
);
}
export default connect(createMapStateToProps)(CutoffUnmetRow);
Loading…
Cancel
Save