diff --git a/frontend/src/Activity/Blacklist/Blacklist.js b/frontend/src/Activity/Blacklist/Blacklist.js
index 2d0a1e9f4..015b19759 100644
--- a/frontend/src/Activity/Blacklist/Blacklist.js
+++ b/frontend/src/Activity/Blacklist/Blacklist.js
@@ -1,7 +1,14 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { align, icons } from 'Helpers/Props';
+import getRemovedItems from 'Utilities/Object/getRemovedItems';
+import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
+import getSelectedIds from 'Utilities/Table/getSelectedIds';
+import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
+import selectAll from 'Utilities/Table/selectAll';
+import toggleSelected from 'Utilities/Table/toggleSelected';
+import { align, icons, kinds } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
@@ -15,6 +22,72 @@ import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ allSelected: false,
+ allUnselected: false,
+ lastToggled: null,
+ selectedState: {},
+ isConfirmRemoveModalOpen: false,
+ items: props.items
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ items
+ } = this.props;
+
+ if (hasDifferentItems(prevProps.items, items)) {
+ this.setState((state) => {
+ return {
+ ...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
+ items
+ };
+ });
+
+ return;
+ }
+ }
+
+ //
+ // Control
+
+ getSelectedIds = () => {
+ 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);
+ });
+ }
+
+ onRemoveSelectedPress = () => {
+ this.setState({ isConfirmRemoveModalOpen: true });
+ }
+
+ onRemoveSelectedConfirmed = () => {
+ this.props.onRemoveSelected(this.getSelectedIds());
+ this.setState({ isConfirmRemoveModalOpen: false });
+ }
+
+ onConfirmRemoveModalClose = () => {
+ this.setState({ isConfirmRemoveModalOpen: false });
+ }
+
//
// Render
@@ -26,15 +99,33 @@ class Blacklist extends Component {
items,
columns,
totalRecords,
+ isRemoving,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
+ const {
+ allSelected,
+ allUnselected,
+ selectedState,
+ isConfirmRemoveModalOpen
+ } = this.state;
+
+ const selectedIds = this.getSelectedIds();
+
return (
+
+
{
isFetching && !isPopulated &&
-
+
}
{
!isFetching && !!error &&
- Unable to load blacklist
+ Unable to load blacklist
}
{
isPopulated && !error && !items.length &&
-
- No history blacklist
-
+
+ No history blacklist
+
}
{
isPopulated && !error && !!items.length &&
-
-
-
- {
- items.map((item) => {
- return (
-
- );
- })
- }
-
-
+
+
+
+ {
+ items.map((item) => {
+ return (
+
+ );
+ })
+ }
+
+
-
-
+
+
}
+
+
);
}
@@ -116,7 +223,9 @@ Blacklist.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
+ isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
+ onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};
diff --git a/frontend/src/Activity/Blacklist/BlacklistConnector.js b/frontend/src/Activity/Blacklist/BlacklistConnector.js
index b182e7bb2..1cb8e4a43 100644
--- a/frontend/src/Activity/Blacklist/BlacklistConnector.js
+++ b/frontend/src/Activity/Blacklist/BlacklistConnector.js
@@ -89,6 +89,10 @@ class BlacklistConnector extends Component {
this.props.gotoBlacklistPage({ page });
}
+ onRemoveSelected = (ids) => {
+ this.props.removeBlacklistItems({ ids });
+ }
+
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
}
@@ -124,6 +128,7 @@ class BlacklistConnector extends Component {
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
+ onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
@@ -143,6 +148,7 @@ BlacklistConnector.propTypes = {
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
+ removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,
diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.js b/frontend/src/Activity/Blacklist/BlacklistRow.js
index 47859a80f..cb07ce803 100644
--- a/frontend/src/Activity/Blacklist/BlacklistRow.js
+++ b/frontend/src/Activity/Blacklist/BlacklistRow.js
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
+import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
@@ -40,6 +41,7 @@ class BlacklistRow extends Component {
render() {
const {
+ id,
series,
sourceTitle,
language,
@@ -48,12 +50,20 @@ class BlacklistRow extends Component {
protocol,
indexer,
message,
+ isSelected,
columns,
+ onSelectedChange,
onRemovePress
} = this.props;
return (
+
+
{
columns.map((column) => {
const {
@@ -179,7 +189,9 @@ BlacklistRow.propTypes = {
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
+ isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
+ onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};
diff --git a/frontend/src/Activity/Blacklist/BlacklistRowConnector.js b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js
index efba18fab..29b9e9e78 100644
--- a/frontend/src/Activity/Blacklist/BlacklistRowConnector.js
+++ b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
+import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import BlacklistRow from './BlacklistRow';
@@ -18,7 +18,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
- dispatch(removeFromBlacklist({ id: props.id }));
+ dispatch(removeBlacklistItem({ id: props.id }));
}
};
}
diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js
index ee7f44f84..c079bc990 100644
--- a/frontend/src/Store/Actions/blacklistActions.js
+++ b/frontend/src/Store/Actions/blacklistActions.js
@@ -1,4 +1,6 @@
import { createAction } from 'redux-actions';
+import { batchActions } from 'redux-batched-actions';
+import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
import { createThunk, handleThunks } from 'Store/thunks';
import { sortDirections } from 'Helpers/Props';
@@ -7,6 +9,7 @@ import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptio
import createHandleActions from './Creators/createHandleActions';
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
+import { set, updateItem } from './baseActions';
//
// Variables
@@ -24,6 +27,7 @@ export const defaultState = {
sortDirection: sortDirections.DESCENDING,
error: null,
items: [],
+ isRemoving: false,
columns: [
{
@@ -87,7 +91,8 @@ export const GOTO_LAST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistLastPage';
export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage';
export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort';
export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption';
-export const REMOVE_FROM_BLACKLIST = 'blacklist/removeFromBlacklist';
+export const REMOVE_BLACKLIST_ITEM = 'blacklist/removeBlacklistItem';
+export const REMOVE_BLACKLIST_ITEMS = 'blacklist/removeBlacklistItems';
export const CLEAR_BLACKLIST = 'blacklist/clearBlacklist';
//
@@ -101,7 +106,8 @@ export const gotoBlacklistLastPage = createThunk(GOTO_LAST_BLACKLIST_PAGE);
export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE);
export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT);
export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION);
-export const removeFromBlacklist = createThunk(REMOVE_FROM_BLACKLIST);
+export const removeBlacklistItem = createThunk(REMOVE_BLACKLIST_ITEM);
+export const removeBlacklistItems = createThunk(REMOVE_BLACKLIST_ITEMS);
export const clearBlacklist = createAction(CLEAR_BLACKLIST);
//
@@ -122,7 +128,53 @@ export const actionHandlers = handleThunks({
[serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT
}),
- [REMOVE_FROM_BLACKLIST]: createRemoveItemHandler(section, '/blacklist')
+ [REMOVE_BLACKLIST_ITEM]: createRemoveItemHandler(section, '/blacklist'),
+
+ [REMOVE_BLACKLIST_ITEMS]: function(getState, payload, dispatch) {
+ const {
+ ids
+ } = payload;
+
+ dispatch(batchActions([
+ ...ids.map((id) => {
+ return updateItem({
+ section,
+ id,
+ isRemoving: true
+ });
+ }),
+
+ set({ section, isRemoving: true })
+ ]));
+
+ const promise = createAjaxRequest({
+ url: '/blacklist/bulk',
+ method: 'DELETE',
+ dataType: 'json',
+ data: JSON.stringify({ ids })
+ }).request;
+
+ promise.done((data) => {
+ // Don't use batchActions with thunks
+ dispatch(fetchBlacklist());
+
+ dispatch(set({ section, isRemoving: false }));
+ });
+
+ promise.fail((xhr) => {
+ dispatch(batchActions([
+ ...ids.map((id) => {
+ return updateItem({
+ section,
+ id,
+ isRemoving: false
+ });
+ }),
+
+ set({ section, isRemoving: false })
+ ]));
+ });
+ }
});
//
diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js
index a73009deb..97d37a599 100644
--- a/frontend/src/Store/Actions/queueActions.js
+++ b/frontend/src/Store/Actions/queueActions.js
@@ -318,9 +318,9 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
- dispatch(batchActions([
- fetchQueue(),
+ dispatch(fetchQueue());
+ dispatch(batchActions([
...ids.map((id) => {
return updateItem({
section: paged,
@@ -404,10 +404,10 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
- dispatch(batchActions([
- set({ section: paged, isRemoving: false }),
- fetchQueue()
- ]));
+ // Don't use batchActions with thunks
+ dispatch(fetchQueue());
+
+ dispatch(set({ section: paged, isRemoving: false }));
});
promise.fail((xhr) => {
diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs
index c08020b22..2ea325b39 100644
--- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs
+++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
@@ -16,6 +17,7 @@ namespace NzbDrone.Core.Blacklisting
bool Blacklisted(int seriesId, ReleaseInfo release);
PagingSpec Paged(PagingSpec pagingSpec);
void Delete(int id);
+ void Delete(List ids);
}
public class BlacklistService : IBlacklistService,
@@ -65,6 +67,11 @@ namespace NzbDrone.Core.Blacklisting
_blacklistRepository.Delete(id);
}
+ public void Delete(List ids)
+ {
+ _blacklistRepository.DeleteMany(ids);
+ }
+
private bool SameNzb(Blacklist item, ReleaseInfo release)
{
if (item.PublishedDate == release.PublishDate)
diff --git a/src/Sonarr.Api.V3/Blacklist/BlacklistBulkResource.cs b/src/Sonarr.Api.V3/Blacklist/BlacklistBulkResource.cs
new file mode 100644
index 000000000..7831a71d9
--- /dev/null
+++ b/src/Sonarr.Api.V3/Blacklist/BlacklistBulkResource.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace NzbDrone.Api.V3.Blacklist
+{
+ public class BlacklistBulkResource
+ {
+ public List Ids { get; set; }
+ }
+}
diff --git a/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs b/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs
index 76c5144d0..e2172b473 100644
--- a/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs
+++ b/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs
@@ -1,6 +1,8 @@
-using NzbDrone.Core.Blacklisting;
+using NzbDrone.Api.V3.Blacklist;
+using NzbDrone.Core.Blacklisting;
using NzbDrone.Core.Datastore;
using Sonarr.Http;
+using Sonarr.Http.Extensions;
namespace Sonarr.Api.V3.Blacklist
{
@@ -13,6 +15,8 @@ namespace Sonarr.Api.V3.Blacklist
_blacklistService = blacklistService;
GetResourcePaged = GetBlacklist;
DeleteResource = DeleteBlacklist;
+
+ Delete("/bulk", x => Remove());
}
private PagingResource GetBlacklist(PagingResource pagingResource)
@@ -26,5 +30,14 @@ namespace Sonarr.Api.V3.Blacklist
{
_blacklistService.Delete(id);
}
+
+ private object Remove()
+ {
+ var resource = Request.Body.FromJson();
+
+ _blacklistService.Delete(resource.Ids);
+
+ return new object();
+ }
}
}
\ No newline at end of file