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",