From cf415e61dee1e9cf8b9efe393f4fca23bd28704d Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 14 Jan 2023 23:31:01 -0600 Subject: [PATCH] New: Bulk Delete for Unmapped Files (cherry picked from commit 71c1edd47c5377bcdeeb68e9cededf122a6ce6b4) --- frontend/src/Store/Actions/bookFileActions.js | 8 + .../src/UnmappedFiles/UnmappedFilesTable.js | 137 +++++++++++++++++- .../UnmappedFilesTableConnector.js | 11 +- .../UnmappedFiles/UnmappedFilesTableHeader.js | 18 +++ .../UnmappedFiles/UnmappedFilesTableRow.css | 6 + .../UnmappedFilesTableRow.css.d.ts | 1 + .../UnmappedFiles/UnmappedFilesTableRow.js | 20 ++- src/NzbDrone.Core/Localization/Core/en.json | 1 + .../BookFiles/BookFileController.cs | 14 +- 9 files changed, 204 insertions(+), 12 deletions(-) diff --git a/frontend/src/Store/Actions/bookFileActions.js b/frontend/src/Store/Actions/bookFileActions.js index 29b3059b7..66c4b40d1 100644 --- a/frontend/src/Store/Actions/bookFileActions.js +++ b/frontend/src/Store/Actions/bookFileActions.js @@ -41,6 +41,14 @@ export const defaultState = { }, columns: [ + { + name: 'select', + columnLabel: 'Select', + isSortable: false, + isVisible: true, + isModifiable: false, + isHidden: true + }, { name: 'path', label: 'Path', diff --git a/frontend/src/UnmappedFiles/UnmappedFilesTable.js b/frontend/src/UnmappedFiles/UnmappedFilesTable.js index b7999fd8a..ee966b487 100644 --- a/frontend/src/UnmappedFiles/UnmappedFilesTable.js +++ b/frontend/src/UnmappedFiles/UnmappedFilesTable.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; @@ -9,8 +10,12 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import VirtualTable from 'Components/Table/VirtualTable'; import VirtualTableRow from 'Components/Table/VirtualTableRow'; -import { align, icons, sortDirections } from 'Helpers/Props'; +import { align, icons, kinds, sortDirections } from 'Helpers/Props'; +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 UnmappedFilesTableHeader from './UnmappedFilesTableHeader'; import UnmappedFilesTableRow from './UnmappedFilesTableRow'; @@ -23,10 +28,43 @@ class UnmappedFilesTable extends Component { super(props, context); this.state = { - scroller: null + scroller: null, + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} }; } + componentDidMount() { + this.setSelectedState(); + } + + componentDidUpdate(prevProps) { + const { + items, + sortKey, + sortDirection, + isDeleting, + deleteError + } = this.props; + + if (sortKey !== prevProps.sortKey || + sortDirection !== prevProps.sortDirection || + hasDifferentItemsOrOrder(prevProps.items, items) + ) { + this.setSelectedState(); + } + + const hasFinishedDeleting = prevProps.isDeleting && + !isDeleting && + !deleteError; + + if (hasFinishedDeleting) { + this.onSelectAllChange({ value: false }); + } + } + // // Control @@ -34,6 +72,68 @@ class UnmappedFilesTable 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((file) => { + const isItemSelected = selectedState[file.id]; + + if (isItemSelected) { + newSelectedState[file.id] = isItemSelected; + } else { + newSelectedState[file.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 }); + } + + 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); + }); + }; + + onDeleteUnmappedFilesPress = () => { + const selectedIds = this.getSelectedIds(); + + this.props.deleteUnmappedFiles(selectedIds); + }; + rowRenderer = ({ key, rowIndex, style }) => { const { items, @@ -41,6 +141,10 @@ class UnmappedFilesTable extends Component { deleteUnmappedFile } = this.props; + const { + selectedState + } = this.state; + const item = items[rowIndex]; return ( @@ -51,6 +155,8 @@ class UnmappedFilesTable extends Component { @@ -63,6 +169,7 @@ class UnmappedFilesTable extends Component { const { isFetching, isPopulated, + isDeleting, error, items, columns, @@ -72,13 +179,19 @@ class UnmappedFilesTable extends Component { onSortPress, isScanningFolders, onAddMissingAuthorsPress, + deleteUnmappedFiles, ...otherProps } = this.props; const { - scroller + scroller, + allSelected, + allUnselected, + selectedState } = this.state; + const selectedTrackFileIds = this.getSelectedIds(); + return ( @@ -90,6 +203,13 @@ class UnmappedFilesTable extends Component { isSpinning={isScanningFolders} onPress={onAddMissingAuthorsPress} /> + @@ -117,9 +237,9 @@ class UnmappedFilesTable extends Component { { isPopulated && !error && !items.length && -
+ Success! My work is done, all files on disk are matched to known books. -
+ } { @@ -138,8 +258,12 @@ class UnmappedFilesTable extends Component { sortDirection={sortDirection} onTableOptionChange={onTableOptionChange} onSortPress={onSortPress} + allSelected={allSelected} + allUnselected={allUnselected} + onSelectAllChange={this.onSelectAllChange} /> } + selectedState={selectedState} sortKey={sortKey} sortDirection={sortDirection} /> @@ -153,6 +277,8 @@ class UnmappedFilesTable extends Component { UnmappedFilesTable.propTypes = { isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, + isDeleting: PropTypes.bool.isRequired, + deleteError: PropTypes.object, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -161,6 +287,7 @@ UnmappedFilesTable.propTypes = { onTableOptionChange: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired, deleteUnmappedFile: PropTypes.func.isRequired, + deleteUnmappedFiles: PropTypes.func.isRequired, isScanningFolders: PropTypes.bool.isRequired, onAddMissingAuthorsPress: PropTypes.func.isRequired }; diff --git a/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js b/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js index 5d6df606f..1a8e15eb9 100644 --- a/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js +++ b/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import * as commandNames from 'Commands/commandNames'; import withCurrentPage from 'Components/withCurrentPage'; -import { deleteBookFile, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions'; +import { deleteBookFile, deleteBookFiles, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions'; import { executeCommand } from 'Store/Actions/commandActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; @@ -28,7 +28,9 @@ function createMapStateToProps() { items, ...otherProps } = bookFiles; + const unmappedFiles = _.filter(items, { bookId: 0 }); + return { items: unmappedFiles, ...otherProps, @@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) { dispatch(deleteBookFile({ id })); }, + deleteUnmappedFiles(bookFileIds) { + dispatch(deleteBookFiles({ bookFileIds })); + }, + onAddMissingAuthorsPress() { dispatch(executeCommand({ name: commandNames.RESCAN_FOLDERS, @@ -106,7 +112,8 @@ UnmappedFilesTableConnector.propTypes = { onSortPress: PropTypes.func.isRequired, onTableOptionChange: PropTypes.func.isRequired, fetchUnmappedFiles: PropTypes.func.isRequired, - deleteUnmappedFile: PropTypes.func.isRequired + deleteUnmappedFile: PropTypes.func.isRequired, + deleteUnmappedFiles: PropTypes.func.isRequired }; export default withCurrentPage( diff --git a/frontend/src/UnmappedFiles/UnmappedFilesTableHeader.js b/frontend/src/UnmappedFiles/UnmappedFilesTableHeader.js index 7ac0a4e44..5b4b22933 100644 --- a/frontend/src/UnmappedFiles/UnmappedFilesTableHeader.js +++ b/frontend/src/UnmappedFiles/UnmappedFilesTableHeader.js @@ -4,6 +4,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 hasGrowableColumns from './hasGrowableColumns'; import styles from './UnmappedFilesTableHeader.css'; @@ -12,6 +13,9 @@ function UnmappedFilesTableHeader(props) { const { columns, onTableOptionChange, + allSelected, + allUnselected, + onSelectAllChange, ...otherProps } = props; @@ -30,6 +34,17 @@ function UnmappedFilesTableHeader(props) { return null; } + if (name === 'select') { + return ( + + ); + } + if (name === 'actions') { return ( + ); + } + if (name === 'path') { return ( 0 && bookFile.Author != null && bookFile.Author.Value != null) + { + _mediaFileDeletionService.DeleteTrackFile(bookFile.Author.Value, bookFile); + } + else + { + _mediaFileDeletionService.DeleteTrackFile(bookFile, "Unmapped_Files"); + } } - return Ok(); + return new { }; } [NonAction]