diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx
index 37de7940a..155d53a78 100644
--- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx
+++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx
@@ -1,21 +1,28 @@
import React, { useCallback } from 'react';
+import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
+import { deleteImportListExclusion } from 'Store/Actions/Settings/importListExclusions';
import ImportListExclusion from 'typings/ImportListExclusion';
+import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import EditImportListExclusionModal from './EditImportListExclusionModal';
import styles from './ImportListExclusionRow.css';
interface ImportListExclusionRowProps extends ImportListExclusion {
- onConfirmDeleteImportListExclusion: (id: number) => void;
+ isSelected: boolean;
+ onSelectedChange: (options: SelectStateInputProps) => void;
}
function ImportListExclusionRow(props: ImportListExclusionRowProps) {
- const { id, title, tvdbId, onConfirmDeleteImportListExclusion } = props;
+ const { id, tvdbId, title, isSelected, onSelectedChange } = props;
+
+ const dispatch = useDispatch();
const [
isEditImportListExclusionModalOpen,
@@ -29,12 +36,18 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) {
setDeleteImportListExclusionModalClosed,
] = useModalOpenState(false);
- const onConfirmDeleteImportListExclusionPress = useCallback(() => {
- onConfirmDeleteImportListExclusion(id);
- }, [id, onConfirmDeleteImportListExclusion]);
+ const handleDeletePress = useCallback(() => {
+ dispatch(deleteImportListExclusion({ id }));
+ }, [id, dispatch]);
return (
+
+
{title}
{tvdbId}
@@ -58,7 +71,7 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) {
title={translate('DeleteImportListExclusion')}
message={translate('DeleteImportListExclusionMessageText')}
confirmLabel={translate('Delete')}
- onConfirm={onConfirmDeleteImportListExclusionPress}
+ onConfirm={handleDeletePress}
onCancel={setDeleteImportListExclusionModalClosed}
/>
diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx
index 8c7033686..628a73b07 100644
--- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx
+++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx
@@ -1,24 +1,40 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { useHistory } from 'react-router';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FieldSet from 'Components/FieldSet';
import IconButton from 'Components/Link/IconButton';
+import SpinnerButton from 'Components/Link/SpinnerButton';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageSectionContent from 'Components/Page/PageSectionContent';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import TableRow from 'Components/Table/TableRow';
+import usePaging from 'Components/Table/usePaging';
+import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
-import { icons } from 'Helpers/Props';
-import * as importListExclusionActions from 'Store/Actions/Settings/importListExclusions';
+import usePrevious from 'Helpers/Hooks/usePrevious';
+import useSelectState from 'Helpers/Hooks/useSelectState';
+import { icons, kinds } from 'Helpers/Props';
+import {
+ bulkDeleteImportListExclusions,
+ clearImportListExclusions,
+ fetchImportListExclusions,
+ gotoImportListExclusionPage,
+ setImportListExclusionSort,
+ setImportListExclusionTableOption,
+} from 'Store/Actions/Settings/importListExclusions';
+import { CheckInputChanged } from 'typings/inputs';
+import { SelectStateInputProps } from 'typings/props';
+import { TableOptionsChangePayload } from 'typings/Table';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
+import getSelectedIds from 'Utilities/Table/getSelectedIds';
import EditImportListExclusionModal from './EditImportListExclusionModal';
import ImportListExclusionRow from './ImportListExclusionRow';
@@ -54,95 +70,7 @@ function createImportListExlucionsSelector() {
}
function ImportListExclusions() {
- const history = useHistory();
- const useCurrentPage = history.action === 'POP';
-
- const dispatch = useDispatch();
-
- const fetchImportListExclusions = useCallback(() => {
- dispatch(importListExclusionActions.fetchImportListExclusions());
- }, [dispatch]);
-
- const deleteImportListExclusion = useCallback(
- (payload: { id: number }) => {
- dispatch(importListExclusionActions.deleteImportListExclusion(payload));
- },
- [dispatch]
- );
-
- const gotoImportListExclusionFirstPage = useCallback(() => {
- dispatch(importListExclusionActions.gotoImportListExclusionFirstPage());
- }, [dispatch]);
-
- const gotoImportListExclusionPreviousPage = useCallback(() => {
- dispatch(importListExclusionActions.gotoImportListExclusionPreviousPage());
- }, [dispatch]);
-
- const gotoImportListExclusionNextPage = useCallback(() => {
- dispatch(importListExclusionActions.gotoImportListExclusionNextPage());
- }, [dispatch]);
-
- const gotoImportListExclusionLastPage = useCallback(() => {
- dispatch(importListExclusionActions.gotoImportListExclusionLastPage());
- }, [dispatch]);
-
- const gotoImportListExclusionPage = useCallback(
- (page: number) => {
- dispatch(
- importListExclusionActions.gotoImportListExclusionPage({ page })
- );
- },
- [dispatch]
- );
-
- const setImportListExclusionSort = useCallback(
- (sortKey: { sortKey: string }) => {
- dispatch(
- importListExclusionActions.setImportListExclusionSort({ sortKey })
- );
- },
- [dispatch]
- );
-
- const setImportListTableOption = useCallback(
- (payload: { pageSize: number }) => {
- dispatch(
- importListExclusionActions.setImportListExclusionTableOption(payload)
- );
-
- if (payload.pageSize) {
- dispatch(importListExclusionActions.gotoImportListExclusionFirstPage());
- }
- },
- [dispatch]
- );
-
- const repopulate = useCallback(() => {
- gotoImportListExclusionFirstPage();
- }, [gotoImportListExclusionFirstPage]);
-
- useEffect(() => {
- registerPagePopulator(repopulate);
-
- if (useCurrentPage) {
- fetchImportListExclusions();
- } else {
- gotoImportListExclusionFirstPage();
- }
-
- return () => unregisterPagePopulator(repopulate);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const onConfirmDeleteImportListExclusion = useCallback(
- (id: number) => {
- deleteImportListExclusion({ id });
- repopulate();
- },
- [deleteImportListExclusion, repopulate]
- );
-
- const selected = useSelector(createImportListExlucionsSelector());
+ const requestCurrentPage = useCurrentPage();
const {
isFetching,
@@ -152,9 +80,127 @@ function ImportListExclusions() {
sortKey,
error,
sortDirection,
+ page,
+ totalPages,
totalRecords,
- ...otherProps
- } = selected;
+ isDeleting,
+ deleteError,
+ } = useSelector(createImportListExlucionsSelector());
+
+ const dispatch = useDispatch();
+
+ const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
+ useState(false);
+ const previousIsDeleting = usePrevious(isDeleting);
+
+ const [selectState, setSelectState] = useSelectState();
+ const { allSelected, allUnselected, selectedState } = selectState;
+
+ const selectedIds = useMemo(() => {
+ return getSelectedIds(selectedState);
+ }, [selectedState]);
+
+ const handleSelectAllChange = useCallback(
+ ({ value }: CheckInputChanged) => {
+ setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
+ },
+ [items, setSelectState]
+ );
+
+ const handleSelectedChange = useCallback(
+ ({ id, value, shiftKey = false }: SelectStateInputProps) => {
+ setSelectState({
+ type: 'toggleSelected',
+ items,
+ id,
+ isSelected: value,
+ shiftKey,
+ });
+ },
+ [items, setSelectState]
+ );
+
+ const handleDeleteSelectedPress = useCallback(() => {
+ setIsConfirmDeleteModalOpen(true);
+ }, [setIsConfirmDeleteModalOpen]);
+
+ const handleDeleteSelectedConfirmed = useCallback(() => {
+ dispatch(bulkDeleteImportListExclusions({ ids: selectedIds }));
+ setIsConfirmDeleteModalOpen(false);
+ }, [selectedIds, setIsConfirmDeleteModalOpen, dispatch]);
+
+ const handleConfirmDeleteModalClose = useCallback(() => {
+ setIsConfirmDeleteModalOpen(false);
+ }, [setIsConfirmDeleteModalOpen]);
+
+ const {
+ handleFirstPagePress,
+ handlePreviousPagePress,
+ handleNextPagePress,
+ handleLastPagePress,
+ handlePageSelect,
+ } = usePaging({
+ page,
+ totalPages,
+ gotoPage: gotoImportListExclusionPage,
+ });
+
+ const handleSortPress = useCallback(
+ (sortKey: { sortKey: string }) => {
+ dispatch(setImportListExclusionSort({ sortKey }));
+ },
+ [dispatch]
+ );
+
+ const handleTableOptionChange = useCallback(
+ (payload: TableOptionsChangePayload) => {
+ dispatch(setImportListExclusionTableOption(payload));
+
+ if (payload.pageSize) {
+ dispatch(gotoImportListExclusionPage({ page: 1 }));
+ }
+ },
+ [dispatch]
+ );
+
+ useEffect(() => {
+ if (requestCurrentPage) {
+ dispatch(fetchImportListExclusions());
+ } else {
+ dispatch(gotoImportListExclusionPage({ page: 1 }));
+ }
+
+ return () => {
+ dispatch(clearImportListExclusions());
+ };
+ }, [requestCurrentPage, dispatch]);
+
+ useEffect(() => {
+ const repopulate = () => {
+ dispatch(fetchImportListExclusions());
+ };
+
+ registerPagePopulator(repopulate);
+
+ return () => {
+ unregisterPagePopulator(repopulate);
+ };
+ }, [dispatch]);
+
+ useEffect(() => {
+ if (previousIsDeleting && !isDeleting && !deleteError) {
+ setSelectState({ type: 'unselectAll', items });
+
+ dispatch(fetchImportListExclusions());
+ }
+ }, [
+ previousIsDeleting,
+ isDeleting,
+ deleteError,
+ items,
+ dispatch,
+ setSelectState,
+ ]);
const [
isAddImportListExclusionModalOpen,
@@ -173,13 +219,17 @@ function ImportListExclusions() {
error={error}
>
{items.map((item) => {
@@ -187,16 +237,23 @@ function ImportListExclusions() {
);
})}
-
-
+
+
+ {translate('Delete')}
+
+
+
+
);
diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js
index 3af8bf9ec..a89d65208 100644
--- a/frontend/src/Store/Actions/Settings/importListExclusions.js
+++ b/frontend/src/Store/Actions/Settings/importListExclusions.js
@@ -1,7 +1,9 @@
import { createAction } from 'redux-actions';
+import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers';
+import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
import { createThunk, handleThunks } from 'Store/thunks';
@@ -16,29 +18,26 @@ const section = 'settings.importListExclusions';
// Actions Types
export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions';
-export const GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionFirstPage';
-export const GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPreviousPage';
-export const GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionNextPage';
-export const GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionLastPage';
export const GOTO_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPage';
export const SET_IMPORT_LIST_EXCLUSION_SORT = 'settings/importListExclusions/setImportListExclusionSort';
-export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption';
export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion';
export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion';
+export const BULK_DELETE_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/bulkDeleteImportListExclusions';
+export const CLEAR_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/clearImportListExclusions';
+
+export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption';
export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue';
//
// Action Creators
export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS);
-export const gotoImportListExclusionFirstPage = createThunk(GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE);
-export const gotoImportListExclusionPreviousPage = createThunk(GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE);
-export const gotoImportListExclusionNextPage = createThunk(GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE);
-export const gotoImportListExclusionLastPage = createThunk(GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE);
export const gotoImportListExclusionPage = createThunk(GOTO_IMPORT_LIST_EXCLUSION_PAGE);
export const setImportListExclusionSort = createThunk(SET_IMPORT_LIST_EXCLUSION_SORT);
export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION);
export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION);
+export const bulkDeleteImportListExclusions = createThunk(BULK_DELETE_IMPORT_LIST_EXCLUSIONS);
+export const clearImportListExclusions = createAction(CLEAR_IMPORT_LIST_EXCLUSIONS);
export const setImportListExclusionTableOption = createAction(SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION);
export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => {
@@ -64,6 +63,8 @@ export default {
items: [],
isSaving: false,
saveError: null,
+ isDeleting: false,
+ deleteError: null,
pendingChanges: {}
},
@@ -77,16 +78,13 @@ export default {
fetchImportListExclusions,
{
[serverSideCollectionHandlers.FETCH]: FETCH_IMPORT_LIST_EXCLUSIONS,
- [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE,
- [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE,
- [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE,
- [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE,
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_IMPORT_LIST_EXCLUSION_PAGE,
[serverSideCollectionHandlers.SORT]: SET_IMPORT_LIST_EXCLUSION_SORT
}
),
[SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'),
- [DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion')
+ [DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion'),
+ [BULK_DELETE_IMPORT_LIST_EXCLUSIONS]: createBulkRemoveItemHandler(section, '/importlistexclusion/bulk')
}),
//
@@ -94,7 +92,19 @@ export default {
reducers: {
[SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section),
- [SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section)
+ [SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section),
+
+ [CLEAR_IMPORT_LIST_EXCLUSIONS]: createClearReducer(section, {
+ isFetching: false,
+ isPopulated: false,
+ error: null,
+ items: [],
+ isDeleting: false,
+ deleteError: null,
+ pendingChanges: {},
+ totalPages: 0,
+ totalRecords: 0
+ })
}
};
diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs
index 2a9f0a9ec..704472125 100644
--- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs
+++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs
@@ -12,6 +12,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions
List All();
PagingSpec Paged(PagingSpec pagingSpec);
void Delete(int id);
+ void Delete(List ids);
ImportListExclusion Get(int id);
ImportListExclusion FindByTvdbId(int tvdbId);
ImportListExclusion Update(ImportListExclusion importListExclusion);
@@ -41,6 +42,11 @@ namespace NzbDrone.Core.ImportLists.Exclusions
_repo.Delete(id);
}
+ public void Delete(List ids)
+ {
+ _repo.DeleteMany(ids);
+ }
+
public ImportListExclusion Get(int id)
{
return _repo.Get(id);
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 8d7d90087..3ffc97af5 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -363,10 +363,12 @@
"DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?",
"DeleteRootFolder": "Delete Root Folder",
"DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{path}'?",
+ "DeleteSelected": "Delete Selected",
"DeleteSelectedDownloadClients": "Delete Download Client(s)",
"DeleteSelectedDownloadClientsMessageText": "Are you sure you want to delete {count} selected download client(s)?",
"DeleteSelectedEpisodeFiles": "Delete Selected Episode Files",
"DeleteSelectedEpisodeFilesHelpText": "Are you sure you want to delete the selected episode files?",
+ "DeleteSelectedImportListExclusionsMessageText": "Are you sure you want to delete the selected import list exclusions?",
"DeleteSelectedImportLists": "Delete Import List(s)",
"DeleteSelectedImportListsMessageText": "Are you sure you want to delete {count} selected import list(s)?",
"DeleteSelectedIndexers": "Delete Indexer(s)",
@@ -1787,8 +1789,8 @@
"SeasonPremieresOnly": "Season Premieres Only",
"Seasons": "Seasons",
"SeasonsMonitoredAll": "All",
- "SeasonsMonitoredPartial": "Partial",
"SeasonsMonitoredNone": "None",
+ "SeasonsMonitoredPartial": "Partial",
"SeasonsMonitoredStatus": "Seasons Monitored",
"SecretToken": "Secret Token",
"Security": "Security",
diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionBulkResource.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionBulkResource.cs
new file mode 100644
index 000000000..c257d35ad
--- /dev/null
+++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionBulkResource.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Sonarr.Api.V3.ImportLists
+{
+ public class ImportListExclusionBulkResource
+ {
+ public HashSet Ids { get; set; }
+ }
+}
diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs
index 58742ad79..8efb44e18 100644
--- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs
+++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.ImportLists.Exclusions;
@@ -65,9 +66,18 @@ namespace Sonarr.Api.V3.ImportLists
}
[RestDeleteById]
- public void DeleteImportListExclusionResource(int id)
+ public void DeleteImportListExclusion(int id)
{
_importListExclusionService.Delete(id);
}
+
+ [HttpDelete("bulk")]
+ [Produces("application/json")]
+ public object DeleteImportListExclusions([FromBody] ImportListExclusionBulkResource resource)
+ {
+ _importListExclusionService.Delete(resource.Ids.ToList());
+
+ return new { };
+ }
}
}