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,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectEpisodeModalContentConnector from './SelectEpisodeModalContentConnector';
class SelectEpisodeModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectEpisodeModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectEpisodeModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectEpisodeModal;

View file

@ -0,0 +1,183 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SelectEpisodeRow from './SelectEpisodeRow';
const columns = [
{
name: 'episodeNumber',
label: '#',
isSortable: true,
isVisible: true
},
{
name: 'title',
label: 'Title',
isVisible: true
},
{
name: 'airDate',
label: 'Air Date',
isVisible: true
}
];
class SelectEpisodeModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
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);
});
}
onEpisodesSelect = () => {
this.props.onEpisodesSelect(this.getSelectedIds());
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
sortKey,
sortDirection,
onSortPress,
onModalClose
} = this.props;
const {
allSelected,
allUnselected,
selectedState
} = this.state;
const errorMessage = error && error.message || 'Unable to load episodes';
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Episode(s)
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
error &&
<div>{errorMessage}</div>
}
{
isPopulated && !!items.length &&
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<SelectEpisodeRow
key={item.id}
id={item.id}
episodeNumber={item.episodeNumber}
title={item.title}
airDate={item.airDate}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
}
{
isPopulated && !items.length &&
'No episodes were found for the selected season'
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onEpisodesSelect}
>
Select Episodes
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectEpisodeModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
onEpisodesSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectEpisodeModalContent;

View file

@ -0,0 +1,103 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import { fetchEpisodes, setEpisodesSort, clearEpisodes } from 'Store/Actions/episodeActions';
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import SelectEpisodeModalContent from './SelectEpisodeModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector(),
(episodes) => {
return episodes;
}
);
}
const mapDispatchToProps = {
fetchEpisodes,
setEpisodesSort,
clearEpisodes,
updateInteractiveImportItem
};
class SelectEpisodeModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
artistId,
seasonNumber
} = this.props;
this.props.fetchEpisodes({ artistId, seasonNumber });
}
componentWillUnmount() {
// This clears the episodes for the queue and hides the queue
// We'll need another place to store episodes for manual import
this.props.clearEpisodes();
}
//
// Listeners
onSortPress = (sortKey, sortDirection) => {
this.props.setEpisodesSort({ sortKey, sortDirection });
}
onEpisodesSelect = (episodeIds) => {
const episodes = _.reduce(this.props.items, (acc, item) => {
if (episodeIds.indexOf(item.id) > -1) {
acc.push(item);
}
return acc;
}, []);
this.props.updateInteractiveImportItem({
id: this.props.id,
episodes: _.sortBy(episodes, 'episodeNumber')
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectEpisodeModalContent
{...this.props}
onSortPress={this.onSortPress}
onEpisodesSelect={this.onEpisodesSelect}
/>
);
}
}
SelectEpisodeModalContentConnector.propTypes = {
id: PropTypes.number.isRequired,
artistId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchEpisodes: PropTypes.func.isRequired,
setEpisodesSort: PropTypes.func.isRequired,
clearEpisodes: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'episodes' }
)(SelectEpisodeModalContentConnector);

View file

@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TableRowButton from 'Components/Table/TableRowButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
class SelectEpisodeRow extends Component {
//
// Listeners
onPress = () => {
const {
id,
isSelected
} = this.props;
this.props.onSelectedChange({ id, value: !isSelected });
}
//
// Render
render() {
const {
id,
episodeNumber,
title,
airDate,
isSelected,
onSelectedChange
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell>
{episodeNumber}
</TableRowCell>
<TableRowCell>
{title}
</TableRowCell>
<TableRowCell>
{airDate}
</TableRowCell>
</TableRowButton>
);
}
}
SelectEpisodeRow.propTypes = {
id: PropTypes.number.isRequired,
episodeNumber: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
airDate: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
export default SelectEpisodeRow;

View file

@ -0,0 +1,24 @@
.recentFoldersContainer {
margin-top: 15px;
}
.buttonsContainer {
margin-top: 30px;
}
.buttonContainer {
display: flex;
justify-content: center;
margin-top: 10px;
}
.button {
composes: button from 'Components/Link/Button.css';
width: 300px;
}
.buttonIcon {
margin-right: 5px;
}

View file

@ -0,0 +1,161 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
import PathInputConnector from 'Components/Form/PathInputConnector';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css';
const recentFoldersColumns = [
{
name: 'folder',
label: 'Folder'
},
{
name: 'lastUsed',
label: 'Last Used'
}
];
class InteractiveImportSelectFolderModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
folder: ''
};
}
//
// Listeners
onPathChange = ({ value }) => {
this.setState({ folder: value });
}
onRecentPathPress = (folder) => {
this.setState({ folder });
}
onQuickImportPress = () => {
this.props.onQuickImportPress(this.state.folder);
}
onInteractiveImportPress = () => {
this.props.onInteractiveImportPress(this.state.folder);
}
//
// Render
render() {
const {
recentFolders,
onModalClose
} = this.props;
const folder = this.state.folder;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Folder
</ModalHeader>
<ModalBody>
<PathInputConnector
name="folder"
value={folder}
onChange={this.onPathChange}
/>
{
!!recentFolders.length &&
<div className={styles.recentFoldersContainer}>
<Table
columns={recentFoldersColumns}
>
<TableBody>
{
recentFolders.map((recentFolder) => {
return (
<RecentFolderRow
key={recentFolder.folder}
folder={recentFolder.folder}
lastUsed={recentFolder.lastUsed}
onPress={this.onRecentPathPress}
/>
);
})
}
</TableBody>
</Table>
</div>
}
<div className={styles.buttonsContainer}>
<div className={styles.buttonContainer}>
<Button
className={styles.button}
kind={kinds.PRIMARY}
size={sizes.LARGE}
isDisabled={!folder}
onPress={this.onQuickImportPress}
>
<Icon
className={styles.buttonIcon}
name={icons.QUICK}
/>
Quick Import
</Button>
</div>
<div className={styles.buttonContainer}>
<Button
className={styles.button}
kind={kinds.PRIMARY}
size={sizes.LARGE}
isDisabled={!folder}
onPress={this.onInteractiveImportPress}
>
<Icon
className={styles.buttonIcon}
name={icons.INTERACTIVE}
/>
Interactive Import
</Button>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
);
}
}
InteractiveImportSelectFolderModalContent.propTypes = {
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onQuickImportPress: PropTypes.func.isRequired,
onInteractiveImportPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default InteractiveImportSelectFolderModalContent;

View file

@ -0,0 +1,73 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import InteractiveImportSelectFolderModalContent from './InteractiveImportSelectFolderModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.interactiveImport.recentFolders,
(recentFolders) => {
return {
recentFolders
};
}
);
}
const mapDispatchToProps = {
addRecentFolder,
executeCommand
};
class InteractiveImportSelectFolderModalContentConnector extends Component {
//
// Listeners
onQuickImportPress = (folder) => {
this.props.addRecentFolder({ folder });
this.props.executeCommand({
name: commandNames.DOWNLOADED_EPSIODES_SCAN,
path: folder
});
this.props.onModalClose();
}
onInteractiveImportPress = (folder) => {
this.props.addRecentFolder({ folder });
this.props.onFolderSelect(folder);
}
//
// Render
render() {
if (this.path) {
return null;
}
return (
<InteractiveImportSelectFolderModalContent
{...this.props}
onQuickImportPress={this.onQuickImportPress}
onInteractiveImportPress={this.onInteractiveImportPress}
/>
);
}
}
InteractiveImportSelectFolderModalContentConnector.propTypes = {
path: PropTypes.string,
onFolderSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
addRecentFolder: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportSelectFolderModalContentConnector);

View file

@ -0,0 +1,41 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TableRowButton from 'Components/Table/TableRowButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
class RecentFolderRow extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.folder);
}
//
// Render
render() {
const {
folder,
lastUsed
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCellConnector date={lastUsed} />
</TableRowButton>
);
}
}
RecentFolderRow.propTypes = {
folder: PropTypes.string.isRequired,
lastUsed: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default RecentFolderRow;

View file

@ -0,0 +1,45 @@
.footer {
composes: modalFooter from 'Components/Modal/ModalFooter.css';
justify-content: space-between;
padding: 15px;
}
.leftButtons,
.centerButtons,
.rightButtons {
display: flex;
flex: 1 0 33%;
flex-wrap: wrap;
}
.centerButtons {
justify-content: center;
}
.rightButtons {
justify-content: flex-end;
}
.importMode {
composes: select from 'Components/Form/SelectInput.css';
width: auto;
}
.errorMessage {
color: $dangerColor;
}
@media only screen and (max-width: $breakpointSmall) {
.footer {
a,
button {
margin-left: 0;
&:first-child {
margin-bottom: 5px;
}
}
}
}

View file

@ -0,0 +1,320 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { icons, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectInput from 'Components/Form/SelectInput';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
const columns = [
{
name: 'relativePath',
label: 'Relative Path',
isSortable: true,
isVisible: true
},
{
name: 'series',
label: 'Series',
isSortable: true,
isVisible: true
},
{
name: 'season',
label: 'Season',
isVisible: true
},
{
name: 'episodes',
label: 'Episode(s)',
isVisible: true
},
{
name: 'quality',
label: 'Quality',
isSortable: true,
isVisible: true
},
{
name: 'size',
label: 'Size',
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER
}),
isVisible: true
}
];
class InteractiveImportModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
invalidRowsSelected: [],
isSelectSeriesModalOpen: false,
isSelectSeasonModalOpen: false
};
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
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);
});
}
onValidRowChange = (id, isValid) => {
this.setState((state) => {
if (isValid) {
return {
invalidRowsSelected: _.without(state.invalidRowsSelected, id)
};
}
return {
invalidRowsSelected: [...state.invalidRowsSelected, id]
};
});
}
onImportSelectedPress = () => {
const selected = this.getSelectedIds();
this.props.onImportSelectedPress(selected, this.state.importMode);
}
onImportModeChange = ({ value }) => {
this.props.onImportModeChange(value);
}
onSelectSeriesPress = () => {
this.setState({ isSelectSeriesModalOpen: true });
}
onSelectSeasonPress = () => {
this.setState({ isSelectSeasonModalOpen: true });
}
onSelectSeriesModalClose = () => {
this.setState({ isSelectSeriesModalOpen: false });
}
onSelectSeasonModalClose = () => {
this.setState({ isSelectSeasonModalOpen: false });
}
//
// Render
render() {
const {
downloadId,
title,
folder,
isFetching,
isPopulated,
error,
items,
sortKey,
sortDirection,
importMode,
interactiveImportErrorMessage,
onSortPress,
onModalClose
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
invalidRowsSelected,
isSelectSeriesModalOpen,
isSelectSeasonModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null;
const errorMessage = error && error.message || 'Unable to load manual import items';
const importModeOptions = [
{ key: 'move', value: 'Move Files' },
{ key: 'copy', value: 'Copy Files' }
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - {title || folder}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
error &&
<div>{errorMessage}</div>
}
{
isPopulated && !!items.length &&
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<InteractiveImportRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange}
/>
);
})
}
</TableBody>
</Table>
}
{
isPopulated && !items.length &&
'No video files were found in the selected folder'
}
</ModalBody>
<ModalFooter className={styles.footer}>
{
!downloadId &&
<div className={styles.leftButtons}>
<SelectInput
className={styles.importMode}
name="importMode"
value={importMode}
values={importModeOptions}
onChange={this.onImportModeChange}
/>
</div>
}
<div className={downloadId ? styles.leftButtons : styles.centerButtons}>
<Button onPress={this.onSelectSeriesPress}>
Select Series
</Button>
<Button onPress={this.onSelectSeasonPress}>
Select Season
</Button>
</div>
<div className={styles.rightButtons}>
<Button onPress={onModalClose}>
Cancel
</Button>
{
interactiveImportErrorMessage &&
<span className={styles.errorMessage}>{interactiveImportErrorMessage}</span>
}
<Button
kind={kinds.SUCCESS}
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
onPress={this.onImportSelectedPress}
>
Import
</Button>
</div>
</ModalFooter>
<SelectSeriesModal
isOpen={isSelectSeriesModalOpen}
ids={selectedIds}
onModalClose={this.onSelectSeriesModalClose}
/>
<SelectSeasonModal
isOpen={isSelectSeasonModalOpen}
ids={selectedIds}
artistId={selectedItem && selectedItem.series && selectedItem.series.id}
onModalClose={this.onSelectSeasonModalClose}
/>
</ModalContent>
);
}
}
InteractiveImportModalContent.propTypes = {
downloadId: PropTypes.string,
importMode: PropTypes.string.isRequired,
title: PropTypes.string,
folder: PropTypes.string,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
interactiveImportErrorMessage: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
onImportModeChange: PropTypes.func.isRequired,
onImportSelectedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
InteractiveImportModalContent.defaultProps = {
importMode: 'move'
};
export default InteractiveImportModalContent;

View file

@ -0,0 +1,152 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import InteractiveImportModalContent from './InteractiveImportModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector(),
(interactiveImport) => {
return interactiveImport;
}
);
}
const mapDispatchToProps = {
fetchInteractiveImportItems,
setInteractiveImportSort,
setInteractiveImportMode,
clearInteractiveImport,
executeCommand
};
class InteractiveImportModalContentConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
interactiveImportErrorMessage: null
};
}
componentDidMount() {
const {
downloadId,
folder
} = this.props;
this.props.fetchInteractiveImportItems({ downloadId, folder });
}
componentWillUnmount() {
this.props.clearInteractiveImport();
}
//
// Listeners
onSortPress = (sortKey, sortDirection) => {
this.props.setInteractiveImportSort({ sortKey, sortDirection });
}
onImportModeChange = (importMode) => {
this.props.setInteractiveImportMode({ importMode });
}
onImportSelectedPress = (selected, importMode) => {
const files = [];
_.forEach(this.props.items, (item) => {
const isSelected = selected.indexOf(item.id) > -1;
if (isSelected) {
const {
series,
seasonNumber,
episodes,
quality
} = item;
if (!series) {
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
return false;
}
if (isNaN(seasonNumber)) {
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
return false;
}
if (!episodes || !episodes.length) {
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
return false;
}
files.push({
path: item.path,
artistId: series.id,
episodeIds: _.map(episodes, 'id'),
quality,
downloadId: this.props.downloadId
});
}
});
if (!files.length) {
return;
}
this.props.executeCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode
});
this.props.onModalClose();
}
//
// Render
render() {
return (
<InteractiveImportModalContent
{...this.props}
interactiveImportErrorMessage={this.state.interactiveImportErrorMessage}
onSortPress={this.onSortPress}
onImportModeChange={this.onImportModeChange}
onImportSelectedPress={this.onImportSelectedPress}
/>
);
}
}
InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string,
folder: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchInteractiveImportItems: PropTypes.func.isRequired,
setInteractiveImportSort: PropTypes.func.isRequired,
clearInteractiveImport: PropTypes.func.isRequired,
setInteractiveImportMode: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'interactiveImport' }
)(InteractiveImportModalContentConnector);

View file

@ -0,0 +1,11 @@
.relativePath {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}
.quality {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
text-align: center;
}

View file

@ -0,0 +1,294 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Popover from 'Components/Tooltip/Popover';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
class InteractiveImportRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isSelectSeriesModalOpen: false,
isSelectSeasonModalOpen: false,
isSelectEpisodeModalOpen: false,
isSelectQualityModalOpen: false
};
}
componentDidMount() {
const {
id,
series,
seasonNumber,
episodes,
quality
} = this.props;
if (series && seasonNumber !== undefined && episodes.length && quality) {
this.props.onSelectedChange({ id, value: true });
}
}
componentDidUpdate(prevProps) {
const {
id,
series,
seasonNumber,
episodes,
quality,
isSelected,
onValidRowChange
} = this.props;
if (prevProps.isSelected === isSelected) {
return;
}
const isValid = !!(series && seasonNumber != null && episodes.length && quality);
if (isSelected && !isValid) {
onValidRowChange(id, false);
} else {
onValidRowChange(id, true);
}
}
//
// Control
selectRowAfterChange = (value) => {
const {
id,
isSelected
} = this.props;
if (!isSelected && value === true) {
this.props.onSelectedChange({ id, value });
}
}
//
// Listeners
onSelectSeriesPress = () => {
this.setState({ isSelectSeriesModalOpen: true });
}
onSelectSeasonPress = () => {
this.setState({ isSelectSeasonModalOpen: true });
}
onSelectEpisodePress = () => {
this.setState({ isSelectEpisodeModalOpen: true });
}
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
onSelectSeriesModalClose = (changed) => {
this.setState({ isSelectSeriesModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectSeasonModalClose = (changed) => {
this.setState({ isSelectSeasonModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectEpisodeModalClose = (changed) => {
this.setState({ isSelectEpisodeModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
}
//
// Render
render() {
const {
id,
relativePath,
series,
seasonNumber,
episodes,
quality,
size,
rejections,
isSelected,
onSelectedChange
} = this.props;
const {
isSelectSeriesModalOpen,
isSelectSeasonModalOpen,
isSelectEpisodeModalOpen,
isSelectQualityModalOpen
} = this.state;
const seriesTitle = series ? series.title : '';
const episodeNumbers = episodes.map((episode) => episode.episodeNumber)
.join(', ');
const showSeriesPlaceholder = isSelected && !series;
const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber);
const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell
className={styles.relativePath}
title={relativePath}
>
{relativePath}
</TableRowCell>
<TableRowCellButton
onPress={this.onSelectSeriesPress}
>
{
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
}
</TableRowCellButton>
<TableRowCellButton
isDisabled={!series}
onPress={this.onSelectSeasonPress}
>
{
showSeasonNumberPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seasonNumber
}
</TableRowCellButton>
<TableRowCellButton
isDisabled={!series || isNaN(seasonNumber)}
onPress={this.onSelectEpisodePress}
>
{
showEpisodeNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : episodeNumbers
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
onPress={this.onSelectQualityPress}
>
<EpisodeQuality
quality={quality}
/>
</TableRowCellButton>
<TableRowCell>
{formatBytes(size)}
</TableRowCell>
<TableRowCell>
{
!!rejections.length &&
<Popover
anchor={
<Icon
name={icons.DANGER}
kind={kinds.DANGER}
/>
}
title="Release Rejected"
body={
<ul>
{
rejections.map((rejection, index) => {
return (
<li key={index}>
{rejection.reason}
</li>
);
})
}
</ul>
}
position={tooltipPositions.LEFT}
/>
}
</TableRowCell>
<SelectSeriesModal
isOpen={isSelectSeriesModalOpen}
ids={[id]}
onModalClose={this.onSelectSeriesModalClose}
/>
<SelectSeasonModal
isOpen={isSelectSeasonModalOpen}
ids={[id]}
artistId={series && series.id}
onModalClose={this.onSelectSeasonModalClose}
/>
<SelectEpisodeModal
isOpen={isSelectEpisodeModalOpen}
id={id}
artistId={series && series.id}
seasonNumber={seasonNumber}
onModalClose={this.onSelectEpisodeModalClose}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
id={id}
qualityId={quality.quality.id}
proper={quality.revision.version > 1}
real={quality.revision.real > 0}
onModalClose={this.onSelectQualityModalClose}
/>
</TableRow>
);
}
}
InteractiveImportRow.propTypes = {
id: PropTypes.number.isRequired,
relativePath: PropTypes.string.isRequired,
series: PropTypes.object,
seasonNumber: PropTypes.number,
episodes: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onValidRowChange: PropTypes.func.isRequired
};
InteractiveImportRow.defaultProps = {
episodes: []
};
export default InteractiveImportRow;

View file

@ -0,0 +1,7 @@
.placeholder {
display: inline-block;
margin: -8px 0;
width: 100%;
height: 25px;
border: 2px dashed $dangerColor;
}

View file

@ -0,0 +1,10 @@
import React from 'react';
import styles from './InteractiveImportRowCellPlaceholder.css';
function InteractiveImportRowCellPlaceholder() {
return (
<span className={styles.placeholder}></span>
);
}
export default InteractiveImportRowCellPlaceholder;

View file

@ -0,0 +1,78 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import InteractiveImportSelectFolderModalContentConnector from './Folder/InteractiveImportSelectFolderModalContentConnector';
import InteractiveImportModalContentConnector from './Interactive/InteractiveImportModalContentConnector';
class InteractiveImportModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
folder: null
};
}
componentDidUpdate(prevProps) {
if (prevProps.isOpen && !this.props.isOpen) {
this.setState({ folder: null });
}
}
//
// Listeners
onFolderSelect = (folder) => {
this.setState({ folder });
}
//
// Render
render() {
const {
isOpen,
folder,
downloadId,
onModalClose,
...otherProps
} = this.props;
const folderPath = folder || this.state.folder;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
{
folderPath || downloadId ?
<InteractiveImportModalContentConnector
folder={folderPath}
downloadId={downloadId}
{...otherProps}
onModalClose={onModalClose}
/> :
<InteractiveImportSelectFolderModalContentConnector
{...otherProps}
onFolderSelect={this.onFolderSelect}
onModalClose={onModalClose}
/>
}
</Modal>
);
}
}
InteractiveImportModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
folder: PropTypes.string,
downloadId: PropTypes.string,
onModalClose: PropTypes.func.isRequired
};
export default InteractiveImportModal;

View file

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectQualityModalContentConnector from './SelectQualityModalContentConnector';
class SelectQualityModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectQualityModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectQualityModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectQualityModal;

View file

@ -0,0 +1,166 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
class SelectQualityModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
qualityId,
proper,
real
} = props;
this.state = {
qualityId,
proper,
real
};
}
//
// Listeners
onQualityChange = ({ value }) => {
this.setState({ qualityId: parseInt(value) });
}
onProperChange = ({ value }) => {
this.setState({ proper: value });
}
onRealChange = ({ value }) => {
this.setState({ real: value });
}
onQualitySelect = () => {
this.props.onQualitySelect(this.state);
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onModalClose
} = this.props;
const {
qualityId,
proper,
real
} = this.state;
const qualityOptions = items.map(({ quality }) => {
return {
key: quality.id,
value: quality.name
};
});
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Quality
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load qualities</div>
}
{
isPopulated && !error &&
<Form>
<FormGroup>
<FormLabel>Quality</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="quality"
value={qualityId}
values={qualityOptions}
onChange={this.onQualityChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Proper</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="proper"
value={proper}
onChange={this.onProperChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Real</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="real"
value={real}
onChange={this.onRealChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onQualitySelect}
>
Select Quality
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectQualityModalContent.propTypes = {
qualityId: PropTypes.number.isRequired,
proper: PropTypes.bool.isRequired,
real: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onQualitySelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectQualityModalContent;

View file

@ -0,0 +1,94 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import SelectQualityModalContent from './SelectQualityModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles,
(qualityProfiles) => {
const {
isFetchingSchema: isFetching,
schemaPopulated: isPopulated,
schemaError: error,
schema
} = qualityProfiles;
return {
isFetching,
isPopulated,
error,
items: schema.items || []
};
}
);
}
const mapDispatchToProps = {
fetchQualityProfileSchema,
updateInteractiveImportItem
};
class SelectQualityModalContentConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
if (!this.props.isPopulated) {
this.props.fetchQualityProfileSchema();
}
}
//
// Listeners
onQualitySelect = ({ qualityId, proper, real }) => {
const quality = _.find(this.props.items,
(item) => item.quality.id === qualityId).quality;
const revision = {
version: proper ? 2 : 1,
real: real ? 1 : 0
};
this.props.updateInteractiveImportItem({
id: this.props.id,
quality: {
quality,
revision
}
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectQualityModalContent
{...this.props}
onQualitySelect={this.onQualitySelect}
/>
);
}
}
SelectQualityModalContentConnector.propTypes = {
id: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQualityProfileSchema: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectQualityModalContentConnector);

View file

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectSeasonModalContentConnector from './SelectSeasonModalContentConnector';
class SelectSeasonModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectSeasonModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectSeasonModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectSeasonModal;

View file

@ -0,0 +1,58 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import SelectSeasonRow from './SelectSeasonRow';
class SelectSeasonModalContent extends Component {
//
// Render
render() {
const {
items,
onSeasonSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Season
</ModalHeader>
<ModalBody>
{
items.map((item) => {
return (
<SelectSeasonRow
key={item.seasonNumber}
seasonNumber={item.seasonNumber}
onSeasonSelect={onSeasonSelect}
/>
);
})
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectSeasonModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onSeasonSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectSeasonModalContent;

View file

@ -0,0 +1,62 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import SelectSeasonModalContent from './SelectSeasonModalContent';
function createMapStateToProps() {
return createSelector(
createArtistSelector(),
(series) => {
return {
items: series.seasons
};
}
);
}
const mapDispatchToProps = {
updateInteractiveImportItem
};
class SelectSeasonModalContentConnector extends Component {
//
// Listeners
onSeasonSelect = (seasonNumber) => {
this.props.ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
seasonNumber,
episodes: []
});
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectSeasonModalContent
{...this.props}
onSeasonSelect={this.onSeasonSelect}
/>
);
}
}
SelectSeasonModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
artistId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeasonModalContentConnector);

View file

@ -0,0 +1,4 @@
.season {
padding: 8px;
border-bottom: 1px solid $borderColor;
}

View file

@ -0,0 +1,40 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import styles from './SelectSeasonRow.css';
class SelectSeasonRow extends Component {
//
// Listeners
onPress = () => {
this.props.onSeasonSelect(this.props.seasonNumber);
}
//
// Render
render() {
const seasonNumber = this.props.seasonNumber;
return (
<Link
className={styles.season}
component="div"
onPress={this.onPress}
>
{
seasonNumber === 0 ? 'Specials' : `Season ${seasonNumber}`
}
</Link>
);
}
}
SelectSeasonRow.propTypes = {
seasonNumber: PropTypes.number.isRequired,
onSeasonSelect: PropTypes.func.isRequired
};
export default SelectSeasonRow;

View file

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectSeriesModalContentConnector from './SelectSeriesModalContentConnector';
class SelectSeriesModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectSeriesModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectSeriesModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectSeriesModal;

View file

@ -0,0 +1,18 @@
.modalBody {
composes: modalBody from 'Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
.filterInput {
composes: text from 'Components/Form/TextInput.css';
flex: 0 0 auto;
margin-bottom: 20px;
}
.scroller {
flex: 1 1 auto;
}

View file

@ -0,0 +1,99 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { scrollDirections } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Scroller from 'Components/Scroller/Scroller';
import TextInput from 'Components/Form/TextInput';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import SelectSeriesRow from './SelectSeriesRow';
import styles from './SelectSeriesModalContent.css';
class SelectSeriesModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
filter: ''
};
}
//
// Listeners
onFilterChange = ({ value }) => {
this.setState({ filter: value.toLowerCase() });
}
//
// Render
render() {
const {
items,
onSeriesSelect,
onModalClose
} = this.props;
const filter = this.state.filter;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Series
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<TextInput
className={styles.filterInput}
placeholder="Filter series"
name="filter"
value={filter}
autoFocus={true}
onChange={this.onFilterChange}
/>
<Scroller className={styles.scroller}>
{
items.map((item) => {
return item.title.toLowerCase().includes(filter) ?
(
<SelectSeriesRow
key={item.id}
id={item.id}
title={item.title}
onSeriesSelect={onSeriesSelect}
/>
) :
null;
})
}
</Scroller>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectSeriesModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onSeriesSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectSeriesModalContent;

View file

@ -0,0 +1,65 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import SelectSeriesModalContent from './SelectSeriesModalContent';
function createMapStateToProps() {
return createSelector(
createAllSeriesSelector(),
(items) => {
return {
items
};
}
);
}
const mapDispatchToProps = {
updateInteractiveImportItem
};
class SelectSeriesModalContentConnector extends Component {
//
// Listeners
onSeriesSelect = (artistId) => {
const series = _.find(this.props.items, { id: artistId });
this.props.ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
series,
seasonNumber: undefined,
episodes: []
});
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectSeriesModalContent
{...this.props}
onSeriesSelect={this.onSeriesSelect}
/>
);
}
}
SelectSeriesModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeriesModalContentConnector);

View file

@ -0,0 +1,4 @@
.series {
padding: 8px;
border-bottom: 1px solid $borderColor;
}

View file

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import styles from './SelectSeriesRow.css';
class SelectSeriesRow extends Component {
//
// Listeners
onPress = () => {
this.props.onSeriesSelect(this.props.id);
}
//
// Render
render() {
return (
<Link
className={styles.series}
component="div"
onPress={this.onPress}
>
{this.props.title}
</Link>
);
}
}
SelectSeriesRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
onSeriesSelect: PropTypes.func.isRequired
};
export default SelectSeriesRow;