Fixed: Manual Import Reprocessing

This commit is contained in:
Qstick
2023-01-14 11:45:54 -06:00
parent bc63587428
commit 3825ecd393
20 changed files with 422 additions and 36 deletions

View File

@@ -53,7 +53,7 @@ class SelectAuthorModalContentConnector extends Component {
});
});
this.props.saveInteractiveImportItem({ id: ids });
this.props.saveInteractiveImportItem({ ids });
this.props.onModalClose(true);
};

View File

@@ -69,7 +69,7 @@ class SelectBookModalContentConnector extends Component {
});
});
this.props.saveInteractiveImportItem({ id: ids });
this.props.saveInteractiveImportItem({ ids });
this.props.onModalClose(true);
};

View File

@@ -78,7 +78,7 @@ class SelectEditionModalContentConnector extends Component {
});
});
this.props.saveInteractiveImportItem({ id: ids });
this.props.saveInteractiveImportItem({ ids });
this.props.onModalClose(true);
};

View File

@@ -21,7 +21,9 @@ import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
@@ -46,6 +48,11 @@ const columns = [
label: 'Book',
isVisible: true
},
{
name: 'releaseGroup',
label: 'Release Group',
isVisible: true
},
{
name: 'quality',
label: 'Quality',
@@ -75,14 +82,16 @@ const filterExistingFilesOptions = {
};
const importModeOptions = [
{ key: 'move', value: 'Move Files' },
{ key: 'copy', value: 'Hardlink/Copy Files' }
{ key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: translate('HardlinkCopyFiles') }
];
const SELECT = 'select';
const AUTHOR = 'author';
const BOOK = 'book';
const EDITION = 'edition';
const RELEASE_GROUP = 'releaseGroup';
const QUALITY = 'quality';
const replaceExistingFilesOptions = {
@@ -280,7 +289,8 @@ class InteractiveImportModalContent extends Component {
{ key: SELECT, value: 'Select...', disabled: true },
{ key: BOOK, value: 'Select Book' },
{ key: EDITION, value: 'Select Edition' },
{ key: QUALITY, value: 'Select Quality' }
{ key: QUALITY, value: 'Select Quality' },
{ key: RELEASE_GROUP, value: 'Select ReleaseGroup' }
];
if (allowAuthorChange) {
@@ -483,6 +493,13 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === RELEASE_GROUP}
ids={selectedIds}
releaseGroup=""
onModalClose={this.onSelectModalClose}
/>
<SelectQualityModal
isOpen={selectModalOpen === QUALITY}
ids={selectedIds}

View File

@@ -120,6 +120,11 @@ class InteractiveImportModalContentConnector extends Component {
onImportSelectedPress = (selected, importMode) => {
const files = [];
if (importMode === 'chooseImportMethod') {
this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' });
return;
}
_.forEach(this.props.items, (item) => {
const isSelected = selected.indexOf(item.id) > -1;

View File

@@ -17,10 +17,11 @@
cursor: pointer;
}
.loading {
.reprocessing {
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin-top: 0;
text-align: start;
}
.additionalFile {

View File

@@ -3,7 +3,6 @@ import React, { Component } from 'react';
import BookQuality from 'Book/BookQuality';
import FileDetails from 'BookFile/FileDetails';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
@@ -15,6 +14,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
@@ -32,6 +32,7 @@ class InteractiveImportRow extends Component {
isDetailsModalOpen: false,
isSelectAuthorModalOpen: false,
isSelectBookModalOpen: false,
isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false
};
}
@@ -123,6 +124,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectBookModalOpen: true });
};
onSelectReleaseGroupPress = () => {
this.setState({ isSelectReleaseGroupModalOpen: true });
};
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
};
@@ -137,6 +142,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed);
};
onSelectReleaseGroupModalClose = (changed) => {
this.setState({ isSelectReleaseGroupModalOpen: false });
this.selectRowAfterChange(changed);
};
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
@@ -153,19 +163,21 @@ class InteractiveImportRow extends Component {
author,
book,
quality,
releaseGroup,
size,
rejections,
additionalFile,
isSelected,
isReprocessing,
onSelectedChange,
audioTags,
isSaving
audioTags
} = this.props;
const {
isDetailsModalOpen,
isSelectAuthorModalOpen,
isSelectBookModalOpen,
isSelectReleaseGroupModalOpen,
isSelectQualityModalOpen
} = this.state;
@@ -176,7 +188,8 @@ class InteractiveImportRow extends Component {
}
const showAuthorPlaceholder = isSelected && !author;
const showBookNumberPlaceholder = isSelected && !!author && !book;
const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const pathCellContents = (
@@ -237,6 +250,17 @@ class InteractiveImportRow extends Component {
}
</TableRowCellButton>
<TableRowCellButton
title={translate('ClickToChangeReleaseGroup')}
onPress={this.onSelectReleaseGroupPress}
>
{
showReleaseGroupPlaceholder ?
<InteractiveImportRowCellPlaceholder /> :
releaseGroup
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title={translate('ClickToChangeQuality')}
@@ -262,14 +286,7 @@ class InteractiveImportRow extends Component {
<TableRowCell>
{
isSaving &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isSaving && rejections && rejections.length ?
rejections && rejections.length ?
<Popover
anchor={
<Icon
@@ -292,6 +309,7 @@ class InteractiveImportRow extends Component {
</ul>
}
position={tooltipPositions.LEFT}
canFlip={false}
/> :
null
}
@@ -322,6 +340,13 @@ class InteractiveImportRow extends Component {
onModalClose={this.onSelectBookModalClose}
/>
<SelectReleaseGroupModal
isOpen={isSelectReleaseGroupModalOpen}
ids={[id]}
releaseGroup={releaseGroup ?? ''}
onModalClose={this.onSelectReleaseGroupModalClose}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
ids={[id]}
@@ -343,13 +368,14 @@ InteractiveImportRow.propTypes = {
author: PropTypes.object,
book: PropTypes.object,
foreignEditionId: PropTypes.string,
releaseGroup: PropTypes.string,
quality: PropTypes.object,
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired,
additionalFile: PropTypes.bool.isRequired,
isReprocessing: PropTypes.bool,
isSelected: PropTypes.bool,
isSaving: PropTypes.bool.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onValidRowChange: PropTypes.func.isRequired
};

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import InteractiveImportSelectFolderModalContentConnector from './Folder/InteractiveImportSelectFolderModalContentConnector';
import InteractiveImportModalContentConnector from './Interactive/InteractiveImportModalContentConnector';
@@ -47,6 +48,7 @@ class InteractiveImportModal extends Component {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
@@ -73,7 +75,12 @@ InteractiveImportModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
folder: PropTypes.string,
downloadId: PropTypes.string,
modalTitle: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired
};
InteractiveImportModal.defaultProps = {
modalTitle: 'Manual Import'
};
export default InteractiveImportModal;

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import getQualities from 'Utilities/Quality/getQualities';
import SelectQualityModalContent from './SelectQualityModalContent';
@@ -31,7 +31,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
};
class SelectQualityModalContentConnector extends Component {
@@ -49,6 +50,12 @@ class SelectQualityModalContentConnector extends Component {
// Listeners
onQualitySelect = ({ qualityId, proper, real }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchSaveInteractiveImportItems
} = this.props;
const quality = _.find(this.props.items,
(item) => item.id === qualityId);
@@ -57,14 +64,16 @@ class SelectQualityModalContentConnector extends Component {
real: real ? 1 : 0
};
this.props.dispatchUpdateInteractiveImportItems({
ids: this.props.ids,
dispatchUpdateInteractiveImportItems({
ids,
quality: {
quality,
revision
}
});
dispatchSaveInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
@@ -89,6 +98,7 @@ SelectQualityModalContentConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

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

View File

@@ -0,0 +1,7 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}

View File

@@ -0,0 +1,103 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
import styles from './SelectReleaseGroupModalContent.css';
class SelectReleaseGroupModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
releaseGroup
} = props;
this.state = {
releaseGroup
};
}
//
// Listeners
onReleaseGroupChange = ({ value }) => {
this.setState({ releaseGroup: value });
};
onReleaseGroupSelect = () => {
this.props.onReleaseGroupSelect(this.state);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
releaseGroup
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Set Release Group
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>Release Group</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
autoFocus={true}
onChange={this.onReleaseGroupChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onReleaseGroupSelect}
>
Set Release Group
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectReleaseGroupModalContent.propTypes = {
releaseGroup: PropTypes.string.isRequired,
onReleaseGroupSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModalContent;

View File

@@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
};
class SelectReleaseGroupModalContentConnector extends Component {
//
// Listeners
onReleaseGroupSelect = ({ releaseGroup }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchSaveInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
releaseGroup
});
dispatchSaveInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectReleaseGroupModalContent
{...this.props}
onReleaseGroupSelect={this.onReleaseGroupSelect}
/>
);
}
}
SelectReleaseGroupModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectReleaseGroupModalContentConnector);

View File

@@ -5,10 +5,9 @@ import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import updateSectionState from 'Utilities/State/updateSectionState';
import { set, update } from './baseActions';
import { set, update, updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
//
@@ -18,6 +17,8 @@ export const section = 'interactiveImport';
const booksSection = `${section}.books`;
const bookFilesSection = `${section}.bookFiles`;
let abortCurrentRequest = null;
let currentIds = [];
const MAXIMUM_RECENT_FOLDERS = 10;
@@ -34,7 +35,7 @@ export const defaultState = {
sortKey: 'quality',
sortDirection: sortDirections.DESCENDING,
recentFolders: [],
importMode: 'move',
importMode: 'chooseImportMode',
sortPredicates: {
path: function(item, direction) {
const path = item.path;
@@ -156,7 +157,82 @@ export const actionHandlers = handleThunks({
});
},
[SAVE_INTERACTIVE_IMPORT_ITEM]: createSaveProviderHandler(section, '/manualimport', {}, true),
[SAVE_INTERACTIVE_IMPORT_ITEM]: function(getState, payload, dispatch) {
if (abortCurrentRequest) {
abortCurrentRequest();
}
dispatch(batchActions([
...currentIds.map((id) => updateItem({
section,
id,
isReprocessing: false,
updateOnly: true
})),
...payload.ids.map((id) => updateItem({
section,
id,
isReprocessing: true,
updateOnly: true
}))
]));
const items = getState()[section].items;
const requestPayload = payload.ids.map((id) => {
const item = items.find((i) => i.id === id);
return {
id,
path: item.path,
authorId: item.author ? item.author.id : undefined,
bookId: item.book ? item.book.id : undefined,
foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined,
quality: item.quality,
releaseGroup: item.releaseGroup,
downloadId: item.downloadId,
additionalFile: item.additionalFile,
replaceExistingFiles: item.replaceExistingFiles,
disableReleaseSwitching: item.disableReleaseSwitching
};
});
const { request, abortRequest } = createAjaxRequest({
method: 'POST',
url: '/manualimport',
contentType: 'application/json',
data: JSON.stringify(requestPayload)
});
abortCurrentRequest = abortRequest;
currentIds = payload.ids;
request.done((data) => {
dispatch(batchActions(
data.map((item) => updateItem({
section,
...item,
isReprocessing: false,
updateOnly: true
}))
));
});
request.fail((xhr) => {
if (xhr.aborted) {
return;
}
dispatch(batchActions(
payload.ids.map((id) => updateItem({
section,
id,
isReprocessing: false,
updateOnly: true
}))
));
});
},
[FETCH_INTERACTIVE_IMPORT_BOOKS]: createFetchHandler(booksSection, '/book'),