New: Bulk import list exclusions removal

This commit is contained in:
Bogdan 2024-08-09 20:05:07 +03:00
parent 2f04b037a1
commit 9e7c74c2ac
7 changed files with 249 additions and 132 deletions

View File

@ -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 (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell>{title}</TableRowCell>
<TableRowCell>{tvdbId}</TableRowCell>
@ -58,7 +71,7 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) {
title={translate('DeleteImportListExclusion')}
message={translate('DeleteImportListExclusionMessageText')}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDeleteImportListExclusionPress}
onConfirm={handleDeletePress}
onCancel={setDeleteImportListExclusionModalClosed}
/>
</TableRow>

View File

@ -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}
>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={COLUMNS}
canModifyColumns={false}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={setImportListExclusionSort}
onTableOptionChange={setImportListTableOption}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
@ -187,16 +237,23 @@ function ImportListExclusions() {
<ImportListExclusionRow
key={item.id}
{...item}
onConfirmDeleteImportListExclusion={
onConfirmDeleteImportListExclusion
}
isSelected={selectedState[item.id] || false}
onSelectedChange={handleSelectedChange}
/>
);
})}
<TableRow>
<TableRowCell />
<TableRowCell />
<TableRowCell colSpan={3}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedIds.length}
onPress={handleDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
</TableRowCell>
<TableRowCell>
<IconButton
@ -209,21 +266,31 @@ function ImportListExclusions() {
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
pageSize={pageSize}
isFetching={isFetching}
onFirstPagePress={gotoImportListExclusionFirstPage}
onPreviousPagePress={gotoImportListExclusionPreviousPage}
onNextPagePress={gotoImportListExclusionNextPage}
onLastPagePress={gotoImportListExclusionLastPage}
onPageSelect={gotoImportListExclusionPage}
{...otherProps}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
<EditImportListExclusionModal
isOpen={isAddImportListExclusionModalOpen}
onModalClose={setAddImportListExclusionModalClosed}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelected')}
message={translate('DeleteSelectedImportListExclusionsMessageText')}
confirmLabel={translate('DeleteSelected')}
onConfirm={handleDeleteSelectedConfirmed}
onCancel={handleConfirmDeleteModalClose}
/>
</PageSectionContent>
</FieldSet>
);

View File

@ -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
})
}
};

View File

@ -12,6 +12,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions
List<ImportListExclusion> All();
PagingSpec<ImportListExclusion> Paged(PagingSpec<ImportListExclusion> pagingSpec);
void Delete(int id);
void Delete(List<int> 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<int> ids)
{
_repo.DeleteMany(ids);
}
public ImportListExclusion Get(int id)
{
return _repo.Get(id);

View File

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

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Sonarr.Api.V3.ImportLists
{
public class ImportListExclusionBulkResource
{
public HashSet<int> Ids { get; set; }
}
}

View File

@ -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 { };
}
}
}