Initial Commit Rework

This commit is contained in:
Qstick 2017-09-03 22:20:56 -04:00
parent 74a4cc048c
commit 95051cbd63
2483 changed files with 101351 additions and 111396 deletions

View file

@ -0,0 +1,285 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
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 { align, icons, kinds } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import MenuContent from 'Components/Menu/MenuContent';
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import CutoffUnmetRow from './CutoffUnmetRow';
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);
});
}s;
}
//
// 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 selected = this.getSelectedIds();
this.props.onToggleSelectedPress(selected);
}
onSearchAllCutoffUnmetPress = () => {
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true });
}
onSearchAllCutoffUnmetConfirmed = () => {
this.props.onSearchAllCutoffUnmetPress();
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
}
onConfirmSearchAllCutoffUnmetModalClose = () => {
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isSearchingForEpisodes,
isSearchingForCutoffUnmetEpisodes,
isSaving,
filterKey,
filterValue,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmSearchAllCutoffUnmetModalOpen
} = this.state;
const itemsSelected = !!this.getSelectedIds().length;
return (
<PageContent title="Cutoff Unmet">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Search Selected"
iconName={icons.SEARCH}
isDisabled={!itemsSelected}
isSpinning={isSearchingForEpisodes}
onPress={this.onSearchSelectedPress}
/>
<PageToolbarButton
label={filterKey === 'monitored' && filterValue ? 'Unmonitor Selected' : 'Monitor Selected'}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={this.onToggleSelectedPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="Search All"
iconName={icons.SEARCH}
isSpinning={isSearchingForCutoffUnmetEpisodes}
onPress={this.onSearchAllCutoffUnmetPress}
/>
<PageToolbarSeparator />
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<FilterMenu alignMenu={align.RIGHT}>
<MenuContent>
<FilterMenuItem
name="monitored"
value={true}
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
Monitored
</FilterMenuItem>
<FilterMenuItem
name="monitored"
value={false}
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
Unmonitored
</FilterMenuItem>
</MenuContent>
</FilterMenu>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && error &&
<div>
Error fetching cutoff unmet
</div>
}
{
isPopulated && !error && !items.length &&
<div>
No cutoff unmet items
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<CutoffUnmetRow
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="Search for all Cutoff Unmet episodes"
message={
<div>
<div>
Are you sure you want to search for all {totalRecords} Cutoff Unmet episodes?
</div>
<div>
This cannot be cancelled once started without restarting Sonarr.
</div>
</div>
}
confirmLabel="Search"
onConfirm={this.onSearchAllCutoffUnmetConfirmed}
onCancel={this.onConfirmSearchAllCutoffUnmetModalClose}
/>
</div>
}
</PageContentBodyConnector>
</PageContent>
);
}
}
CutoffUnmet.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isSearchingForEpisodes: PropTypes.bool.isRequired,
isSearchingForCutoffUnmetEpisodes: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
filterKey: PropTypes.string,
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
onFilterSelect: PropTypes.func.isRequired,
onSearchSelectedPress: PropTypes.func.isRequired,
onToggleSelectedPress: PropTypes.func.isRequired,
onSearchAllCutoffUnmetPress: PropTypes.func.isRequired
};
export default CutoffUnmet;

View file

@ -0,0 +1,180 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import * as wantedActions from 'Store/Actions/wantedActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
import * as commandNames from 'Commands/commandNames';
import CutoffUnmet from './CutoffUnmet';
function createMapStateToProps() {
return createSelector(
(state) => state.wanted.cutoffUnmet,
createCommandsSelector(),
(cutoffUnmet, commands) => {
const isSearchingForEpisodes = _.some(commands, { name: commandNames.EPISODE_SEARCH });
const isSearchingForCutoffUnmetEpisodes = _.some(commands, { name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH });
return {
isSearchingForEpisodes,
isSearchingForCutoffUnmetEpisodes,
isSaving: _.some(cutoffUnmet.items, { isSaving: true }),
...cutoffUnmet
};
}
);
}
const mapDispatchToProps = {
...wantedActions,
executeCommand,
fetchQueueDetails,
clearQueueDetails,
fetchEpisodeFiles,
clearEpisodeFiles
};
class CutoffUnmetConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.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() {
this.props.clearCutoffUnmet();
this.props.clearQueueDetails();
this.props.clearEpisodeFiles();
}
//
// 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 = (filterKey, filterValue) => {
this.props.setCutoffUnmetFilter({ filterKey, filterValue });
}
onTableOptionChange = (payload) => {
this.props.setCutoffUnmetTableOption(payload);
if (payload.pageSize) {
this.props.gotoCutoffUnmetFirstPage();
}
}
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: selected
});
}
onToggleSelectedPress = (selected) => {
const {
filterKey,
filterValue
} = this.props;
this.props.batchToggleCutoffUnmetEpisodes({
episodeIds: selected,
monitored: filterKey !== 'monitored' || !filterValue
});
}
onSearchAllCutoffUnmetPress = () => {
this.props.executeCommand({
name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH
});
}
//
// 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 = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
filterKey: PropTypes.string.isRequired,
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
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,
batchToggleCutoffUnmetEpisodes: 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 connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector);

View file

@ -0,0 +1,7 @@
.episode,
.language,
.status {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}

View file

@ -0,0 +1,169 @@
import PropTypes from 'prop-types';
import React from 'react';
import episodeEntities from 'Episode/episodeEntities';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
import ArtistNameLink from 'Artist/ArtistNameLink';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import styles from './CutoffUnmetRow.css';
function CutoffUnmetRow(props) {
const {
id,
episodeFileId,
series,
seasonNumber,
episodeNumber,
absoluteEpisodeNumber,
sceneSeasonNumber,
sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber,
airDateUtc,
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}>
<ArtistNameLink
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}
sceneSeasonNumber={sceneSeasonNumber}
sceneEpisodeNumber={sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
/>
</TableRowCell>
);
}
if (name === 'episodeTitle') {
return (
<TableRowCell key={name}>
<EpisodeTitleLink
episodeId={id}
artistId={series.id}
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
episodeTitle={title}
showOpenSeriesButton={true}
/>
</TableRowCell>
);
}
if (name === 'airDateUtc') {
return (
<RelativeDateCellConnector
key={name}
date={airDateUtc}
/>
);
}
if (name === 'language') {
return (
<TableRowCell
key={name}
className={styles.language}
>
<EpisodeFileLanguageConnector
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<EpisodeStatusConnector
episodeId={id}
episodeFileId={episodeFileId}
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<EpisodeSearchCellConnector
key={name}
episodeId={id}
artistId={series.id}
episodeTitle={title}
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
showOpenSeriesButton={true}
/>
);
}
})
}
</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,
airDateUtc: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default CutoffUnmetRow;

View file

@ -0,0 +1,307 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
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 { align, icons, kinds } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import MenuContent from 'Components/Menu/MenuContent';
import FilterMenuItem from 'Components/Menu/FilterMenuItem';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MissingRow from './MissingRow';
class Missing extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmSearchAllMissingModalOpen: 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 selected = this.getSelectedIds();
this.props.onToggleSelectedPress(selected);
}
onSearchAllMissingPress = () => {
this.setState({ isConfirmSearchAllMissingModalOpen: true });
}
onSearchAllMissingConfirmed = () => {
this.props.onSearchAllMissingPress();
this.setState({ isConfirmSearchAllMissingModalOpen: false });
}
onConfirmSearchAllMissingModalClose = () => {
this.setState({ isConfirmSearchAllMissingModalOpen: false });
}
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
}
onInteractiveImportModalClose = () => {
this.setState({ isInteractiveImportModalOpen: false });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isSearchingForEpisodes,
isSearchingForMissingEpisodes,
isSaving,
filterKey,
filterValue,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmSearchAllMissingModalOpen,
isInteractiveImportModalOpen
} = this.state;
const itemsSelected = !!this.getSelectedIds().length;
return (
<PageContent title="Missing">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Search Selected"
iconName={icons.SEARCH}
isDisabled={!itemsSelected}
isSpinning={isSearchingForEpisodes}
onPress={this.onSearchSelectedPress}
/>
<PageToolbarButton
label={filterKey === 'monitored' && filterValue ? 'Unmonitor Selected' : 'Monitor Selected'}
iconName={icons.MONITORED}
isDisabled={!itemsSelected}
isSpinning={isSaving}
onPress={this.onToggleSelectedPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="Search All"
iconName={icons.SEARCH}
isSpinning={isSearchingForMissingEpisodes}
onPress={this.onSearchAllMissingPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="Manual Import"
iconName={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<FilterMenu alignMenu={align.RIGHT}>
<MenuContent>
<FilterMenuItem
name="monitored"
value={true}
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
Monitored
</FilterMenuItem>
<FilterMenuItem
name="monitored"
value={false}
filterKey={filterKey}
filterValue={filterValue}
onPress={this.onFilterMenuItemPress}
>
Unmonitored
</FilterMenuItem>
</MenuContent>
</FilterMenu>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && error &&
<div>
Error fetching missing items
</div>
}
{
isPopulated && !error && !items.length &&
<div>
No missing items
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<MissingRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
<ConfirmModal
isOpen={isConfirmSearchAllMissingModalOpen}
kind={kinds.DANGER}
title="Search for all missing episodes"
message={
<div>
<div>
Are you sure you want to search for all {totalRecords} missing episodes?
</div>
<div>
This cannot be cancelled once started without restarting Sonarr.
</div>
</div>
}
confirmLabel="Search"
onConfirm={this.onSearchAllMissingConfirmed}
onCancel={this.onConfirmSearchAllMissingModalClose}
/>
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
onModalClose={this.onInteractiveImportModalClose}
/>
</div>
}
</PageContentBodyConnector>
</PageContent>
);
}
}
Missing.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isSearchingForEpisodes: PropTypes.bool.isRequired,
isSearchingForMissingEpisodes: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
filterKey: PropTypes.string,
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
onFilterSelect: PropTypes.func.isRequired,
onSearchSelectedPress: PropTypes.func.isRequired,
onToggleSelectedPress: PropTypes.func.isRequired,
onSearchAllMissingPress: PropTypes.func.isRequired
};
export default Missing;

View file

@ -0,0 +1,168 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import * as wantedActions from 'Store/Actions/wantedActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
import * as commandNames from 'Commands/commandNames';
import Missing from './Missing';
function createMapStateToProps() {
return createSelector(
(state) => state.wanted.missing,
createCommandsSelector(),
(missing, commands) => {
const isSearchingForEpisodes = _.some(commands, { name: commandNames.EPISODE_SEARCH });
const isSearchingForMissingEpisodes = _.some(commands, { name: commandNames.MISSING_EPISODE_SEARCH });
return {
isSearchingForEpisodes,
isSearchingForMissingEpisodes,
isSaving: _.some(missing.items, { isSaving: true }),
...missing
};
}
);
}
const mapDispatchToProps = {
...wantedActions,
executeCommand,
fetchQueueDetails,
clearQueueDetails
};
class MissingConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.gotoMissingFirstPage();
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const episodeIds = selectUniqueIds(this.props.items, 'id');
this.props.fetchQueueDetails({ episodeIds });
}
}
componentWillUnmount() {
this.props.clearMissing();
this.props.clearQueueDetails();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoMissingFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoMissingPreviousPage();
}
onNextPagePress = () => {
this.props.gotoMissingNextPage();
}
onLastPagePress = () => {
this.props.gotoMissingLastPage();
}
onPageSelect = (page) => {
this.props.gotoMissingPage({ page });
}
onSortPress = (sortKey) => {
this.props.setMissingSort({ sortKey });
}
onFilterSelect = (filterKey, filterValue) => {
this.props.setMissingFilter({ filterKey, filterValue });
}
onTableOptionChange = (payload) => {
this.props.setMissingTableOption(payload);
if (payload.pageSize) {
this.props.gotoMissingFirstPage();
}
}
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: selected
});
}
onToggleSelectedPress = (selected) => {
const {
filterKey,
filterValue
} = this.props;
this.props.batchToggleMissingEpisodes({
episodeIds: selected,
monitored: filterKey !== 'monitored' || !filterValue
});
}
onSearchAllMissingPress = () => {
this.props.executeCommand({
name: commandNames.MISSING_EPISODE_SEARCH
});
}
//
// Render
render() {
return (
<Missing
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}
onSearchAllMissingPress={this.onSearchAllMissingPress}
{...this.props}
/>
);
}
}
MissingConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
filterKey: PropTypes.string.isRequired,
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
fetchMissing: PropTypes.func.isRequired,
gotoMissingFirstPage: PropTypes.func.isRequired,
gotoMissingPreviousPage: PropTypes.func.isRequired,
gotoMissingNextPage: PropTypes.func.isRequired,
gotoMissingLastPage: PropTypes.func.isRequired,
gotoMissingPage: PropTypes.func.isRequired,
setMissingSort: PropTypes.func.isRequired,
setMissingFilter: PropTypes.func.isRequired,
setMissingTableOption: PropTypes.func.isRequired,
clearMissing: PropTypes.func.isRequired,
batchToggleMissingEpisodes: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MissingConnector);

View file

@ -0,0 +1,6 @@
.episode,
.status {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 100px;
}

View file

@ -0,0 +1,155 @@
import PropTypes from 'prop-types';
import React from 'react';
import episodeEntities from 'Episode/episodeEntities';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
import ArtistNameLink from 'Artist/ArtistNameLink';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import styles from './MissingRow.css';
function MissingRow(props) {
const {
id,
episodeFileId,
series,
seasonNumber,
episodeNumber,
absoluteEpisodeNumber,
sceneSeasonNumber,
sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber,
airDateUtc,
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}>
<ArtistNameLink
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}
sceneSeasonNumber={sceneSeasonNumber}
sceneEpisodeNumber={sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
/>
</TableRowCell>
);
}
if (name === 'episodeTitle') {
return (
<TableRowCell key={name}>
<EpisodeTitleLink
episodeId={id}
artistId={series.id}
episodeEntity={episodeEntities.WANTED_MISSING}
episodeTitle={title}
showOpenSeriesButton={true}
/>
</TableRowCell>
);
}
if (name === 'airDateUtc') {
return (
<RelativeDateCellConnector
key={name}
date={airDateUtc}
/>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<EpisodeStatusConnector
episodeId={id}
episodeFileId={episodeFileId}
episodeEntity={episodeEntities.WANTED_MISSING}
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<EpisodeSearchCellConnector
key={name}
episodeId={id}
artistId={series.id}
episodeTitle={title}
episodeEntity={episodeEntities.WANTED_MISSING}
showOpenSeriesButton={true}
/>
);
}
})
}
</TableRow>
);
}
MissingRow.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,
airDateUtc: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default MissingRow;