+ {
+ isEditorActive &&
+
+
+
+ }
+
{
status === 'ended' &&
);
@@ -264,7 +274,10 @@ AuthorIndexOverviews.propTypes = {
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
- timeFormat: PropTypes.string.isRequired
+ timeFormat: PropTypes.string.isRequired,
+ selectedState: PropTypes.object.isRequired,
+ onSelectedChange: PropTypes.func.isRequired,
+ isEditorActive: PropTypes.bool.isRequired
};
export default AuthorIndexOverviews;
diff --git a/frontend/src/Author/Index/Posters/AuthorIndexPoster.css b/frontend/src/Author/Index/Posters/AuthorIndexPoster.css
index 50dfce8d0..3df74dcae 100644
--- a/frontend/src/Author/Index/Posters/AuthorIndexPoster.css
+++ b/frontend/src/Author/Index/Posters/AuthorIndexPoster.css
@@ -78,6 +78,13 @@ $hoverScale: 1.05;
color: $white;
}
+.editorSelect {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ z-index: 4;
+}
+
.controls {
position: absolute;
bottom: 10px;
diff --git a/frontend/src/Author/Index/Posters/AuthorIndexPoster.js b/frontend/src/Author/Index/Posters/AuthorIndexPoster.js
index 3afeb4e8d..40a4c3294 100644
--- a/frontend/src/Author/Index/Posters/AuthorIndexPoster.js
+++ b/frontend/src/Author/Index/Posters/AuthorIndexPoster.js
@@ -4,6 +4,7 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
+import CheckInput from 'Components/Form/CheckInput';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
@@ -63,6 +64,15 @@ class AuthorIndexPoster extends Component {
}
}
+ onChange = ({ value, shiftKey }) => {
+ const {
+ id,
+ onSelectedChange
+ } = this.props;
+
+ onSelectedChange({ id, value, shiftKey });
+ }
+
//
// Render
@@ -92,6 +102,9 @@ class AuthorIndexPoster extends Component {
isSearchingAuthor,
onRefreshAuthorPress,
onSearchPress,
+ isEditorActive,
+ isSelected,
+ onSelectedChange,
...otherProps
} = this.props;
@@ -120,6 +133,18 @@ class AuthorIndexPoster extends Component {
+ {
+ isEditorActive &&
+
+
+
+ }
+
);
@@ -328,7 +338,10 @@ AuthorIndexPosters.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
- timeFormat: PropTypes.string.isRequired
+ timeFormat: PropTypes.string.isRequired,
+ selectedState: PropTypes.object.isRequired,
+ onSelectedChange: PropTypes.func.isRequired,
+ isEditorActive: PropTypes.bool.isRequired
};
export default AuthorIndexPosters;
diff --git a/frontend/src/Author/Index/Table/AuthorIndexHeader.js b/frontend/src/Author/Index/Table/AuthorIndexHeader.js
index 465c48eaf..62d6e1e49 100644
--- a/frontend/src/Author/Index/Table/AuthorIndexHeader.js
+++ b/frontend/src/Author/Index/Table/AuthorIndexHeader.js
@@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
+import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import AuthorIndexTableOptionsConnector from './AuthorIndexTableOptionsConnector';
import hasGrowableColumns from './hasGrowableColumns';
@@ -15,6 +16,10 @@ function AuthorIndexHeader(props) {
showBanners,
columns,
onTableOptionChange,
+ allSelected,
+ allUnselected,
+ onSelectAllChange,
+ isEditorActive,
...otherProps
} = props;
@@ -33,6 +38,21 @@ function AuthorIndexHeader(props) {
return null;
}
+ if (name === 'select') {
+ if (isEditorActive) {
+ return (
+
+ );
+ }
+
+ return null;
+ }
+
if (name === 'actions') {
return (
+ );
+ }
+
if (name === 'status') {
return (
@@ -87,7 +93,12 @@ class AuthorIndexTable extends Component {
isSmallScreen,
onSortPress,
scroller,
- scrollTop
+ scrollTop,
+ allSelected,
+ allUnselected,
+ onSelectAllChange,
+ isEditorActive,
+ selectedState
} = this.props;
return (
@@ -108,8 +119,13 @@ class AuthorIndexTable extends Component {
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
+ allSelected={allSelected}
+ allUnselected={allUnselected}
+ onSelectAllChange={onSelectAllChange}
+ isEditorActive={isEditorActive}
/>
}
+ selectedState={selectedState}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
@@ -129,7 +145,13 @@ AuthorIndexTable.propTypes = {
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
- onSortPress: PropTypes.func.isRequired
+ onSortPress: PropTypes.func.isRequired,
+ allSelected: PropTypes.bool.isRequired,
+ allUnselected: PropTypes.bool.isRequired,
+ selectedState: PropTypes.object.isRequired,
+ onSelectedChange: PropTypes.func.isRequired,
+ onSelectAllChange: PropTypes.func.isRequired,
+ isEditorActive: PropTypes.bool.isRequired
};
export default AuthorIndexTable;
diff --git a/frontend/src/Book/Editor/Delete/DeleteBookModalContentConnector.js b/frontend/src/Book/Editor/Delete/DeleteBookModalContentConnector.js
index 234e56a29..5c402082b 100644
--- a/frontend/src/Book/Editor/Delete/DeleteBookModalContentConnector.js
+++ b/frontend/src/Book/Editor/Delete/DeleteBookModalContentConnector.js
@@ -1,7 +1,7 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { bulkDeleteBook } from 'Store/Actions/bookEditorActions';
+import { bulkDeleteBook } from 'Store/Actions/bookIndexActions';
import DeleteBookModalContent from './DeleteBookModalContent';
function createMapStateToProps() {
diff --git a/frontend/src/Book/Index/BookIndex.js b/frontend/src/Book/Index/BookIndex.js
index 2e5aec413..8bc4ecb12 100644
--- a/frontend/src/Book/Index/BookIndex.js
+++ b/frontend/src/Book/Index/BookIndex.js
@@ -2,7 +2,9 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import NoAuthor from 'Author/NoAuthor';
+import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar';
@@ -11,10 +13,13 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
-import { align, icons, sortDirections } from 'Helpers/Props';
+import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
+import getSelectedIds from 'Utilities/Table/getSelectedIds';
+import selectAll from 'Utilities/Table/selectAll';
+import toggleSelected from 'Utilities/Table/toggleSelected';
import BookIndexFooterConnector from './BookIndexFooterConnector';
import BookIndexFilterMenu from './Menus/BookIndexFilterMenu';
import BookIndexSortMenu from './Menus/BookIndexSortMenu';
@@ -52,12 +57,19 @@ class BookIndex extends Component {
jumpBarItems: { order: [] },
jumpToCharacter: null,
isPosterOptionsModalOpen: false,
- isOverviewOptionsModalOpen: false
+ isOverviewOptionsModalOpen: false,
+ isConfirmSearchModalOpen: false,
+ isEditorActive: false,
+ allSelected: false,
+ allUnselected: false,
+ lastToggled: null,
+ selectedState: {}
};
}
componentDidMount() {
this.setJumpBarItems();
+ this.setSelectedState();
}
componentDidUpdate(prevProps) {
@@ -72,6 +84,7 @@ class BookIndex extends Component {
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
+ this.setSelectedState();
}
if (this.state.jumpToCharacter != null) {
@@ -86,6 +99,48 @@ class BookIndex extends Component {
this.setState({ scroller: ref });
}
+ getSelectedIds = () => {
+ if (this.state.allUnselected) {
+ return [];
+ }
+ return getSelectedIds(this.state.selectedState);
+ }
+
+ setSelectedState() {
+ const {
+ items
+ } = this.props;
+
+ const {
+ selectedState
+ } = this.state;
+
+ const newSelectedState = {};
+
+ items.forEach((book) => {
+ const isItemSelected = selectedState[book.id];
+
+ if (isItemSelected) {
+ newSelectedState[book.id] = isItemSelected;
+ } else {
+ newSelectedState[book.id] = false;
+ }
+ });
+
+ const selectedCount = getSelectedIds(newSelectedState).length;
+ const newStateCount = Object.keys(newSelectedState).length;
+ let isAllSelected = false;
+ let isAllUnselected = false;
+
+ if (selectedCount === 0) {
+ isAllUnselected = true;
+ } else if (selectedCount === newStateCount) {
+ isAllSelected = true;
+ }
+
+ this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
+ }
+
setJumpBarItems() {
const {
items,
@@ -150,10 +205,64 @@ class BookIndex extends Component {
this.setState({ isOverviewOptionsModalOpen: false });
}
+ onEditorTogglePress = () => {
+ if (this.state.isEditorActive) {
+ this.setState({ isEditorActive: false });
+ } else {
+ const newState = selectAll(this.state.selectedState, false);
+ newState.isEditorActive = true;
+ this.setState(newState);
+ }
+ }
+
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
}
+ onSelectAllChange = ({ value }) => {
+ this.setState(selectAll(this.state.selectedState, value));
+ }
+
+ onSelectAllPress = () => {
+ this.onSelectAllChange({ value: !this.state.allSelected });
+ }
+
+ onSelectedChange = ({ id, value, shiftKey = false }) => {
+ this.setState((state) => {
+ return toggleSelected(state, this.props.items, id, value, shiftKey);
+ });
+ }
+
+ onSaveSelected = (changes) => {
+ this.props.onSaveSelected({
+ bookIds: this.getSelectedIds(),
+ ...changes
+ });
+ }
+
+ onSearchPress = () => {
+ this.setState({ isConfirmSearchModalOpen: true });
+ }
+
+ onRefreshBookPress = () => {
+ const selectedIds = this.getSelectedIds();
+ const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : [];
+
+ this.props.onRefreshBookPress(refreshIds);
+ }
+
+ onSearchConfirmed = () => {
+ const selectedMovieIds = this.getSelectedIds();
+ const searchIds = this.state.isMovieEditorActive && selectedMovieIds.length > 0 ? selectedMovieIds : this.props.items.map((m) => m.id);
+
+ this.props.onSearchPress(searchIds);
+ this.setState({ isConfirmSearchModalOpen: false });
+ }
+
+ onConfirmSearchModalClose = () => {
+ this.setState({ isConfirmSearchModalOpen: false });
+ }
+
//
// Render
@@ -173,11 +282,15 @@ class BookIndex extends Component {
view,
isRefreshingBook,
isRssSyncExecuting,
+ isSearching,
+ isSaving,
+ saveError,
+ isDeleting,
+ deleteError,
onScroll,
onSortSelect,
onFilterSelect,
onViewSelect,
- onRefreshAuthorPress,
onRssSyncPress,
...otherProps
} = this.props;
@@ -187,23 +300,35 @@ class BookIndex extends Component {
jumpBarItems,
jumpToCharacter,
isPosterOptionsModalOpen,
- isOverviewOptionsModalOpen
+ isOverviewOptionsModalOpen,
+ isConfirmSearchModalOpen,
+ isEditorActive,
+ selectedState,
+ allSelected,
+ allUnselected
} = this.state;
+ const selectedBookIds = this.getSelectedIds();
+
const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoAuthor = !totalItems;
+ const refreshLabel = isEditorActive && selectedBookIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
+ const searchIndexLabel = selectedFilterKey === 'all' ? translate('SearchAll') : translate('SearchFiltered');
+ const searchEditorLabel = selectedBookIds.length > 0 ? translate('SearchSelected') : translate('SearchAll');
+ const searchWarningCount = isEditorActive && selectedBookIds.length > 0 ? selectedBookIds.length : items.length;
+
return (
+
+
+
+
+
+
+ {
+ isEditorActive ?
+ :
+
+ }
+
+ {
+ isEditorActive ?
+ :
+ null
+ }
+
@@ -336,6 +505,19 @@ class BookIndex extends Component {
}
+ {
+ isLoaded && isEditorActive &&
+
+ }
+
+
+
+
+ {translate('MassBookSearchWarning', [searchWarningCount])}
+
+
+ {translate('ThisCannotBeCancelled')}
+
+
+ }
+ confirmLabel={translate('Search')}
+ onConfirm={this.onSearchConfirmed}
+ onCancel={this.onConfirmSearchModalClose}
+ />
);
}
@@ -365,14 +566,21 @@ BookIndex.propTypes = {
sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired,
isRefreshingBook: PropTypes.bool.isRequired,
+ isSearching: PropTypes.bool.isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
+ isSaving: PropTypes.bool.isRequired,
+ saveError: PropTypes.object,
+ isDeleting: PropTypes.bool.isRequired,
+ deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired,
- onRefreshAuthorPress: PropTypes.func.isRequired,
+ onRefreshBookPress: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired,
- onScroll: PropTypes.func.isRequired
+ onSearchPress: PropTypes.func.isRequired,
+ onScroll: PropTypes.func.isRequired,
+ onSaveSelected: PropTypes.func.isRequired
};
export default BookIndex;
diff --git a/frontend/src/Book/Index/BookIndexConnector.js b/frontend/src/Book/Index/BookIndexConnector.js
index 589edfc10..b8fa7f7fd 100644
--- a/frontend/src/Book/Index/BookIndexConnector.js
+++ b/frontend/src/Book/Index/BookIndexConnector.js
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
-import { setBookFilter, setBookSort, setBookTableOption, setBookView } from 'Store/Actions/bookIndexActions';
+import { saveBookEditor, setBookFilter, setBookSort, setBookTableOption, setBookView } from 'Store/Actions/bookIndexActions';
import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions';
import createBookClientSideCollectionItemsSelector from 'Store/Selectors/createBookClientSideCollectionItemsSelector';
@@ -19,12 +19,16 @@ function createMapStateToProps() {
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC),
+ createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
+ createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
createDimensionsSelector(),
(
book,
isRefreshingAuthorCommand,
isRefreshingBookCommand,
isRssSyncExecuting,
+ isCutoffBooksSearch,
+ isMissingBooksSearch,
dimensionsState
) => {
const isRefreshingBook = isRefreshingBookCommand || isRefreshingAuthorCommand;
@@ -32,6 +36,7 @@ function createMapStateToProps() {
...book,
isRefreshingBook,
isRssSyncExecuting,
+ isSearching: isCutoffBooksSearch || isMissingBooksSearch,
isSmallScreen: dimensionsState.isSmallScreen
};
}
@@ -56,9 +61,14 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setBookView({ view }));
},
- onRefreshAuthorPress() {
+ dispatchSaveBookEditor(payload) {
+ dispatch(saveBookEditor(payload));
+ },
+
+ onRefreshBookPress(items) {
dispatch(executeCommand({
- name: commandNames.REFRESH_AUTHOR
+ name: commandNames.BULK_REFRESH_BOOK,
+ bookIds: items
}));
},
@@ -66,6 +76,13 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(executeCommand({
name: commandNames.RSS_SYNC
}));
+ },
+
+ onSearchPress(items) {
+ dispatch(executeCommand({
+ name: commandNames.BOOK_SEARCH,
+ bookIds: items
+ }));
}
};
}
@@ -79,6 +96,10 @@ class BookIndexConnector extends Component {
this.props.dispatchSetBookView(view);
}
+ onSaveSelected = (payload) => {
+ this.props.dispatchSaveBookEditor(payload);
+ }
+
onScroll = ({ scrollTop }) => {
scrollPositions.bookIndex = scrollTop;
}
@@ -92,6 +113,7 @@ class BookIndexConnector extends Component {
{...this.props}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
+ onSaveSelected={this.onSaveSelected}
/>
);
}
@@ -100,7 +122,8 @@ class BookIndexConnector extends Component {
BookIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
- dispatchSetBookView: PropTypes.func.isRequired
+ dispatchSetBookView: PropTypes.func.isRequired,
+ dispatchSaveBookEditor: PropTypes.func.isRequired
};
export default withScrollPosition(
diff --git a/frontend/src/Book/Index/BookIndexItemConnector.js b/frontend/src/Book/Index/BookIndexItemConnector.js
index 93a1c0ff1..d27abe2a2 100644
--- a/frontend/src/Book/Index/BookIndexItemConnector.js
+++ b/frontend/src/Book/Index/BookIndexItemConnector.js
@@ -42,7 +42,7 @@ function createMapStateToProps() {
executingCommands
) => {
- // If an book is deleted this selector may fire before the parent
+ // If a book is deleted this selector may fire before the parent
// selectors, which will result in an undefined book, if that happens
// we want to return early here and again in the render function to avoid
// trying to show an book that has no information available.
diff --git a/frontend/src/Book/Index/Overview/BookIndexOverview.css b/frontend/src/Book/Index/Overview/BookIndexOverview.css
index f2eb3d995..48afa9f6c 100644
--- a/frontend/src/Book/Index/Overview/BookIndexOverview.css
+++ b/frontend/src/Book/Index/Overview/BookIndexOverview.css
@@ -19,6 +19,13 @@ $hoverScale: 1.05;
left: 0;
}
+.editorSelect {
+ position: absolute;
+ top: 0;
+ left: 5px;
+ z-index: 3;
+}
+
.posterContainer {
position: relative;
overflow: hidden;
diff --git a/frontend/src/Book/Index/Overview/BookIndexOverview.js b/frontend/src/Book/Index/Overview/BookIndexOverview.js
index 94ed20ace..37983d3a9 100644
--- a/frontend/src/Book/Index/Overview/BookIndexOverview.js
+++ b/frontend/src/Book/Index/Overview/BookIndexOverview.js
@@ -5,6 +5,7 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar';
+import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -67,6 +68,15 @@ class BookIndexOverview extends Component {
this.setState({ isDeleteAuthorModalOpen: false });
}
+ onChange = ({ value, shiftKey }) => {
+ const {
+ id,
+ onSelectedChange
+ } = this.props;
+
+ onSelectedChange({ id, value, shiftKey });
+ }
+
//
// Render
@@ -95,6 +105,8 @@ class BookIndexOverview extends Component {
isSearchingBook,
onRefreshBookPress,
onSearchPress,
+ isEditorActive,
+ isSelected,
...otherProps
} = this.props;
@@ -125,6 +137,17 @@ class BookIndexOverview extends Component {
+ {
+ isEditorActive &&
+
+
+
+ }
{
status === 'ended' &&
);
@@ -263,7 +273,10 @@ BookIndexOverviews.propTypes = {
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
- timeFormat: PropTypes.string.isRequired
+ timeFormat: PropTypes.string.isRequired,
+ selectedState: PropTypes.object.isRequired,
+ onSelectedChange: PropTypes.func.isRequired,
+ isEditorActive: PropTypes.bool.isRequired
};
export default BookIndexOverviews;
diff --git a/frontend/src/Book/Index/Posters/BookIndexPoster.css b/frontend/src/Book/Index/Posters/BookIndexPoster.css
index 50dfce8d0..3df74dcae 100644
--- a/frontend/src/Book/Index/Posters/BookIndexPoster.css
+++ b/frontend/src/Book/Index/Posters/BookIndexPoster.css
@@ -78,6 +78,13 @@ $hoverScale: 1.05;
color: $white;
}
+.editorSelect {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ z-index: 4;
+}
+
.controls {
position: absolute;
bottom: 10px;
diff --git a/frontend/src/Book/Index/Posters/BookIndexPoster.js b/frontend/src/Book/Index/Posters/BookIndexPoster.js
index 7f0be2943..f4c984e6f 100644
--- a/frontend/src/Book/Index/Posters/BookIndexPoster.js
+++ b/frontend/src/Book/Index/Posters/BookIndexPoster.js
@@ -5,6 +5,7 @@ import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import EditBookModalConnector from 'Book/Edit/EditBookModalConnector';
import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar';
+import CheckInput from 'Components/Form/CheckInput';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
@@ -73,6 +74,15 @@ class BookIndexPoster extends Component {
}
}
+ onChange = ({ value, shiftKey }) => {
+ const {
+ id,
+ onSelectedChange
+ } = this.props;
+
+ onSelectedChange({ id, value, shiftKey });
+ }
+
//
// Render
@@ -103,6 +113,9 @@ class BookIndexPoster extends Component {
isSearchingBook,
onRefreshBookPress,
onSearchPress,
+ isEditorActive,
+ isSelected,
+ onSelectedChange,
...otherProps
} = this.props;
@@ -132,6 +145,18 @@ class BookIndexPoster extends Component {
+ {
+ isEditorActive &&
+
+
+
+ }
+
);
@@ -333,7 +343,10 @@ BookIndexPosters.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
- timeFormat: PropTypes.string.isRequired
+ timeFormat: PropTypes.string.isRequired,
+ selectedState: PropTypes.object.isRequired,
+ onSelectedChange: PropTypes.func.isRequired,
+ isEditorActive: PropTypes.bool.isRequired
};
export default BookIndexPosters;
diff --git a/frontend/src/Book/Index/Table/BookIndexHeader.js b/frontend/src/Book/Index/Table/BookIndexHeader.js
index d15f99458..55da9fc15 100644
--- a/frontend/src/Book/Index/Table/BookIndexHeader.js
+++ b/frontend/src/Book/Index/Table/BookIndexHeader.js
@@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
+import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import BookIndexTableOptionsConnector from './BookIndexTableOptionsConnector';
import styles from './BookIndexHeader.css';
@@ -13,6 +14,10 @@ function BookIndexHeader(props) {
const {
columns,
onTableOptionChange,
+ allSelected,
+ allUnselected,
+ onSelectAllChange,
+ isEditorActive,
...otherProps
} = props;
@@ -31,6 +36,21 @@ function BookIndexHeader(props) {
return null;
}
+ if (name === 'select') {
+ if (isEditorActive) {
+ return (
+
+ );
+ }
+
+ return null;
+ }
+
if (name === 'actions') {
return (
+ );
+ }
+
if (name === 'status') {
return (
{
const {
items,
- columns
+ columns,
+ selectedState,
+ onSelectedChange,
+ isEditorActive
} = this.props;
const book = items[rowIndex];
@@ -64,6 +67,9 @@ class BookIndexTable extends Component {
columns={columns}
authorId={book.authorId}
bookId={book.id}
+ isSelected={selectedState[book.id]}
+ onSelectedChange={onSelectedChange}
+ isEditorActive={isEditorActive}
/>
);
@@ -81,7 +87,12 @@ class BookIndexTable extends Component {
isSmallScreen,
onSortPress,
scroller,
- scrollTop
+ scrollTop,
+ allSelected,
+ allUnselected,
+ onSelectAllChange,
+ isEditorActive,
+ selectedState
} = this.props;
return (
@@ -101,8 +112,13 @@ class BookIndexTable extends Component {
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
+ allSelected={allSelected}
+ allUnselected={allUnselected}
+ onSelectAllChange={onSelectAllChange}
+ isEditorActive={isEditorActive}
/>
}
+ selectedState={selectedState}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
@@ -120,7 +136,13 @@ BookIndexTable.propTypes = {
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
- onSortPress: PropTypes.func.isRequired
+ onSortPress: PropTypes.func.isRequired,
+ allSelected: PropTypes.bool.isRequired,
+ allUnselected: PropTypes.bool.isRequired,
+ selectedState: PropTypes.object.isRequired,
+ onSelectedChange: PropTypes.func.isRequired,
+ onSelectAllChange: PropTypes.func.isRequired,
+ isEditorActive: PropTypes.bool.isRequired
};
export default BookIndexTable;
diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js
index a18b5742d..c3e4aaf83 100644
--- a/frontend/src/Commands/commandNames.js
+++ b/frontend/src/Commands/commandNames.js
@@ -12,7 +12,9 @@ export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_BOOK_SEARCH = 'MissingBookSearch';
export const MOVE_AUTHOR = 'MoveAuthor';
export const REFRESH_AUTHOR = 'RefreshAuthor';
+export const BULK_REFRESH_AUTHOR = 'BulkRefreshAuthor';
export const REFRESH_BOOK = 'RefreshBook';
+export const BULK_REFRESH_BOOK = 'BulkRefreshBook';
export const RENAME_FILES = 'RenameFiles';
export const RENAME_AUTHOR = 'RenameAuthor';
export const RESCAN_FOLDERS = 'RescanFolders';
diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js
index e29ecbbfe..f213d8a52 100644
--- a/frontend/src/Components/Page/Sidebar/PageSidebar.js
+++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js
@@ -36,10 +36,6 @@ const links = [
title: 'Add New',
to: '/add/search'
},
- {
- title: 'Mass Editor',
- to: '/authoreditor'
- },
{
title: 'Bookshelf',
to: '/shelf'
diff --git a/frontend/src/Store/Actions/authorEditorActions.js b/frontend/src/Store/Actions/authorEditorActions.js
deleted file mode 100644
index 583784cb6..000000000
--- a/frontend/src/Store/Actions/authorEditorActions.js
+++ /dev/null
@@ -1,243 +0,0 @@
-import { createAction } from 'redux-actions';
-import { batchActions } from 'redux-batched-actions';
-import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
-import { createThunk, handleThunks } from 'Store/thunks';
-import createAjaxRequest from 'Utilities/createAjaxRequest';
-import { filterPredicates, filters, sortPredicates } from './authorActions';
-import { set, updateItem } from './baseActions';
-import createHandleActions from './Creators/createHandleActions';
-import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
-import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
-import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
-
-//
-// Variables
-
-export const section = 'authorEditor';
-
-//
-// State
-
-export const defaultState = {
- isSaving: false,
- saveError: null,
- isDeleting: false,
- deleteError: null,
- sortKey: 'sortName',
- sortDirection: sortDirections.ASCENDING,
- secondarySortKey: 'sortName',
- secondarySortDirection: sortDirections.ASCENDING,
- selectedFilterKey: 'all',
- filters,
- filterPredicates,
-
- columns: [
- {
- name: 'status',
- columnLabel: 'Status',
- isSortable: true,
- isVisible: true,
- isModifiable: false
- },
- {
- name: 'sortName',
- label: 'Name',
- isSortable: true,
- isVisible: true
- },
- {
- name: 'qualityProfileId',
- label: 'Quality Profile',
- isSortable: true,
- isVisible: true
- },
- {
- name: 'metadataProfileId',
- label: 'Metadata Profile',
- isSortable: true,
- isVisible: true
- },
- {
- name: 'path',
- label: 'Path',
- isSortable: true,
- isVisible: true
- },
- {
- name: 'sizeOnDisk',
- label: 'Size on Disk',
- isSortable: true,
- isVisible: false
- },
- {
- name: 'tags',
- label: 'Tags',
- isSortable: false,
- isVisible: true
- }
- ],
-
- filterBuilderProps: [
- {
- name: 'monitored',
- label: 'Monitored',
- type: filterBuilderTypes.EXACT,
- valueType: filterBuilderValueTypes.BOOL
- },
- {
- name: 'status',
- label: 'Status',
- type: filterBuilderTypes.EXACT,
- valueType: filterBuilderValueTypes.AUTHOR_STATUS
- },
- {
- name: 'qualityProfileId',
- label: 'Quality Profile',
- type: filterBuilderTypes.EXACT,
- valueType: filterBuilderValueTypes.QUALITY_PROFILE
- },
- {
- name: 'metadataProfileId',
- label: 'Metadata Profile',
- type: filterBuilderTypes.EXACT,
- valueType: filterBuilderValueTypes.METADATA_PROFILE
- },
- {
- name: 'path',
- label: 'Path',
- type: filterBuilderTypes.STRING
- },
- {
- name: 'rootFolderPath',
- label: 'Root Folder Path',
- type: filterBuilderTypes.EXACT
- },
- {
- name: 'sizeOnDisk',
- label: 'Size on Disk',
- type: filterBuilderTypes.NUMBER,
- valueType: filterBuilderValueTypes.BYTES
- },
- {
- name: 'tags',
- label: 'Tags',
- type: filterBuilderTypes.ARRAY,
- valueType: filterBuilderValueTypes.TAG
- }
- ],
-
- sortPredicates
-};
-
-export const persistState = [
- 'authorEditor.sortKey',
- 'authorEditor.sortDirection',
- 'authorEditor.selectedFilterKey',
- 'authorEditor.customFilters'
-];
-
-//
-// Actions Types
-
-export const SET_AUTHOR_EDITOR_SORT = 'authorEditor/setAuthorEditorSort';
-export const SET_AUTHOR_EDITOR_FILTER = 'authorEditor/setAuthorEditorFilter';
-export const SAVE_AUTHOR_EDITOR = 'authorEditor/saveAuthorEditor';
-export const BULK_DELETE_AUTHOR = 'authorEditor/bulkDeleteAuthor';
-export const SET_AUTHOR_EDITOR_TABLE_OPTION = 'authorEditor/setAuthorEditorTableOption';
-
-//
-// Action Creators
-
-export const setAuthorEditorSort = createAction(SET_AUTHOR_EDITOR_SORT);
-export const setAuthorEditorFilter = createAction(SET_AUTHOR_EDITOR_FILTER);
-export const saveAuthorEditor = createThunk(SAVE_AUTHOR_EDITOR);
-export const bulkDeleteAuthor = createThunk(BULK_DELETE_AUTHOR);
-export const setAuthorEditorTableOption = createAction(SET_AUTHOR_EDITOR_TABLE_OPTION);
-
-//
-// Action Handlers
-
-export const actionHandlers = handleThunks({
- [SAVE_AUTHOR_EDITOR]: function(getState, payload, dispatch) {
- dispatch(set({
- section,
- isSaving: true
- }));
-
- const promise = createAjaxRequest({
- url: '/author/editor',
- method: 'PUT',
- data: JSON.stringify(payload),
- dataType: 'json'
- }).request;
-
- promise.done((data) => {
- dispatch(batchActions([
- ...data.map((author) => {
- return updateItem({
- id: author.id,
- section: 'authors',
- ...author
- });
- }),
-
- set({
- section,
- isSaving: false,
- saveError: null
- })
- ]));
- });
-
- promise.fail((xhr) => {
- dispatch(set({
- section,
- isSaving: false,
- saveError: xhr
- }));
- });
- },
-
- [BULK_DELETE_AUTHOR]: function(getState, payload, dispatch) {
- dispatch(set({
- section,
- isDeleting: true
- }));
-
- const promise = createAjaxRequest({
- url: '/author/editor',
- method: 'DELETE',
- data: JSON.stringify(payload),
- dataType: 'json'
- }).request;
-
- promise.done(() => {
- // SignalR will take care of removing the author 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({
-
- [SET_AUTHOR_EDITOR_TABLE_OPTION]: createSetTableOptionReducer(section),
- [SET_AUTHOR_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
- [SET_AUTHOR_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
-
-}, defaultState, section);
diff --git a/frontend/src/Store/Actions/authorIndexActions.js b/frontend/src/Store/Actions/authorIndexActions.js
index e6b332fb9..caba32fa0 100644
--- a/frontend/src/Store/Actions/authorIndexActions.js
+++ b/frontend/src/Store/Actions/authorIndexActions.js
@@ -1,7 +1,11 @@
import { createAction } from 'redux-actions';
+import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props';
+import { createThunk, handleThunks } from 'Store/thunks';
import sortByName from 'Utilities/Array/sortByName';
+import createAjaxRequest from 'Utilities/createAjaxRequest';
import { filterPredicates, filters, sortPredicates } from './authorActions';
+import { set, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
@@ -16,6 +20,10 @@ export const section = 'authorIndex';
// State
export const defaultState = {
+ isSaving: false,
+ saveError: null,
+ isDeleting: false,
+ deleteError: null,
sortKey: 'sortNameLastFirst',
sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'sortNameLastFirst',
@@ -52,6 +60,14 @@ export const defaultState = {
},
columns: [
+ {
+ name: 'select',
+ columnLabel: 'Select',
+ isSortable: false,
+ isVisible: true,
+ isModifiable: false,
+ isHidden: true
+ },
{
name: 'status',
columnLabel: 'Status',
@@ -328,6 +344,8 @@ export const SET_AUTHOR_TABLE_OPTION = 'authorIndex/setAuthorTableOption';
export const SET_AUTHOR_POSTER_OPTION = 'authorIndex/setAuthorPosterOption';
export const SET_AUTHOR_BANNER_OPTION = 'authorIndex/setAuthorBannerOption';
export const SET_AUTHOR_OVERVIEW_OPTION = 'authorIndex/setAuthorOverviewOption';
+export const SAVE_AUTHOR_EDITOR = 'authorIndex/saveAuthorEditor';
+export const BULK_DELETE_AUTHOR = 'authorIndex/bulkDeleteAuthor';
//
// Action Creators
@@ -339,6 +357,85 @@ export const setAuthorTableOption = createAction(SET_AUTHOR_TABLE_OPTION);
export const setAuthorPosterOption = createAction(SET_AUTHOR_POSTER_OPTION);
export const setAuthorBannerOption = createAction(SET_AUTHOR_BANNER_OPTION);
export const setAuthorOverviewOption = createAction(SET_AUTHOR_OVERVIEW_OPTION);
+export const saveAuthorEditor = createThunk(SAVE_AUTHOR_EDITOR);
+export const bulkDeleteAuthor = createThunk(BULK_DELETE_AUTHOR);
+
+//
+// Action Handlers
+
+export const actionHandlers = handleThunks({
+ [SAVE_AUTHOR_EDITOR]: function(getState, payload, dispatch) {
+ dispatch(set({
+ section,
+ isSaving: true
+ }));
+
+ const promise = createAjaxRequest({
+ url: '/author/editor',
+ method: 'PUT',
+ data: JSON.stringify(payload),
+ dataType: 'json'
+ }).request;
+
+ promise.done((data) => {
+ dispatch(batchActions([
+ ...data.map((author) => {
+ return updateItem({
+ id: author.id,
+ section: 'authors',
+ ...author
+ });
+ }),
+
+ set({
+ section,
+ isSaving: false,
+ saveError: null
+ })
+ ]));
+ });
+
+ promise.fail((xhr) => {
+ dispatch(set({
+ section,
+ isSaving: false,
+ saveError: xhr
+ }));
+ });
+ },
+
+ [BULK_DELETE_AUTHOR]: function(getState, payload, dispatch) {
+ dispatch(set({
+ section,
+ isDeleting: true
+ }));
+
+ const promise = createAjaxRequest({
+ url: '/author/editor',
+ method: 'DELETE',
+ data: JSON.stringify(payload),
+ dataType: 'json'
+ }).request;
+
+ promise.done(() => {
+ // SignaR will take care of removing the author from the collection
+
+ dispatch(set({
+ section,
+ isDeleting: false,
+ deleteError: null
+ }));
+ });
+
+ promise.fail((xhr) => {
+ dispatch(set({
+ section,
+ isDeleting: false,
+ deleteError: xhr
+ }));
+ });
+ }
+});
//
// Reducers
diff --git a/frontend/src/Store/Actions/bookEditorActions.js b/frontend/src/Store/Actions/bookEditorActions.js
deleted file mode 100644
index 83e53e414..000000000
--- a/frontend/src/Store/Actions/bookEditorActions.js
+++ /dev/null
@@ -1,114 +0,0 @@
-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);
diff --git a/frontend/src/Store/Actions/bookIndexActions.js b/frontend/src/Store/Actions/bookIndexActions.js
index 8c5a52e30..da38b4238 100644
--- a/frontend/src/Store/Actions/bookIndexActions.js
+++ b/frontend/src/Store/Actions/bookIndexActions.js
@@ -1,6 +1,10 @@
import { createAction } from 'redux-actions';
+import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
+import { createThunk, handleThunks } from 'Store/thunks';
import sortByName from 'Utilities/Array/sortByName';
+import createAjaxRequest from 'Utilities/createAjaxRequest';
+import { set, updateItem } from './baseActions';
import { filterPredicates, filters, sortPredicates } from './bookActions';
import createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
@@ -16,6 +20,10 @@ export const section = 'bookIndex';
// State
export const defaultState = {
+ isSaving: false,
+ saveError: null,
+ isDeleting: false,
+ deleteError: null,
sortKey: 'title',
sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'title',
@@ -49,6 +57,14 @@ export const defaultState = {
},
columns: [
+ {
+ name: 'select',
+ columnLabel: 'Select',
+ isSortable: false,
+ isVisible: true,
+ isModifiable: false,
+ isHidden: true
+ },
{
name: 'status',
columnLabel: 'Status',
@@ -253,6 +269,8 @@ export const SET_BOOK_TABLE_OPTION = 'bookIndex/setBookTableOption';
export const SET_BOOK_POSTER_OPTION = 'bookIndex/setBookPosterOption';
export const SET_BOOK_BANNER_OPTION = 'bookIndex/setBookBannerOption';
export const SET_BOOK_OVERVIEW_OPTION = 'bookIndex/setBookOverviewOption';
+export const SAVE_BOOK_EDITOR = 'bookEditor/saveBookEditor';
+export const BULK_DELETE_BOOK = 'bookEditor/bulkDeleteBook';
//
// Action Creators
@@ -264,6 +282,85 @@ export const setBookTableOption = createAction(SET_BOOK_TABLE_OPTION);
export const setBookPosterOption = createAction(SET_BOOK_POSTER_OPTION);
export const setBookBannerOption = createAction(SET_BOOK_BANNER_OPTION);
export const setBookOverviewOption = createAction(SET_BOOK_OVERVIEW_OPTION);
+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
diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js
index 1627bff02..5873e8384 100644
--- a/frontend/src/Store/Actions/index.js
+++ b/frontend/src/Store/Actions/index.js
@@ -1,12 +1,10 @@
import * as app from './appActions';
import * as author from './authorActions';
import * as authorDetails from './authorDetailsActions';
-import * as authorEditor from './authorEditorActions';
import * as authorHistory from './authorHistoryActions';
import * as authorIndex from './authorIndexActions';
import * as blocklist from './blocklistActions';
import * as books from './bookActions';
-import * as bookEditor from './bookEditorActions';
import * as bookFiles from './bookFileActions';
import * as bookHistory from './bookHistoryActions';
import * as bookIndex from './bookIndexActions';
@@ -36,7 +34,6 @@ export default [
app,
author,
authorDetails,
- authorEditor,
authorHistory,
authorIndex,
blocklist,
@@ -44,7 +41,6 @@ export default [
bookHistory,
bookIndex,
books,
- bookEditor,
bookStudio,
calendar,
captcha,
diff --git a/frontend/src/Store/Selectors/createBookQualityProfileSelector.js b/frontend/src/Store/Selectors/createBookQualityProfileSelector.js
index 14ad1db2d..5ad877c77 100644
--- a/frontend/src/Store/Selectors/createBookQualityProfileSelector.js
+++ b/frontend/src/Store/Selectors/createBookQualityProfileSelector.js
@@ -5,7 +5,11 @@ function createBookQualityProfileSelector() {
return createSelector(
(state) => state.settings.qualityProfiles.items,
createBookSelector(),
- (qualityProfiles, book = {}) => {
+ (qualityProfiles, book) => {
+ if (!book) {
+ return {};
+ }
+
return qualityProfiles.find((profile) => {
return profile.id === book.author.qualityProfileId;
});
diff --git a/frontend/src/Store/Selectors/createBookSelector.js b/frontend/src/Store/Selectors/createBookSelector.js
index 6797f3107..bac4bcea4 100644
--- a/frontend/src/Store/Selectors/createBookSelector.js
+++ b/frontend/src/Store/Selectors/createBookSelector.js
@@ -1,13 +1,12 @@
-import _ from 'lodash';
import { createSelector } from 'reselect';
-import bookEntities from 'Book/bookEntities';
function createBookSelector() {
return createSelector(
(state, { bookId }) => bookId,
- (state, { bookEntity = bookEntities.BOOKS }) => _.get(state, bookEntity, { items: [] }),
- (bookId, books) => {
- return _.find(books.items, { id: bookId });
+ (state) => state.books.itemMap,
+ (state) => state.books.items,
+ (bookId, itemMap, allBooks) => {
+ return allBooks[itemMap[bookId]];
}
);
}
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index beeaeda2f..235d254b6 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -46,6 +46,7 @@
"AuthorClickToChangeBook": "Click to change book",
"AuthorEditor": "Author Editor",
"AuthorFolderFormat": "Author Folder Format",
+ "AuthorIndex": "Author Index",
"AuthorNameHelpText": "The name of the author/book to exclude (can be anything meaningful)",
"Authors": "Authors",
"Automatic": "Automatic",
@@ -67,6 +68,7 @@
"BookAvailableButMissing": "Book Available, but Missing",
"BookDownloaded": "Book Downloaded",
"BookEditor": "Book Editor",
+ "BookIndex": "Book Index",
"BookFileCountBookCountTotalTotalBookCountInterp": "{0} / {1} (Total: {2})",
"BookFileCounttotalBookCountBooksDownloadedInterp": "{0}/{1} books downloaded",
"BookFilesCountMessage": "No book files",
@@ -343,6 +345,8 @@
"ManualImport": "Manual Import",
"MarkAsFailed": "Mark as Failed",
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
+ "MassBookSearch": "Mass Book Search",
+ "MassBookSearchWarning": "Are you sure you want to perform mass book search for {0} books?",
"MaximumLimits": "Maximum Limits",
"MaximumSize": "Maximum Size",
"MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.",
@@ -649,6 +653,7 @@
"TheAuthorFolderAndAllOfItsContentWillBeDeleted": "The author folder {0} and all of its content will be deleted.",
"TheBooksFilesWillBeDeleted": "The book's files will be deleted.",
"TheFollowingFilesWillBeDeleted": "The following files will be deleted:",
+ "ThisCannotBeCancelled": "This cannot be cancelled once started without restarting Readarr.",
"ThisWillApplyToAllIndexersPleaseFollowTheRulesSetForthByThem": "This will apply to all indexers, please follow the rules set forth by them",
"Time": "Time",
"TimeFormat": "Time Format",
@@ -713,6 +718,7 @@
"UnmonitoredHelpText": "Include unmonitored books in the iCal feed",
"UnselectAll": "Unselect All",
"UpdateAll": "Update all",
+ "UpdateSelected": "Updated selected",
"UpdateAutomaticallyHelpText": "Automatically download and install updates. You will still be able to install from System: Updates",
"UpdateCovers": "Update Covers",
"UpdateCoversHelpText": "Set book covers in Calibre to match those in Readarr",
diff --git a/src/Readarr.Api.V1/Books/BookEditorController.cs b/src/Readarr.Api.V1/Books/BookEditorController.cs
index 7a4e9ea1e..9d260bc7e 100644
--- a/src/Readarr.Api.V1/Books/BookEditorController.cs
+++ b/src/Readarr.Api.V1/Books/BookEditorController.cs
@@ -35,14 +35,12 @@ public IActionResult SaveAll([FromBody] BookEditorResource resource)
}
[HttpDelete]
- public object DeleteBook([FromBody] BookEditorResource resource)
+ public void DeleteBook([FromBody] BookEditorResource resource)
{
foreach (var bookId in resource.BookIds)
{
_bookService.DeleteBook(bookId, resource.DeleteFiles ?? false, resource.AddImportListExclusion ?? false);
}
-
- return new object();
}
}
}