diff --git a/frontend/src/Store/Actions/trackFileActions.js b/frontend/src/Store/Actions/trackFileActions.js index 0e4f91d54..2530c5879 100644 --- a/frontend/src/Store/Actions/trackFileActions.js +++ b/frontend/src/Store/Actions/trackFileActions.js @@ -42,6 +42,14 @@ export const defaultState = { }, columns: [ + { + name: 'select', + columnLabel: 'Select', + isSortable: false, + isVisible: true, + isModifiable: false, + isHidden: true + }, { name: 'path', label: translate('Path'), diff --git a/frontend/src/UnmappedFiles/UnmappedFilesTable.js b/frontend/src/UnmappedFiles/UnmappedFilesTable.js index 21b687dc5..f9213a4d3 100644 --- a/frontend/src/UnmappedFiles/UnmappedFilesTable.js +++ b/frontend/src/UnmappedFiles/UnmappedFilesTable.js @@ -10,7 +10,11 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions import VirtualTable from 'Components/Table/VirtualTable'; import VirtualTableRow from 'Components/Table/VirtualTableRow'; import { align, icons, 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 +27,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 +71,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 +140,10 @@ class UnmappedFilesTable extends Component { deleteUnmappedFile } = this.props; + const { + selectedState + } = this.state; + const item = items[rowIndex]; return ( @@ -51,6 +154,8 @@ class UnmappedFilesTable extends Component { @@ -63,6 +168,7 @@ class UnmappedFilesTable extends Component { const { isFetching, isPopulated, + isDeleting, error, items, columns, @@ -72,13 +178,19 @@ class UnmappedFilesTable extends Component { onSortPress, isScanningFolders, onAddMissingArtistsPress, + deleteUnmappedFiles, ...otherProps } = this.props; const { - scroller + scroller, + allSelected, + allUnselected, + selectedState } = this.state; + const selectedTrackFileIds = this.getSelectedIds(); + return ( @@ -90,6 +202,13 @@ class UnmappedFilesTable extends Component { isSpinning={isScanningFolders} onPress={onAddMissingArtistsPress} /> + @@ -138,8 +257,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 +276,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 +286,7 @@ UnmappedFilesTable.propTypes = { onTableOptionChange: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired, deleteUnmappedFile: PropTypes.func.isRequired, + deleteUnmappedFiles: PropTypes.func.isRequired, isScanningFolders: PropTypes.bool.isRequired, onAddMissingArtistsPress: PropTypes.func.isRequired }; diff --git a/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js b/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js index c74b7be34..63484b210 100644 --- a/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js +++ b/frontend/src/UnmappedFiles/UnmappedFilesTableConnector.js @@ -6,7 +6,7 @@ import { createSelector } from 'reselect'; import * as commandNames from 'Commands/commandNames'; import withCurrentPage from 'Components/withCurrentPage'; import { executeCommand } from 'Store/Actions/commandActions'; -import { deleteTrackFile, fetchTrackFiles, setTrackFilesSort, setTrackFilesTableOption } from 'Store/Actions/trackFileActions'; +import { deleteTrackFile, deleteTrackFiles, fetchTrackFiles, setTrackFilesSort, setTrackFilesTableOption } from 'Store/Actions/trackFileActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -28,7 +28,9 @@ function createMapStateToProps() { items, ...otherProps } = trackFiles; + const unmappedFiles = _.filter(items, { albumId: 0 }); + return { items: unmappedFiles, ...otherProps, @@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) { dispatch(deleteTrackFile({ id })); }, + deleteUnmappedFiles(trackFileIds) { + dispatch(deleteTrackFiles({ trackFileIds })); + }, + onAddMissingArtistsPress() { dispatch(executeCommand({ name: commandNames.RESCAN_FOLDERS, @@ -105,7 +111,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 && trackFile.Artist != null && trackFile.Artist.Value != null) + { + _mediaFileDeletionService.DeleteTrackFile(trackFile.Artist.Value, trackFile); + } + else + { + _mediaFileDeletionService.DeleteTrackFile(trackFile, "Unmapped_Files"); + } } - return Ok(); + return new { }; } [NonAction] diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 6685bfc36..4d882f484 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -196,6 +196,7 @@ "DeleteReleaseProfileMessageText": "Are you sure you want to delete this releaseProfile?", "DeleteRootFolder": "Delete Root Folder", "DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{0}'?", + "DeleteSelected": "Delete Selected", "DeleteSelectedTrackFiles": "Delete Selected Track Files", "DeleteSelectedTrackFilesMessageText": "Are you sure you want to delete the selected track files?", "DeleteTag": "Delete Tag",