diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 9859502ab..99d3e4098 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -5,7 +5,6 @@ import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector'; import HistoryConnector from 'Activity/History/HistoryConnector'; import QueueConnector from 'Activity/Queue/QueueConnector'; import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector'; -import AuthorEditorConnector from 'Author/Editor/AuthorEditorConnector'; import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector'; import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector'; import BookIndexConnector from 'Book/Index/BookIndexConnector'; @@ -82,11 +81,6 @@ function AppRoutes(props) { component={AddNewItemConnector} /> - - { - this.setState({ isBookEditorActive: !this.state.isBookEditorActive }); + this.setState({ isEditorActive: !this.state.isEditorActive }); } onExpandAllPress = () => { @@ -249,7 +249,7 @@ class AuthorDetails extends Component { isDeleteAuthorModalOpen, isInteractiveImportModalOpen, isMonitorOptionsModalOpen, - isBookEditorActive, + isEditorActive, allSelected, selectedState, allExpanded, @@ -335,7 +335,7 @@ class AuthorDetails extends Component { { - isBookEditorActive ? + isEditorActive ? @@ -585,7 +585,7 @@ class AuthorDetails extends Component { { - isBookEditorActive && + isEditorActive && state.books, - (state) => state.bookEditor, - (books, editor) => { + (state) => state.bookIndex, + (books, index) => { const { items, isFetching, @@ -31,6 +31,13 @@ const selectBooks = createSelector( error } = books; + const { + isSaving, + saveError, + isDeleting, + deleteError + } = index; + const hasBooks = !!items.length; const hasMonitoredBooks = items.some((e) => e.monitored); @@ -40,7 +47,10 @@ const selectBooks = createSelector( booksError: error, hasBooks, hasMonitoredBooks, - ...editor + isSaving, + saveError, + isDeleting, + deleteError }; } ); @@ -112,7 +122,11 @@ function createMapStateToProps() { isBooksPopulated, booksError, hasBooks, - hasMonitoredBooks + hasMonitoredBooks, + isSaving, + saveError, + isDeleting, + deleteError } = books; const { @@ -172,6 +186,10 @@ function createMapStateToProps() { isFetching, isPopulated, booksError, + isSaving, + saveError, + isDeleting, + deleteError, seriesError, bookFilesError, hasBooks, diff --git a/frontend/src/Author/Details/AuthorDetailsSeason.js b/frontend/src/Author/Details/AuthorDetailsSeason.js index 34ea09b38..c19ccf38d 100644 --- a/frontend/src/Author/Details/AuthorDetailsSeason.js +++ b/frontend/src/Author/Details/AuthorDetailsSeason.js @@ -78,7 +78,7 @@ class AuthorDetailsSeason extends Component { render() { const { items, - isBookEditorActive, + isEditorActive, columns, sortKey, sortDirection, @@ -88,7 +88,7 @@ class AuthorDetailsSeason extends Component { } = this.props; let titleColumns = columns; - if (!isBookEditorActive) { + if (!isEditorActive) { titleColumns = columns.filter((x) => x.name !== 'select'); } @@ -113,7 +113,7 @@ class AuthorDetailsSeason extends Component { columns={columns} {...item} onMonitorBookPress={this.onMonitorBookPress} - isBookEditorActive={isBookEditorActive} + isEditorActive={isEditorActive} isSelected={selectedState[item.id]} onSelectedChange={this.onSelectedChange} /> @@ -132,7 +132,7 @@ AuthorDetailsSeason.propTypes = { sortKey: PropTypes.string, sortDirection: PropTypes.oneOf(sortDirections.all), items: PropTypes.arrayOf(PropTypes.object).isRequired, - isBookEditorActive: PropTypes.bool.isRequired, + isEditorActive: PropTypes.bool.isRequired, selectedState: PropTypes.object.isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, onTableOptionChange: PropTypes.func.isRequired, diff --git a/frontend/src/Author/Details/BookRow.js b/frontend/src/Author/Details/BookRow.js index 969b38aed..b337e2280 100644 --- a/frontend/src/Author/Details/BookRow.js +++ b/frontend/src/Author/Details/BookRow.js @@ -66,7 +66,7 @@ class BookRow extends Component { authorMonitored, titleSlug, bookFiles, - isBookEditorActive, + isEditorActive, isSelected, onSelectedChange, columns @@ -88,7 +88,7 @@ class BookRow extends Component { return null; } - if (isBookEditorActive && name === 'select') { + if (isEditorActive && name === 'select') { return ( { - return getSelectedIds(this.state.selectedState); - } - - // - // Listeners - - onSelectAllChange = ({ value }) => { - this.setState(selectAll(this.state.selectedState, value)); - } - - onSelectedChange = ({ id, value, shiftKey = false }) => { - this.setState((state) => { - return toggleSelected(state, this.props.items, id, value, shiftKey); - }); - } - - onSaveSelected = (changes) => { - this.props.onSaveSelected({ - authorIds: this.getSelectedIds(), - ...changes - }); - } - - onOrganizeAuthorPress = () => { - this.setState({ isOrganizingAuthorModalOpen: true }); - } - - onOrganizeAuthorModalClose = (organized) => { - this.setState({ isOrganizingAuthorModalOpen: false }); - - if (organized === true) { - this.onSelectAllChange({ value: false }); - } - } - - onRetagAuthorPress = () => { - this.setState({ isRetaggingAuthorModalOpen: true }); - } - - onRetagAuthorModalClose = (organized) => { - this.setState({ isRetaggingAuthorModalOpen: false }); - - if (organized === true) { - this.onSelectAllChange({ value: false }); - } - } - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - totalItems, - items, - columns, - selectedFilterKey, - filters, - customFilters, - sortKey, - sortDirection, - isSaving, - saveError, - isDeleting, - deleteError, - isOrganizingAuthor, - isRetaggingAuthor, - onTableOptionChange, - onSortPress, - onFilterSelect - } = this.props; - - const { - allSelected, - allUnselected, - selectedState - } = this.state; - - const selectedAuthorIds = this.getSelectedIds(); - - return ( - - - - - - - - - - - - - - - - { - isFetching && !isPopulated && - - } - - { - !isFetching && !!error && -
{getErrorMessage(error, 'Failed to load author from API')}
- } - - { - !error && isPopulated && !!items.length && -
- - - { - items.map((item) => { - return ( - - ); - }) - } - -
-
- } - - { - !error && isPopulated && !items.length && - - } -
- - column.name === 'metadataProfileId').isVisible} - onSaveSelected={this.onSaveSelected} - onOrganizeAuthorPress={this.onOrganizeAuthorPress} - onRetagAuthorPress={this.onRetagAuthorPress} - /> - - - - - -
- ); - } -} - -AuthorEditor.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - totalItems: PropTypes.number.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, - sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), - selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, - customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - isDeleting: PropTypes.bool.isRequired, - deleteError: PropTypes.object, - isOrganizingAuthor: PropTypes.bool.isRequired, - isRetaggingAuthor: PropTypes.bool.isRequired, - onTableOptionChange: PropTypes.func.isRequired, - onSortPress: PropTypes.func.isRequired, - onFilterSelect: PropTypes.func.isRequired, - onSaveSelected: PropTypes.func.isRequired -}; - -export default AuthorEditor; diff --git a/frontend/src/Author/Editor/AuthorEditorConnector.js b/frontend/src/Author/Editor/AuthorEditorConnector.js deleted file mode 100644 index befa215d9..000000000 --- a/frontend/src/Author/Editor/AuthorEditorConnector.js +++ /dev/null @@ -1,97 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort, setAuthorEditorTableOption } from 'Store/Actions/authorEditorActions'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchRootFolders } from 'Store/Actions/settingsActions'; -import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import AuthorEditor from './AuthorEditor'; - -function createMapStateToProps() { - return createSelector( - createClientSideCollectionSelector('authors', 'authorEditor'), - createCommandExecutingSelector(commandNames.RENAME_AUTHOR), - createCommandExecutingSelector(commandNames.RETAG_AUTHOR), - (author, isOrganizingAuthor, isRetaggingAuthor) => { - return { - isOrganizingAuthor, - isRetaggingAuthor, - ...author - }; - } - ); -} - -const mapDispatchToProps = { - dispatchSetAuthorEditorSort: setAuthorEditorSort, - dispatchSetAuthorEditorFilter: setAuthorEditorFilter, - dispatchSetAuthorEditorTableOption: setAuthorEditorTableOption, - dispatchSaveAuthorEditor: saveAuthorEditor, - dispatchFetchRootFolders: fetchRootFolders, - dispatchExecuteCommand: executeCommand -}; - -class AuthorEditorConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchRootFolders(); - } - - // - // Listeners - - onSortPress = (sortKey) => { - this.props.dispatchSetAuthorEditorSort({ sortKey }); - } - - onFilterSelect = (selectedFilterKey) => { - this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey }); - } - - onTableOptionChange = (payload) => { - this.props.dispatchSetAuthorEditorTableOption(payload); - } - - onSaveSelected = (payload) => { - this.props.dispatchSaveAuthorEditor(payload); - } - - onMoveSelected = (payload) => { - this.props.dispatchExecuteCommand({ - name: commandNames.MOVE_AUTHOR, - ...payload - }); - } - - // - // Render - - render() { - return ( - - ); - } -} - -AuthorEditorConnector.propTypes = { - dispatchSetAuthorEditorSort: PropTypes.func.isRequired, - dispatchSetAuthorEditorFilter: PropTypes.func.isRequired, - dispatchSetAuthorEditorTableOption: PropTypes.func.isRequired, - dispatchSaveAuthorEditor: PropTypes.func.isRequired, - dispatchFetchRootFolders: PropTypes.func.isRequired, - dispatchExecuteCommand: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AuthorEditorConnector); diff --git a/frontend/src/Author/Editor/AuthorEditorFilterModalConnector.js b/frontend/src/Author/Editor/AuthorEditorFilterModalConnector.js deleted file mode 100644 index 2fa330244..000000000 --- a/frontend/src/Author/Editor/AuthorEditorFilterModalConnector.js +++ /dev/null @@ -1,24 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import FilterModal from 'Components/Filter/FilterModal'; -import { setAuthorEditorFilter } from 'Store/Actions/authorEditorActions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.authors.items, - (state) => state.authorEditor.filterBuilderProps, - (sectionItems, filterBuilderProps) => { - return { - sectionItems, - filterBuilderProps, - customFilterType: 'authorEditor' - }; - } - ); -} - -const mapDispatchToProps = { - dispatchSetFilter: setAuthorEditorFilter -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal); diff --git a/frontend/src/Author/Editor/AuthorEditorFooter.js b/frontend/src/Author/Editor/AuthorEditorFooter.js index b74b78455..d55730e8e 100644 --- a/frontend/src/Author/Editor/AuthorEditorFooter.js +++ b/frontend/src/Author/Editor/AuthorEditorFooter.js @@ -139,7 +139,6 @@ class AuthorEditorFooter extends Component { isDeleting, isOrganizingAuthor, isRetaggingAuthor, - columns, onOrganizeAuthorPress, onRetagAuthorPress } = this.props; @@ -179,87 +178,58 @@ class AuthorEditorFooter extends Component { /> - { - columns.map((column) => { - const { - name, - isVisible - } = column; +
+ - if (!isVisible) { - return null; - } + +
- if (name === 'qualityProfileId') { - return ( -
- +
+ - -
- ); - } + +
- if (name === 'metadataProfileId') { - return ( -
- +
+ - -
- ); - } - - if (name === 'path') { - return ( -
- - - -
- ); - } - - return null; - }) - } + +
@@ -348,7 +318,6 @@ AuthorEditorFooter.propTypes = { isOrganizingAuthor: PropTypes.bool.isRequired, isRetaggingAuthor: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired, - columns: PropTypes.arrayOf(PropTypes.object).isRequired, onSaveSelected: PropTypes.func.isRequired, onOrganizeAuthorPress: PropTypes.func.isRequired, onRetagAuthorPress: PropTypes.func.isRequired diff --git a/frontend/src/Author/Editor/Delete/DeleteAuthorModalContentConnector.js b/frontend/src/Author/Editor/Delete/DeleteAuthorModalContentConnector.js index 399710ebd..fe32448bd 100644 --- a/frontend/src/Author/Editor/Delete/DeleteAuthorModalContentConnector.js +++ b/frontend/src/Author/Editor/Delete/DeleteAuthorModalContentConnector.js @@ -1,7 +1,7 @@ import _ from 'lodash'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { bulkDeleteAuthor } from 'Store/Actions/authorEditorActions'; +import { bulkDeleteAuthor } from 'Store/Actions/authorIndexActions'; import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector'; import DeleteAuthorModalContent from './DeleteAuthorModalContent'; diff --git a/frontend/src/Author/Index/AuthorIndex.js b/frontend/src/Author/Index/AuthorIndex.js index 218486540..517f4e144 100644 --- a/frontend/src/Author/Index/AuthorIndex.js +++ b/frontend/src/Author/Index/AuthorIndex.js @@ -1,6 +1,9 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import RetagAuthorModal from 'Author/Editor/AudioTags/RetagAuthorModal'; +import AuthorEditorFooter from 'Author/Editor/AuthorEditorFooter'; +import OrganizeAuthorModal from 'Author/Editor/Organize/OrganizeAuthorModal'; import NoAuthor from 'Author/NoAuthor'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; @@ -15,6 +18,9 @@ import { align, icons, 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 AuthorIndexFooterConnector from './AuthorIndexFooterConnector'; import AuthorIndexFilterMenu from './Menus/AuthorIndexFilterMenu'; import AuthorIndexSortMenu from './Menus/AuthorIndexSortMenu'; @@ -52,12 +58,20 @@ class AuthorIndex extends Component { jumpBarItems: { order: [] }, jumpToCharacter: null, isPosterOptionsModalOpen: false, - isOverviewOptionsModalOpen: false + isOverviewOptionsModalOpen: false, + isEditorActive: false, + isOrganizingAuthorModalOpen: false, + isRetaggingAuthorModalOpen: false, + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} }; } componentDidMount() { this.setJumpBarItems(); + this.setSelectedState(); } componentDidUpdate(prevProps) { @@ -72,6 +86,7 @@ class AuthorIndex extends Component { hasDifferentItemsOrOrder(prevProps.items, items) ) { this.setJumpBarItems(); + this.setSelectedState(); } if (this.state.jumpToCharacter != null) { @@ -86,6 +101,48 @@ class AuthorIndex 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((author) => { + const isItemSelected = selectedState[author.id]; + + if (isItemSelected) { + newSelectedState[author.id] = isItemSelected; + } else { + newSelectedState[author.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, @@ -149,10 +206,72 @@ class AuthorIndex 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({ + authorIds: this.getSelectedIds(), + ...changes + }); + } + + onOrganizeAuthorPress = () => { + this.setState({ isOrganizingAuthorModalOpen: true }); + } + + onOrganizeAuthorModalClose = (organized) => { + this.setState({ isOrganizingAuthorModalOpen: false }); + + if (organized === true) { + this.onSelectAllChange({ value: false }); + } + } + + onRetagAuthorPress = () => { + this.setState({ isRetaggingAuthorModalOpen: true }); + } + + onRetagAuthorModalClose = (organized) => { + this.setState({ isRetaggingAuthorModalOpen: false }); + + if (organized === true) { + this.onSelectAllChange({ value: false }); + } + } + + onRefreshAuthorPress = () => { + const selectedIds = this.getSelectedIds(); + const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : []; + + this.props.onRefreshAuthorPress(refreshIds); + } + // // Render @@ -172,11 +291,16 @@ class AuthorIndex extends Component { view, isRefreshingAuthor, isRssSyncExecuting, + isOrganizingAuthor, + isRetaggingAuthor, + isSaving, + saveError, + isDeleting, + deleteError, onScroll, onSortSelect, onFilterSelect, onViewSelect, - onRefreshAuthorPress, onRssSyncPress, ...otherProps } = this.props; @@ -186,23 +310,31 @@ class AuthorIndex extends Component { jumpBarItems, jumpToCharacter, isPosterOptionsModalOpen, - isOverviewOptionsModalOpen + isOverviewOptionsModalOpen, + isEditorActive, + selectedState, + allSelected, + allUnselected } = this.state; + const selectedAuthorIds = this.getSelectedIds(); + const ViewComponent = getViewComponent(view); const isLoaded = !!(!error && isPopulated && items.length && scroller); const hasNoAuthor = !totalItems; + const refreshLabel = isEditorActive && selectedAuthorIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll'); + return ( + + + { + isEditorActive ? + : + + } + + { + isEditorActive ? + : + null + } + @@ -332,6 +499,24 @@ class AuthorIndex extends Component { }
+ { + isLoaded && isEditorActive && + + } + + + + + + ); } @@ -361,14 +558,21 @@ AuthorIndex.propTypes = { sortDirection: PropTypes.oneOf(sortDirections.all), view: PropTypes.string.isRequired, isRefreshingAuthor: PropTypes.bool.isRequired, + isOrganizingAuthor: PropTypes.bool.isRequired, + isRetaggingAuthor: 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, onRssSyncPress: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired + onScroll: PropTypes.func.isRequired, + onSaveSelected: PropTypes.func.isRequired }; export default AuthorIndex; diff --git a/frontend/src/Author/Index/AuthorIndexConnector.js b/frontend/src/Author/Index/AuthorIndexConnector.js index 024487822..0ced7009f 100644 --- a/frontend/src/Author/Index/AuthorIndexConnector.js +++ b/frontend/src/Author/Index/AuthorIndexConnector.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 { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions'; +import { saveAuthorEditor, setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions'; import { executeCommand } from 'Store/Actions/commandActions'; import scrollPositions from 'Store/scrollPositions'; import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector'; @@ -18,16 +18,22 @@ function createMapStateToProps() { createAuthorClientSideCollectionItemsSelector('authorIndex'), createCommandExecutingSelector(commandNames.REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.RSS_SYNC), + createCommandExecutingSelector(commandNames.RENAME_AUTHOR), + createCommandExecutingSelector(commandNames.RETAG_AUTHOR), createDimensionsSelector(), ( author, isRefreshingAuthor, + isOrganizingAuthor, + isRetaggingAuthor, isRssSyncExecuting, dimensionsState ) => { return { ...author, isRefreshingAuthor, + isOrganizingAuthor, + isRetaggingAuthor, isRssSyncExecuting, isSmallScreen: dimensionsState.isSmallScreen }; @@ -53,9 +59,14 @@ function createMapDispatchToProps(dispatch, props) { dispatch(setAuthorView({ view })); }, - onRefreshAuthorPress() { + dispatchSaveAuthorEditor(payload) { + dispatch(saveAuthorEditor(payload)); + }, + + onRefreshAuthorPress(items) { dispatch(executeCommand({ - name: commandNames.REFRESH_AUTHOR + name: commandNames.BULK_REFRESH_AUTHOR, + authorIds: items })); }, @@ -76,6 +87,10 @@ class AuthorIndexConnector extends Component { this.props.dispatchSetAuthorView(view); } + onSaveSelected = (payload) => { + this.props.dispatchSaveAuthorEditor(payload); + } + onScroll = ({ scrollTop }) => { scrollPositions.authorIndex = scrollTop; } @@ -89,6 +104,7 @@ class AuthorIndexConnector extends Component { {...this.props} onViewSelect={this.onViewSelect} onScroll={this.onScroll} + onSaveSelected={this.onSaveSelected} /> ); } @@ -97,7 +113,8 @@ class AuthorIndexConnector extends Component { AuthorIndexConnector.propTypes = { isSmallScreen: PropTypes.bool.isRequired, view: PropTypes.string.isRequired, - dispatchSetAuthorView: PropTypes.func.isRequired + dispatchSetAuthorView: PropTypes.func.isRequired, + dispatchSaveAuthorEditor: PropTypes.func.isRequired }; export default withScrollPosition( diff --git a/frontend/src/Author/Index/Overview/AuthorIndexOverview.css b/frontend/src/Author/Index/Overview/AuthorIndexOverview.css index f2eb3d995..48afa9f6c 100644 --- a/frontend/src/Author/Index/Overview/AuthorIndexOverview.css +++ b/frontend/src/Author/Index/Overview/AuthorIndexOverview.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/Author/Index/Overview/AuthorIndexOverview.js b/frontend/src/Author/Index/Overview/AuthorIndexOverview.js index 2d70360c6..7ff444de5 100644 --- a/frontend/src/Author/Index/Overview/AuthorIndexOverview.js +++ b/frontend/src/Author/Index/Overview/AuthorIndexOverview.js @@ -5,6 +5,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 IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; @@ -67,6 +68,15 @@ class AuthorIndexOverview extends Component { this.setState({ isDeleteAuthorModalOpen: false }); } + onChange = ({ value, shiftKey }) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + } + // // Render @@ -97,6 +107,8 @@ class AuthorIndexOverview extends Component { isSearchingAuthor, onRefreshAuthorPress, onSearchPress, + isEditorActive, + isSelected, ...otherProps } = this.props; @@ -127,6 +139,18 @@ class AuthorIndexOverview extends Component {
+ { + 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(); } } }