1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-23 22:25:56 -04:00

New: Manage episodes through Manual Import modal

This commit is contained in:
Mark McDowall
2021-11-03 15:44:16 -07:00
parent b184e62fa7
commit 2bf1ce1763
23 changed files with 401 additions and 739 deletions
@@ -26,6 +26,12 @@
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}
.importMode,
.bulkSelect {
composes: select from '~Components/Form/SelectInput.css';
@@ -112,13 +112,21 @@ class InteractiveImportModalContent extends Component {
constructor(props, context) {
super(props, context);
const instanceColumns = _.cloneDeep(columns);
if (!props.showSeries) {
instanceColumns.find((c) => c.name === 'series').isVisible = false;
}
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
invalidRowsSelected: [],
selectModalOpen: null
withoutEpisodeFileIdRowsSelected: [],
selectModalOpen: null,
columns: instanceColumns
};
}
@@ -136,9 +144,14 @@ class InteractiveImportModalContent extends Component {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
onSelectedChange = ({ id, value, hasEpisodeFileId, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
return {
...toggleSelected(state, this.props.items, id, value, shiftKey),
withoutEpisodeFileIdRowsSelected: hasEpisodeFileId || !value ?
_.without(state.withoutEpisodeFileIdRowsSelected, id) :
[...state.withoutEpisodeFileIdRowsSelected, id]
};
});
}
@@ -156,6 +169,16 @@ class InteractiveImportModalContent extends Component {
});
}
onDeleteSelectedPress = () => {
const {
onDeleteSelectedPress
} = this.props;
const selected = this.getSelectedIds();
onDeleteSelectedPress(selected);
}
onImportSelectedPress = () => {
const {
downloadId,
@@ -193,7 +216,9 @@ class InteractiveImportModalContent extends Component {
const {
downloadId,
allowSeriesChange,
autoSelectRow,
showFilterExistingFiles,
showDelete,
showImportMode,
filterExistingFiles,
title,
@@ -215,6 +240,7 @@ class InteractiveImportModalContent extends Component {
allUnselected,
selectedState,
invalidRowsSelected,
withoutEpisodeFileIdRowsSelected,
selectModalOpen
} = this.state;
@@ -308,7 +334,7 @@ class InteractiveImportModalContent extends Component {
{
isPopulated && !!items.length && !isFetching && !isFetching &&
<Table
columns={columns}
columns={this.state.columns}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
@@ -327,6 +353,8 @@ class InteractiveImportModalContent extends Component {
isSelected={selectedState[item.id]}
{...item}
allowSeriesChange={allowSeriesChange}
autoSelectRow={autoSelectRow}
columns={this.state.columns}
onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange}
/>
@@ -345,6 +373,19 @@ class InteractiveImportModalContent extends Component {
<ModalFooter className={styles.footer}>
<div className={styles.leftButtons}>
{
showDelete ?
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
isDisabled={!selectedIds.length || !!withoutEpisodeFileIdRowsSelected.length}
onPress={onModalClose}
>
Delete
</Button> :
null
}
{
!downloadId && showImportMode ?
<SelectInput
@@ -437,7 +478,10 @@ class InteractiveImportModalContent extends Component {
InteractiveImportModalContent.propTypes = {
downloadId: PropTypes.string,
showSeries: PropTypes.bool.isRequired,
allowSeriesChange: PropTypes.bool.isRequired,
autoSelectRow: PropTypes.bool.isRequired,
showDelete: PropTypes.bool.isRequired,
showImportMode: PropTypes.bool.isRequired,
showFilterExistingFiles: PropTypes.bool.isRequired,
filterExistingFiles: PropTypes.bool.isRequired,
@@ -454,13 +498,17 @@ InteractiveImportModalContent.propTypes = {
onSortPress: PropTypes.func.isRequired,
onFilterExistingFilesChange: PropTypes.func.isRequired,
onImportModeChange: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired,
onImportSelectedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
InteractiveImportModalContent.defaultProps = {
showSeries: true,
allowSeriesChange: true,
autoSelectRow: true,
showFilterExistingFiles: false,
showDelete: false,
showImportMode: true,
importMode: 'move'
};
@@ -1,14 +1,42 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { sortDirections } from 'Helpers/Props';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import { updateEpisodeFiles, deleteEpisodeFiles } from 'Store/Actions/episodeFileActions';
import * as commandNames from 'Commands/commandNames';
import InteractiveImportModalContent from './InteractiveImportModalContent';
function isSameEpisodeFile(file, originalFile) {
const {
series,
seasonNumber,
episodes
} = file;
if (!originalFile) {
return false;
}
if (!originalFile.series || series.id !== originalFile.series.id) {
return false;
}
if (seasonNumber !== originalFile.seasonNumber) {
return false;
}
const episodeIds = episodes.map((e) => e.id);
const originalEpisodeIds = originalFile.episodes ? originalFile.episodes.map((e) => e.id) : [];
return episodeIds.every((episodeId) => {
return originalEpisodeIds.indexOf(episodeId) >= 0;
});
}
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('interactiveImport'),
@@ -23,6 +51,8 @@ const mapDispatchToProps = {
dispatchSetInteractiveImportSort: setInteractiveImportSort,
dispatchSetInteractiveImportMode: setInteractiveImportMode,
dispatchClearInteractiveImport: clearInteractiveImport,
dispatchUpdateEpisodeFiles: updateEpisodeFiles,
dispatchDeleteEpisodeFiles: deleteEpisodeFiles,
dispatchExecuteCommand: executeCommand
};
@@ -44,16 +74,34 @@ class InteractiveImportModalContentConnector extends Component {
const {
downloadId,
seriesId,
folder
seasonNumber,
folder,
initialSortKey,
initialSortDirection,
dispatchSetInteractiveImportSort,
dispatchFetchInteractiveImportItems
} = this.props;
const {
filterExistingFiles
} = this.state;
this.props.dispatchFetchInteractiveImportItems({
if (initialSortKey) {
const sortProps = {
sortKey: initialSortKey
};
if (initialSortDirection) {
sortProps.sortDirection = initialSortDirection;
}
dispatchSetInteractiveImportSort(sortProps);
}
dispatchFetchInteractiveImportItems({
downloadId,
seriesId,
seasonNumber,
folder,
filterExistingFiles
});
@@ -99,10 +147,23 @@ class InteractiveImportModalContentConnector extends Component {
this.props.dispatchSetInteractiveImportMode({ importMode });
}
onDeleteSelectedPress = (selected) => {
// TODO: Delete selected (if they have episode IDs)
}
onImportSelectedPress = (selected, importMode) => {
const {
items,
originalItems,
dispatchUpdateEpisodeFiles,
dispatchExecuteCommand,
onModalClose
} = this.props;
const existingFiles = [];
const files = [];
_.forEach(this.props.items, (item) => {
items.forEach((item) => {
const isSelected = selected.indexOf(item.id) > -1;
if (isSelected) {
@@ -112,32 +173,48 @@ class InteractiveImportModalContentConnector extends Component {
episodes,
releaseGroup,
quality,
language
language,
episodeFileId
} = item;
if (!series) {
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
return false;
return;
}
if (isNaN(seasonNumber)) {
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
return false;
return;
}
if (!episodes || !episodes.length) {
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
return false;
return;
}
if (!quality) {
this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' });
return false;
return;
}
if (!language) {
this.setState({ interactiveImportErrorMessage: 'Language must be chosen for each selected file' });
return false;
return;
}
if (episodeFileId) {
const originalItem = originalItems.find((i) => i.id === item.id);
if (isSameEpisodeFile(item, originalItem)) {
existingFiles.push({
id: episodeFileId,
releaseGroup,
quality,
language
});
return;
}
}
files.push({
@@ -148,22 +225,35 @@ class InteractiveImportModalContentConnector extends Component {
releaseGroup,
quality,
language,
downloadId: this.props.downloadId
downloadId: this.props.downloadId,
episodeFileId
});
}
});
if (!files.length) {
return;
let shouldClose = false;
if (existingFiles.length) {
dispatchUpdateEpisodeFiles({
files: existingFiles
});
shouldClose = true;
}
this.props.dispatchExecuteCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode
});
if (files.length) {
dispatchExecuteCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode
});
this.props.onModalClose();
shouldClose = true;
}
if (shouldClose) {
onModalClose();
}
}
//
@@ -183,6 +273,7 @@ class InteractiveImportModalContentConnector extends Component {
onSortPress={this.onSortPress}
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
onImportModeChange={this.onImportModeChange}
onDeleteSelectedPress={this.onDeleteSelectedPress}
onImportSelectedPress={this.onImportSelectedPress}
/>
);
@@ -192,13 +283,19 @@ class InteractiveImportModalContentConnector extends Component {
InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string,
seriesId: PropTypes.number,
seasonNumber: PropTypes.number,
folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
initialSortKey: PropTypes.string,
initialSortDirection: PropTypes.oneOf(sortDirections.all),
originalItems: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchInteractiveImportItems: PropTypes.func.isRequired,
dispatchSetInteractiveImportSort: PropTypes.func.isRequired,
dispatchSetInteractiveImportMode: PropTypes.func.isRequired,
dispatchClearInteractiveImport: PropTypes.func.isRequired,
dispatchUpdateEpisodeFiles: PropTypes.func.isRequired,
dispatchDeleteEpisodeFiles: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
@@ -41,23 +41,35 @@ class InteractiveImportRow extends Component {
componentDidMount() {
const {
allowSeriesChange,
id,
series,
seasonNumber,
episodes,
quality,
language
language,
episodeFileId,
columns
} = this.props;
if (
allowSeriesChange &&
series &&
seasonNumber != null &&
episodes.length &&
quality &&
language
) {
this.props.onSelectedChange({ id, value: true });
this.props.onSelectedChange({
id,
hasEpisodeFileId: !!episodeFileId,
value: true
});
}
this.setState({
isSeriesColumnVisible: columns.find((c) => c.name === 'series').isVisible
});
}
componentDidUpdate(prevProps) {
@@ -104,17 +116,34 @@ class InteractiveImportRow extends Component {
selectRowAfterChange = (value) => {
const {
id,
episodeFileId,
isSelected
} = this.props;
if (!isSelected && value === true) {
this.props.onSelectedChange({ id, value });
this.props.onSelectedChange({
id,
hasEpisodeFileId: !!episodeFileId,
value
});
}
}
//
// Listeners
onSelectedChange = (result) => {
const {
episodeFileId,
onSelectedChange
} = this.props;
onSelectedChange({
...result,
hasEpisodeFileId: !!episodeFileId
});
}
onSelectSeriesPress = () => {
this.setState({ isSelectSeriesModalOpen: true });
}
@@ -186,8 +215,7 @@ class InteractiveImportRow extends Component {
size,
rejections,
isReprocessing,
isSelected,
onSelectedChange
isSelected
} = this.props;
const {
@@ -224,7 +252,7 @@ class InteractiveImportRow extends Component {
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
onSelectedChange={this.onSelectedChange}
/>
<TableRowCell
@@ -234,15 +262,19 @@ class InteractiveImportRow extends Component {
{relativePath}
</TableRowCell>
<TableRowCellButton
isDisabled={!allowSeriesChange}
title={allowSeriesChange ? 'Click to change series' : undefined}
onPress={this.onSelectSeriesPress}
>
{
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
}
</TableRowCellButton>
{
this.state.isSeriesColumnVisible ?
<TableRowCellButton
isDisabled={!allowSeriesChange}
title={allowSeriesChange ? 'Click to change series' : undefined}
onPress={this.onSelectSeriesPress}
>
{
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
}
</TableRowCellButton> :
null
}
<TableRowCellButton
isDisabled={!series}
@@ -418,6 +450,8 @@ InteractiveImportRow.propTypes = {
language: PropTypes.object,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
episodeFileId: PropTypes.number,
isReprocessing: PropTypes.bool,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,