parent
bf852cadbe
commit
4887ed0d2f
@ -0,0 +1,70 @@
|
|||||||
|
.inputContainer {
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainerContent {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organizeSelectedButton,
|
||||||
|
.tagsButton {
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteSelectedButton {
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
|
||||||
|
margin-left: 50px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||||
|
.deleteSelectedButton {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
.buttonContainer {
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.inputContainer {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainerContent {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedAuthorLabel {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import BookEditorFooterLabel from './BookEditorFooterLabel';
|
||||||
|
import DeleteBookModal from './Delete/DeleteBookModal';
|
||||||
|
import styles from './BookEditorFooter.css';
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
class BookEditorFooter extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
monitored: NO_CHANGE,
|
||||||
|
rootFolderPath: NO_CHANGE,
|
||||||
|
savingTags: false,
|
||||||
|
isDeleteBookModalOpen: false,
|
||||||
|
isTagsModalOpen: false,
|
||||||
|
isConfirmMoveModalOpen: false,
|
||||||
|
destinationRootFolder: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
isSaving,
|
||||||
|
saveError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||||
|
this.setState({
|
||||||
|
monitored: NO_CHANGE,
|
||||||
|
rootFolderPath: NO_CHANGE,
|
||||||
|
savingTags: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.setState({ [name]: value });
|
||||||
|
|
||||||
|
if (value === NO_CHANGE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case 'monitored':
|
||||||
|
this.props.onSaveSelected({ [name]: value === 'monitored' });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.props.onSaveSelected({ [name]: value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteSelectedPress = () => {
|
||||||
|
this.setState({ isDeleteBookModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteBookModalClose = () => {
|
||||||
|
this.setState({ isDeleteBookModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
bookIds,
|
||||||
|
selectedCount,
|
||||||
|
isSaving,
|
||||||
|
isDeleting
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
isDeleteBookModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const monitoredOptions = [
|
||||||
|
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
||||||
|
{ key: 'monitored', value: 'Monitored' },
|
||||||
|
{ key: 'unmonitored', value: 'Unmonitored' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentFooter>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<BookEditorFooterLabel
|
||||||
|
label={translate('MonitorBook')}
|
||||||
|
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
name="monitored"
|
||||||
|
value={monitored}
|
||||||
|
values={monitoredOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<div className={styles.buttonContainerContent}>
|
||||||
|
<BookEditorFooterLabel
|
||||||
|
label={translate('SelectedCountBooksSelectedInterp', [selectedCount])}
|
||||||
|
isSaving={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.deleteSelectedButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
isDisabled={!selectedCount || isDeleting}
|
||||||
|
onPress={this.onDeleteSelectedPress}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</SpinnerButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteBookModal
|
||||||
|
isOpen={isDeleteBookModalOpen}
|
||||||
|
bookIds={bookIds}
|
||||||
|
onModalClose={this.onDeleteBookModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</PageContentFooter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookEditorFooter.propTypes = {
|
||||||
|
bookIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
selectedCount: PropTypes.number.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
deleteError: PropTypes.object,
|
||||||
|
onSaveSelected: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookEditorFooter;
|
@ -0,0 +1,8 @@
|
|||||||
|
.label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.savingIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './BookEditorFooterLabel.css';
|
||||||
|
|
||||||
|
function BookEditorFooterLabel(props) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
isSaving
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{label}
|
||||||
|
|
||||||
|
{
|
||||||
|
isSaving &&
|
||||||
|
<SpinnerIcon
|
||||||
|
className={styles.savingIcon}
|
||||||
|
name={icons.SPINNER}
|
||||||
|
isSpinning={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookEditorFooterLabel.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
BookEditorFooterLabel.defaultProps = {
|
||||||
|
className: styles.label
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookEditorFooterLabel;
|
@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import DeleteBookModalContentConnector from './DeleteBookModalContentConnector';
|
||||||
|
|
||||||
|
function DeleteBookModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<DeleteBookModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteBookModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteBookModal;
|
@ -0,0 +1,9 @@
|
|||||||
|
.message {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteFilesMessage {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: $dangerColor;
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
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 } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './DeleteBookModalContent.css';
|
||||||
|
|
||||||
|
class DeleteBookModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
deleteFiles: false,
|
||||||
|
addImportListExclusion: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDeleteFilesChange = ({ value }) => {
|
||||||
|
this.setState({ deleteFiles: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddImportListExclusionChange = ({ value }) => {
|
||||||
|
this.setState({ addImportListExclusion: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteBookConfirmed = () => {
|
||||||
|
const {
|
||||||
|
deleteFiles,
|
||||||
|
addImportListExclusion
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
this.setState({ deleteFiles: false });
|
||||||
|
this.props.onDeleteSelectedPress(deleteFiles, addImportListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
book,
|
||||||
|
files,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
deleteFiles,
|
||||||
|
addImportListExclusion
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Delete Selected Book
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{`Delete File${book.length > 1 ? 's' : ''}`}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="deleteFiles"
|
||||||
|
value={deleteFiles}
|
||||||
|
helpText={'Delete book files'}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isDisabled={files.length === 0}
|
||||||
|
onChange={this.onDeleteFilesChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AddListExclusion')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="addImportListExclusion"
|
||||||
|
value={addImportListExclusion}
|
||||||
|
helpText={translate('AddImportListExclusionHelpText')}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onChange={this.onAddImportListExclusionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
!addImportListExclusion &&
|
||||||
|
<div className={styles.deleteFilesMessage}>
|
||||||
|
<div>
|
||||||
|
{translate('IfYouDontAddAnImportListExclusionAndTheAuthorHasAMetadataProfileOtherThanNoneThenThisBookMayBeReaddedDuringTheNextAuthorRefresh')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.message}>
|
||||||
|
{`Are you sure you want to delete ${book.length} selected book${book.length > 1 ? 's' : ''}${deleteFiles ? ' and their files' : ''}?`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
book.map((s) => {
|
||||||
|
return (
|
||||||
|
<li key={s.title}>
|
||||||
|
<span>{s.title}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{
|
||||||
|
deleteFiles &&
|
||||||
|
<div>
|
||||||
|
<div className={styles.deleteFilesMessage}>
|
||||||
|
{translate('TheFollowingFilesWillBeDeleted')}
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
files.map((s) => {
|
||||||
|
return (
|
||||||
|
<li key={s.path}>
|
||||||
|
<span>{s.path}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={this.onDeleteBookConfirmed}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteBookModalContent.propTypes = {
|
||||||
|
book: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
files: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onDeleteSelectedPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteBookModalContent;
|
@ -0,0 +1,54 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { bulkDeleteBook } from 'Store/Actions/bookEditorActions';
|
||||||
|
import DeleteBookModalContent from './DeleteBookModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { bookIds }) => bookIds,
|
||||||
|
(state) => state.books.items,
|
||||||
|
(state) => state.bookFiles.items,
|
||||||
|
(bookIds, allBooks, allBookFiles) => {
|
||||||
|
const selectedBook = _.intersectionWith(allBooks, bookIds, (s, id) => {
|
||||||
|
return s.id === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedBook = _.orderBy(selectedBook, 'title');
|
||||||
|
|
||||||
|
const selectedFiles = _.intersectionWith(allBookFiles, bookIds, (s, id) => {
|
||||||
|
return s.bookId === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = _.orderBy(selectedFiles, ['bookId', 'path']);
|
||||||
|
|
||||||
|
const book = _.map(sortedBook, (s) => {
|
||||||
|
return {
|
||||||
|
title: s.title,
|
||||||
|
path: s.path
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
book,
|
||||||
|
files
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onDeleteSelectedPress(deleteFiles, addImportListExclusion) {
|
||||||
|
dispatch(bulkDeleteBook({
|
||||||
|
bookIds: props.bookIds,
|
||||||
|
deleteFiles,
|
||||||
|
addImportListExclusion
|
||||||
|
}));
|
||||||
|
|
||||||
|
props.onModalClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteBookModalContent);
|
@ -0,0 +1,114 @@
|
|||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import { set, updateItem } from './baseActions';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'bookEditor';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const SAVE_BOOK_EDITOR = 'bookEditor/saveBookEditor';
|
||||||
|
export const BULK_DELETE_BOOK = 'bookEditor/bulkDeleteBook';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const saveBookEditor = createThunk(SAVE_BOOK_EDITOR);
|
||||||
|
export const bulkDeleteBook = createThunk(BULK_DELETE_BOOK);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
[SAVE_BOOK_EDITOR]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/book/editor',
|
||||||
|
method: 'PUT',
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
dataType: 'json'
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
...data.map((book) => {
|
||||||
|
return updateItem({
|
||||||
|
id: book.id,
|
||||||
|
section: 'books',
|
||||||
|
...book
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[BULK_DELETE_BOOK]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isDeleting: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
const promise = createAjaxRequest({
|
||||||
|
url: '/book/editor',
|
||||||
|
method: 'DELETE',
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
dataType: 'json'
|
||||||
|
}).request;
|
||||||
|
|
||||||
|
promise.done(() => {
|
||||||
|
// SignalR will take care of removing the book from the collection
|
||||||
|
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({}, defaultState, section);
|
@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NzbDrone.Core.Books;
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
using Readarr.Http;
|
||||||
|
|
||||||
|
namespace Readarr.Api.V1.Books
|
||||||
|
{
|
||||||
|
[V1ApiController("book/editor")]
|
||||||
|
public class BookEditorController : Controller
|
||||||
|
{
|
||||||
|
private readonly IBookService _bookService;
|
||||||
|
private readonly IManageCommandQueue _commandQueueManager;
|
||||||
|
|
||||||
|
public BookEditorController(IBookService bookService, IManageCommandQueue commandQueueManager)
|
||||||
|
{
|
||||||
|
_bookService = bookService;
|
||||||
|
_commandQueueManager = commandQueueManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public IActionResult SaveAll([FromBody] BookEditorResource resource)
|
||||||
|
{
|
||||||
|
var booksToUpdate = _bookService.GetBooks(resource.BookIds);
|
||||||
|
|
||||||
|
foreach (var book in booksToUpdate)
|
||||||
|
{
|
||||||
|
if (resource.Monitored.HasValue)
|
||||||
|
{
|
||||||
|
book.Monitored = resource.Monitored.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_bookService.UpdateMany(booksToUpdate);
|
||||||
|
return Accepted(booksToUpdate.ToResource());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public object DeleteBook([FromBody] BookEditorResource resource)
|
||||||
|
{
|
||||||
|
foreach (var bookId in resource.BookIds)
|
||||||
|
{
|
||||||
|
_bookService.DeleteBook(bookId, resource.DeleteFiles ?? false, resource.AddImportListExclusion ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new object();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Readarr.Api.V1.Books
|
||||||
|
{
|
||||||
|
public class BookEditorResource
|
||||||
|
{
|
||||||
|
public List<int> BookIds { get; set; }
|
||||||
|
public bool? Monitored { get; set; }
|
||||||
|
public bool? DeleteFiles { get; set; }
|
||||||
|
public bool? AddImportListExclusion { get; set; }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue