diff --git a/frontend/src/Activity/Blocklist/Blocklist.js b/frontend/src/Activity/Blocklist/Blocklist.js
deleted file mode 100644
index 19026beb5..000000000
--- a/frontend/src/Activity/Blocklist/Blocklist.js
+++ /dev/null
@@ -1,284 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Alert from 'Components/Alert';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import FilterMenu from 'Components/Menu/FilterMenu';
-import ConfirmModal from 'Components/Modal/ConfirmModal';
-import PageContent from 'Components/Page/PageContent';
-import PageContentBody from 'Components/Page/PageContentBody';
-import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
-import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
-import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
-import Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
-import TablePager from 'Components/Table/TablePager';
-import { align, icons, kinds } from 'Helpers/Props';
-import getRemovedItems from 'Utilities/Object/getRemovedItems';
-import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
-import translate from 'Utilities/String/translate';
-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 BlocklistFilterModal from './BlocklistFilterModal';
-import BlocklistRowConnector from './BlocklistRowConnector';
-
-class Blocklist extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- allSelected: false,
- allUnselected: false,
- lastToggled: null,
- selectedState: {},
- isConfirmRemoveModalOpen: false,
- isConfirmClearModalOpen: 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 });
- };
-
- onClearBlocklistPress = () => {
- this.setState({ isConfirmClearModalOpen: true });
- };
-
- onClearBlocklistConfirmed = () => {
- this.props.onClearBlocklistPress();
- this.setState({ isConfirmClearModalOpen: false });
- };
-
- onConfirmClearModalClose = () => {
- this.setState({ isConfirmClearModalOpen: false });
- };
-
- //
- // Render
-
- render() {
- const {
- isFetching,
- isPopulated,
- error,
- items,
- columns,
- selectedFilterKey,
- filters,
- customFilters,
- totalRecords,
- isRemoving,
- isClearingBlocklistExecuting,
- onFilterSelect,
- ...otherProps
- } = this.props;
-
- const {
- allSelected,
- allUnselected,
- selectedState,
- isConfirmRemoveModalOpen,
- isConfirmClearModalOpen
- } = this.state;
-
- const selectedIds = this.getSelectedIds();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- isFetching && !isPopulated &&
-
- }
-
- {
- !isFetching && !!error &&
-
- {translate('BlocklistLoadError')}
-
- }
-
- {
- isPopulated && !error && !items.length &&
-
- {
- selectedFilterKey === 'all' ?
- translate('NoHistoryBlocklist') :
- translate('BlocklistFilterHasNoItems')
- }
-
- }
-
- {
- isPopulated && !error && !!items.length &&
-
-
-
- {
- items.map((item) => {
- return (
-
- );
- })
- }
-
-
-
-
-
- }
-
-
-
-
-
-
- );
- }
-}
-
-Blocklist.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- error: PropTypes.object,
- items: PropTypes.arrayOf(PropTypes.object).isRequired,
- columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
- filters: PropTypes.arrayOf(PropTypes.object).isRequired,
- customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
- totalRecords: PropTypes.number,
- isRemoving: PropTypes.bool.isRequired,
- isClearingBlocklistExecuting: PropTypes.bool.isRequired,
- onRemoveSelected: PropTypes.func.isRequired,
- onClearBlocklistPress: PropTypes.func.isRequired,
- onFilterSelect: PropTypes.func.isRequired
-};
-
-export default Blocklist;
diff --git a/frontend/src/Activity/Blocklist/Blocklist.tsx b/frontend/src/Activity/Blocklist/Blocklist.tsx
new file mode 100644
index 000000000..4205ae12e
--- /dev/null
+++ b/frontend/src/Activity/Blocklist/Blocklist.tsx
@@ -0,0 +1,326 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { SelectProvider } from 'App/SelectContext';
+import AppState from 'App/State/AppState';
+import * as commandNames from 'Commands/commandNames';
+import Alert from 'Components/Alert';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import FilterMenu from 'Components/Menu/FilterMenu';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
+import PageContent from 'Components/Page/PageContent';
+import PageContentBody from 'Components/Page/PageContentBody';
+import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
+import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
+import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
+import TablePager from 'Components/Table/TablePager';
+import usePaging from 'Components/Table/usePaging';
+import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
+import usePrevious from 'Helpers/Hooks/usePrevious';
+import useSelectState from 'Helpers/Hooks/useSelectState';
+import { align, icons, kinds } from 'Helpers/Props';
+import {
+ clearBlocklist,
+ fetchBlocklist,
+ gotoBlocklistPage,
+ removeBlocklistItems,
+ setBlocklistFilter,
+ setBlocklistSort,
+ setBlocklistTableOption,
+} from 'Store/Actions/blocklistActions';
+import { executeCommand } from 'Store/Actions/commandActions';
+import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
+import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
+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 BlocklistFilterModal from './BlocklistFilterModal';
+import BlocklistRow from './BlocklistRow';
+
+function Blocklist() {
+ const requestCurrentPage = useCurrentPage();
+
+ const {
+ isFetching,
+ isPopulated,
+ error,
+ items,
+ columns,
+ selectedFilterKey,
+ filters,
+ sortKey,
+ sortDirection,
+ page,
+ totalPages,
+ totalRecords,
+ isRemoving,
+ } = useSelector((state: AppState) => state.blocklist);
+
+ const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
+ const isClearingBlocklistExecuting = useSelector(
+ createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
+ );
+ const dispatch = useDispatch();
+
+ const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
+ useState(false);
+ const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
+
+ const [selectState, setSelectState] = useSelectState();
+ const { allSelected, allUnselected, selectedState } = selectState;
+
+ const selectedIds = useMemo(() => {
+ return getSelectedIds(selectedState);
+ }, [selectedState]);
+
+ const wasClearingBlocklistExecuting = usePrevious(
+ isClearingBlocklistExecuting
+ );
+
+ 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 handleRemoveSelectedPress = useCallback(() => {
+ setIsConfirmRemoveModalOpen(true);
+ }, [setIsConfirmRemoveModalOpen]);
+
+ const handleRemoveSelectedConfirmed = useCallback(() => {
+ dispatch(removeBlocklistItems({ ids: selectedIds }));
+ setIsConfirmRemoveModalOpen(false);
+ }, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
+
+ const handleConfirmRemoveModalClose = useCallback(() => {
+ setIsConfirmRemoveModalOpen(false);
+ }, [setIsConfirmRemoveModalOpen]);
+
+ const handleClearBlocklistPress = useCallback(() => {
+ setIsConfirmClearModalOpen(true);
+ }, [setIsConfirmClearModalOpen]);
+
+ const handleClearBlocklistConfirmed = useCallback(() => {
+ dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
+ setIsConfirmClearModalOpen(false);
+ }, [setIsConfirmClearModalOpen, dispatch]);
+
+ const handleConfirmClearModalClose = useCallback(() => {
+ setIsConfirmClearModalOpen(false);
+ }, [setIsConfirmClearModalOpen]);
+
+ const {
+ handleFirstPagePress,
+ handlePreviousPagePress,
+ handleNextPagePress,
+ handleLastPagePress,
+ handlePageSelect,
+ } = usePaging({
+ page,
+ totalPages,
+ gotoPage: gotoBlocklistPage,
+ });
+
+ const handleFilterSelect = useCallback(
+ (selectedFilterKey: string) => {
+ dispatch(setBlocklistFilter({ selectedFilterKey }));
+ },
+ [dispatch]
+ );
+
+ const handleSortPress = useCallback(
+ (sortKey: string) => {
+ dispatch(setBlocklistSort({ sortKey }));
+ },
+ [dispatch]
+ );
+
+ const handleTableOptionChange = useCallback(
+ (payload: TableOptionsChangePayload) => {
+ dispatch(setBlocklistTableOption(payload));
+
+ if (payload.pageSize) {
+ dispatch(gotoBlocklistPage({ page: 1 }));
+ }
+ },
+ [dispatch]
+ );
+
+ useEffect(() => {
+ if (requestCurrentPage) {
+ dispatch(fetchBlocklist());
+ } else {
+ dispatch(gotoBlocklistPage({ page: 1 }));
+ }
+
+ return () => {
+ dispatch(clearBlocklist());
+ };
+ }, [requestCurrentPage, dispatch]);
+
+ useEffect(() => {
+ const repopulate = () => {
+ dispatch(fetchBlocklist());
+ };
+
+ registerPagePopulator(repopulate);
+
+ return () => {
+ unregisterPagePopulator(repopulate);
+ };
+ }, [dispatch]);
+
+ useEffect(() => {
+ if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
+ dispatch(gotoBlocklistPage({ page: 1 }));
+ }
+ }, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isFetching && !isPopulated ? : null}
+
+ {!isFetching && !!error ? (
+ {translate('BlocklistLoadError')}
+ ) : null}
+
+ {isPopulated && !error && !items.length ? (
+
+ {selectedFilterKey === 'all'
+ ? translate('NoBlocklistItems')
+ : translate('BlocklistFilterHasNoItems')}
+
+ ) : null}
+
+ {isPopulated && !error && !!items.length ? (
+
+
+
+ {items.map((item) => {
+ return (
+
+ );
+ })}
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+ );
+}
+
+export default Blocklist;
diff --git a/frontend/src/Activity/Blocklist/BlocklistConnector.js b/frontend/src/Activity/Blocklist/BlocklistConnector.js
deleted file mode 100644
index 5eb055a06..000000000
--- a/frontend/src/Activity/Blocklist/BlocklistConnector.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import * as commandNames from 'Commands/commandNames';
-import withCurrentPage from 'Components/withCurrentPage';
-import * as blocklistActions from 'Store/Actions/blocklistActions';
-import { executeCommand } from 'Store/Actions/commandActions';
-import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
-import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
-import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
-import Blocklist from './Blocklist';
-
-function createMapStateToProps() {
- return createSelector(
- (state) => state.blocklist,
- createCustomFiltersSelector('blocklist'),
- createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
- (blocklist, customFilters, isClearingBlocklistExecuting) => {
- return {
- isClearingBlocklistExecuting,
- customFilters,
- ...blocklist
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- ...blocklistActions,
- executeCommand
-};
-
-class BlocklistConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- const {
- useCurrentPage,
- fetchBlocklist,
- gotoBlocklistFirstPage
- } = this.props;
-
- registerPagePopulator(this.repopulate);
-
- if (useCurrentPage) {
- fetchBlocklist();
- } else {
- gotoBlocklistFirstPage();
- }
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
- this.props.gotoBlocklistFirstPage();
- }
- }
-
- componentWillUnmount() {
- this.props.clearBlocklist();
- unregisterPagePopulator(this.repopulate);
- }
-
- //
- // Control
-
- repopulate = () => {
- this.props.fetchBlocklist();
- };
- //
- // Listeners
-
- onFirstPagePress = () => {
- this.props.gotoBlocklistFirstPage();
- };
-
- onPreviousPagePress = () => {
- this.props.gotoBlocklistPreviousPage();
- };
-
- onNextPagePress = () => {
- this.props.gotoBlocklistNextPage();
- };
-
- onLastPagePress = () => {
- this.props.gotoBlocklistLastPage();
- };
-
- onPageSelect = (page) => {
- this.props.gotoBlocklistPage({ page });
- };
-
- onRemoveSelected = (ids) => {
- this.props.removeBlocklistItems({ ids });
- };
-
- onSortPress = (sortKey) => {
- this.props.setBlocklistSort({ sortKey });
- };
-
- onFilterSelect = (selectedFilterKey) => {
- this.props.setBlocklistFilter({ selectedFilterKey });
- };
-
- onClearBlocklistPress = () => {
- this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
- };
-
- onTableOptionChange = (payload) => {
- this.props.setBlocklistTableOption(payload);
-
- if (payload.pageSize) {
- this.props.gotoBlocklistFirstPage();
- }
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-BlocklistConnector.propTypes = {
- useCurrentPage: PropTypes.bool.isRequired,
- isClearingBlocklistExecuting: PropTypes.bool.isRequired,
- items: PropTypes.arrayOf(PropTypes.object).isRequired,
- fetchBlocklist: PropTypes.func.isRequired,
- gotoBlocklistFirstPage: PropTypes.func.isRequired,
- gotoBlocklistPreviousPage: PropTypes.func.isRequired,
- gotoBlocklistNextPage: PropTypes.func.isRequired,
- gotoBlocklistLastPage: PropTypes.func.isRequired,
- gotoBlocklistPage: PropTypes.func.isRequired,
- removeBlocklistItems: PropTypes.func.isRequired,
- setBlocklistSort: PropTypes.func.isRequired,
- setBlocklistFilter: PropTypes.func.isRequired,
- setBlocklistTableOption: PropTypes.func.isRequired,
- clearBlocklist: PropTypes.func.isRequired,
- executeCommand: PropTypes.func.isRequired
-};
-
-export default withCurrentPage(
- connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
-);
diff --git a/frontend/src/Activity/Blocklist/BlocklistDetailsModal.js b/frontend/src/Activity/Blocklist/BlocklistDetailsModal.js
deleted file mode 100644
index 5f8b98d3d..000000000
--- a/frontend/src/Activity/Blocklist/BlocklistDetailsModal.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import DescriptionList from 'Components/DescriptionList/DescriptionList';
-import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
-import Button from 'Components/Link/Button';
-import Modal from 'Components/Modal/Modal';
-import ModalBody from 'Components/Modal/ModalBody';
-import ModalContent from 'Components/Modal/ModalContent';
-import ModalFooter from 'Components/Modal/ModalFooter';
-import ModalHeader from 'Components/Modal/ModalHeader';
-import translate from 'Utilities/String/translate';
-
-class BlocklistDetailsModal extends Component {
-
- //
- // Render
-
- render() {
- const {
- isOpen,
- sourceTitle,
- protocol,
- indexer,
- message,
- onModalClose
- } = this.props;
-
- return (
-
-
-
- Details
-
-
-
-
-
-
-
-
- {
- !!message &&
-
- }
-
- {
- !!message &&
-
- }
-
-
-
-
-
-
-
-
- );
- }
-}
-
-BlocklistDetailsModal.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- sourceTitle: PropTypes.string.isRequired,
- protocol: PropTypes.string.isRequired,
- indexer: PropTypes.string,
- message: PropTypes.string,
- onModalClose: PropTypes.func.isRequired
-};
-
-export default BlocklistDetailsModal;
diff --git a/frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx b/frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
new file mode 100644
index 000000000..ec026ae92
--- /dev/null
+++ b/frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import DescriptionList from 'Components/DescriptionList/DescriptionList';
+import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
+import Button from 'Components/Link/Button';
+import Modal from 'Components/Modal/Modal';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import DownloadProtocol from 'DownloadClient/DownloadProtocol';
+import translate from 'Utilities/String/translate';
+
+interface BlocklistDetailsModalProps {
+ isOpen: boolean;
+ sourceTitle: string;
+ protocol: DownloadProtocol;
+ indexer?: string;
+ message?: string;
+ onModalClose: () => void;
+}
+
+function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
+ const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
+ props;
+
+ return (
+
+
+ Details
+
+
+
+
+
+
+
+ {message ? (
+
+ ) : null}
+
+ {message ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default BlocklistDetailsModal;
diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.js b/frontend/src/Activity/Blocklist/BlocklistRow.js
deleted file mode 100644
index b6bd2863c..000000000
--- a/frontend/src/Activity/Blocklist/BlocklistRow.js
+++ /dev/null
@@ -1,212 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import IconButton from 'Components/Link/IconButton';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
-import TableRow from 'Components/Table/TableRow';
-import EpisodeFormats from 'Episode/EpisodeFormats';
-import EpisodeLanguages from 'Episode/EpisodeLanguages';
-import EpisodeQuality from 'Episode/EpisodeQuality';
-import { icons, kinds } from 'Helpers/Props';
-import SeriesTitleLink from 'Series/SeriesTitleLink';
-import translate from 'Utilities/String/translate';
-import BlocklistDetailsModal from './BlocklistDetailsModal';
-import styles from './BlocklistRow.css';
-
-class BlocklistRow extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- isDetailsModalOpen: false
- };
- }
-
- //
- // Listeners
-
- onDetailsPress = () => {
- this.setState({ isDetailsModalOpen: true });
- };
-
- onDetailsModalClose = () => {
- this.setState({ isDetailsModalOpen: false });
- };
-
- //
- // Render
-
- render() {
- const {
- id,
- series,
- sourceTitle,
- languages,
- quality,
- customFormats,
- date,
- protocol,
- indexer,
- message,
- isSelected,
- columns,
- onSelectedChange,
- onRemovePress
- } = this.props;
-
- return (
-
-
-
- {
- columns.map((column) => {
- const {
- name,
- isVisible
- } = column;
-
- if (!isVisible) {
- return null;
- }
-
- if (name === 'series.sortTitle') {
- return (
-
-
-
- );
- }
-
- if (name === 'sourceTitle') {
- return (
-
- {sourceTitle}
-
- );
- }
-
- if (name === 'languages') {
- return (
-
-
-
- );
- }
-
- if (name === 'quality') {
- return (
-
-
-
- );
- }
-
- if (name === 'customFormats') {
- return (
-
-
-
- );
- }
-
- if (name === 'date') {
- return (
-
- );
- }
-
- if (name === 'indexer') {
- return (
-
- {indexer}
-
- );
- }
-
- if (name === 'actions') {
- return (
-
-
-
-
-
- );
- }
-
- return null;
- })
- }
-
-
-
- );
- }
-
-}
-
-BlocklistRow.propTypes = {
- id: PropTypes.number.isRequired,
- series: PropTypes.object.isRequired,
- sourceTitle: PropTypes.string.isRequired,
- languages: PropTypes.arrayOf(PropTypes.object).isRequired,
- quality: PropTypes.object.isRequired,
- customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
- date: PropTypes.string.isRequired,
- 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
-};
-
-export default BlocklistRow;
diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.tsx b/frontend/src/Activity/Blocklist/BlocklistRow.tsx
new file mode 100644
index 000000000..58d75b1dd
--- /dev/null
+++ b/frontend/src/Activity/Blocklist/BlocklistRow.tsx
@@ -0,0 +1,163 @@
+import React, { useCallback, useState } from 'react';
+import { useDispatch } from 'react-redux';
+import IconButton from 'Components/Link/IconButton';
+import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
+import Column from 'Components/Table/Column';
+import TableRow from 'Components/Table/TableRow';
+import EpisodeFormats from 'Episode/EpisodeFormats';
+import EpisodeLanguages from 'Episode/EpisodeLanguages';
+import EpisodeQuality from 'Episode/EpisodeQuality';
+import { icons, kinds } from 'Helpers/Props';
+import SeriesTitleLink from 'Series/SeriesTitleLink';
+import useSeries from 'Series/useSeries';
+import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
+import Blocklist from 'typings/Blocklist';
+import { SelectStateInputProps } from 'typings/props';
+import translate from 'Utilities/String/translate';
+import BlocklistDetailsModal from './BlocklistDetailsModal';
+import styles from './BlocklistRow.css';
+
+interface BlocklistRowProps extends Blocklist {
+ isSelected: boolean;
+ columns: Column[];
+ onSelectedChange: (options: SelectStateInputProps) => void;
+}
+
+function BlocklistRow(props: BlocklistRowProps) {
+ const {
+ id,
+ seriesId,
+ sourceTitle,
+ languages,
+ quality,
+ customFormats,
+ date,
+ protocol,
+ indexer,
+ message,
+ isSelected,
+ columns,
+ onSelectedChange,
+ } = props;
+
+ const series = useSeries(seriesId);
+ const dispatch = useDispatch();
+ const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
+
+ const handleDetailsPress = useCallback(() => {
+ setIsDetailsModalOpen(true);
+ }, [setIsDetailsModalOpen]);
+
+ const handleDetailsModalClose = useCallback(() => {
+ setIsDetailsModalOpen(false);
+ }, [setIsDetailsModalOpen]);
+
+ const handleRemovePress = useCallback(() => {
+ dispatch(removeBlocklistItem({ id }));
+ }, [id, dispatch]);
+
+ if (!series) {
+ return null;
+ }
+
+ return (
+
+
+
+ {columns.map((column) => {
+ const { name, isVisible } = column;
+
+ if (!isVisible) {
+ return null;
+ }
+
+ if (name === 'series.sortTitle') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'sourceTitle') {
+ return {sourceTitle};
+ }
+
+ if (name === 'languages') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'quality') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'customFormats') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'date') {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore ts(2739)
+ return ;
+ }
+
+ if (name === 'indexer') {
+ return (
+
+ {indexer}
+
+ );
+ }
+
+ if (name === 'actions') {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return null;
+ })}
+
+
+
+ );
+}
+
+export default BlocklistRow;
diff --git a/frontend/src/Activity/Blocklist/BlocklistRowConnector.js b/frontend/src/Activity/Blocklist/BlocklistRowConnector.js
deleted file mode 100644
index f0b93cd25..000000000
--- a/frontend/src/Activity/Blocklist/BlocklistRowConnector.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
-import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
-import BlocklistRow from './BlocklistRow';
-
-function createMapStateToProps() {
- return createSelector(
- createSeriesSelector(),
- (series) => {
- return {
- series
- };
- }
- );
-}
-
-function createMapDispatchToProps(dispatch, props) {
- return {
- onRemovePress() {
- dispatch(removeBlocklistItem({ id: props.id }));
- }
- };
-}
-
-export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js
index 34e23ac3f..e7f8a37ff 100644
--- a/frontend/src/App/AppRoutes.js
+++ b/frontend/src/App/AppRoutes.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
-import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
+import Blocklist from 'Activity/Blocklist/Blocklist';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
@@ -135,7 +135,7 @@ function AppRoutes(props) {
{/*
diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts
index 30af90d34..f89eb25f7 100644
--- a/frontend/src/App/State/AppSectionState.ts
+++ b/frontend/src/App/State/AppSectionState.ts
@@ -1,5 +1,6 @@
+import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
-import { FilterBuilderProp } from './AppState';
+import { FilterBuilderProp, PropertyFilter } from './AppState';
export interface Error {
responseJSON: {
@@ -18,11 +19,18 @@ export interface AppSectionSaveState {
}
export interface PagedAppSectionState {
+ page: number;
pageSize: number;
+ totalPages: number;
totalRecords?: number;
}
+export interface TableAppSectionState {
+ columns: Column[];
+}
export interface AppSectionFilterState {
+ selectedFilterKey: string;
+ filters: PropertyFilter[];
filterBuilderProps: FilterBuilderProp[];
}
diff --git a/frontend/src/App/State/BlocklistAppState.ts b/frontend/src/App/State/BlocklistAppState.ts
index e838ad625..004a30732 100644
--- a/frontend/src/App/State/BlocklistAppState.ts
+++ b/frontend/src/App/State/BlocklistAppState.ts
@@ -1,8 +1,16 @@
import Blocklist from 'typings/Blocklist';
-import AppSectionState, { AppSectionFilterState } from './AppSectionState';
+import AppSectionState, {
+ AppSectionFilterState,
+ PagedAppSectionState,
+ TableAppSectionState,
+} from './AppSectionState';
interface BlocklistAppState
extends AppSectionState,
- AppSectionFilterState {}
+ AppSectionFilterState,
+ PagedAppSectionState,
+ TableAppSectionState {
+ isRemoving: boolean;
+}
export default BlocklistAppState;
diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts
index 31a696df7..f5644357b 100644
--- a/frontend/src/Components/Table/Column.ts
+++ b/frontend/src/Components/Table/Column.ts
@@ -2,6 +2,7 @@ import React from 'react';
type PropertyFunction = () => T;
+// TODO: Convert to generic so `name` can be a type
interface Column {
name: string;
label: string | PropertyFunction | React.ReactNode;
diff --git a/frontend/src/Components/Table/usePaging.ts b/frontend/src/Components/Table/usePaging.ts
new file mode 100644
index 000000000..dfebb2355
--- /dev/null
+++ b/frontend/src/Components/Table/usePaging.ts
@@ -0,0 +1,54 @@
+import { useCallback, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
+
+interface PagingOptions {
+ page: number;
+ totalPages: number;
+ gotoPage: ({ page }: { page: number }) => void;
+}
+
+function usePaging(options: PagingOptions) {
+ const { page, totalPages, gotoPage } = options;
+ const dispatch = useDispatch();
+
+ const handleFirstPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: 1 }));
+ }, [dispatch, gotoPage]);
+
+ const handlePreviousPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: Math.max(page - 1, 1) }));
+ }, [page, dispatch, gotoPage]);
+
+ const handleNextPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: Math.min(page + 1, totalPages) }));
+ }, [page, totalPages, dispatch, gotoPage]);
+
+ const handleLastPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: totalPages }));
+ }, [totalPages, dispatch, gotoPage]);
+
+ const handlePageSelect = useCallback(
+ (page: number) => {
+ dispatch(gotoPage({ page }));
+ },
+ [dispatch, gotoPage]
+ );
+
+ return useMemo(() => {
+ return {
+ handleFirstPagePress,
+ handlePreviousPagePress,
+ handleNextPagePress,
+ handleLastPagePress,
+ handlePageSelect,
+ };
+ }, [
+ handleFirstPagePress,
+ handlePreviousPagePress,
+ handleNextPagePress,
+ handleLastPagePress,
+ handlePageSelect,
+ ]);
+}
+
+export default usePaging;
diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts
index 090a1a087..417db8178 100644
--- a/frontend/src/DownloadClient/DownloadProtocol.ts
+++ b/frontend/src/DownloadClient/DownloadProtocol.ts
@@ -1,7 +1,3 @@
-enum DownloadProtocol {
- Unknown = 'unknown',
- Usenet = 'usenet',
- Torrent = 'torrent',
-}
+type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
export default DownloadProtocol;
diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts
new file mode 100644
index 000000000..3caf66df2
--- /dev/null
+++ b/frontend/src/Helpers/Hooks/useCurrentPage.ts
@@ -0,0 +1,9 @@
+import { useHistory } from 'react-router-dom';
+
+function useCurrentPage() {
+ const history = useHistory();
+
+ return history.action === 'POP';
+}
+
+export default useCurrentPage;
diff --git a/frontend/src/Series/useSeries.ts b/frontend/src/Series/useSeries.ts
new file mode 100644
index 000000000..073f41541
--- /dev/null
+++ b/frontend/src/Series/useSeries.ts
@@ -0,0 +1,19 @@
+import { useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+
+export function createSeriesSelector(seriesId?: number) {
+ return createSelector(
+ (state: AppState) => state.series.itemMap,
+ (state: AppState) => state.series.items,
+ (itemMap, allSeries) => {
+ return seriesId ? allSeries[itemMap[seriesId]] : undefined;
+ }
+ );
+}
+
+function useSeries(seriesId?: number) {
+ return useSelector(createSeriesSelector(seriesId));
+}
+
+export default useSeries;
diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js
index 6303ad2d1..87ffe7f7c 100644
--- a/frontend/src/Store/Actions/blocklistActions.js
+++ b/frontend/src/Store/Actions/blocklistActions.js
@@ -117,10 +117,6 @@ export const persistState = [
// Action Types
export const FETCH_BLOCKLIST = 'blocklist/fetchBlocklist';
-export const GOTO_FIRST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistFirstPage';
-export const GOTO_PREVIOUS_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPreviousPage';
-export const GOTO_NEXT_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistNextPage';
-export const GOTO_LAST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistLastPage';
export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage';
export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort';
export const SET_BLOCKLIST_FILTER = 'blocklist/setBlocklistFilter';
@@ -133,10 +129,6 @@ export const CLEAR_BLOCKLIST = 'blocklist/clearBlocklist';
// Action Creators
export const fetchBlocklist = createThunk(FETCH_BLOCKLIST);
-export const gotoBlocklistFirstPage = createThunk(GOTO_FIRST_BLOCKLIST_PAGE);
-export const gotoBlocklistPreviousPage = createThunk(GOTO_PREVIOUS_BLOCKLIST_PAGE);
-export const gotoBlocklistNextPage = createThunk(GOTO_NEXT_BLOCKLIST_PAGE);
-export const gotoBlocklistLastPage = createThunk(GOTO_LAST_BLOCKLIST_PAGE);
export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE);
export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT);
export const setBlocklistFilter = createThunk(SET_BLOCKLIST_FILTER);
@@ -155,10 +147,6 @@ export const actionHandlers = handleThunks({
fetchBlocklist,
{
[serverSideCollectionHandlers.FETCH]: FETCH_BLOCKLIST,
- [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_BLOCKLIST_PAGE,
- [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_BLOCKLIST_PAGE,
- [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLOCKLIST_PAGE,
- [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE,
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT,
[serverSideCollectionHandlers.FILTER]: SET_BLOCKLIST_FILTER
diff --git a/frontend/src/typings/Blocklist.ts b/frontend/src/typings/Blocklist.ts
index 4cc675cc5..bbf4cacae 100644
--- a/frontend/src/typings/Blocklist.ts
+++ b/frontend/src/typings/Blocklist.ts
@@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
+import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
@@ -9,8 +10,11 @@ interface Blocklist extends ModelBase {
customFormats: CustomFormat[];
title: string;
date?: string;
- protocol: string;
+ protocol: DownloadProtocol;
+ sourceTitle: string;
seriesId?: number;
+ indexer?: string;
+ message?: string;
}
export default Blocklist;
diff --git a/frontend/src/typings/Table.ts b/frontend/src/typings/Table.ts
new file mode 100644
index 000000000..4f99e2045
--- /dev/null
+++ b/frontend/src/typings/Table.ts
@@ -0,0 +1,6 @@
+import Column from 'Components/Table/Column';
+
+export interface TableOptionsChangePayload {
+ pageSize?: number;
+ columns: Column[];
+}
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 42ad9dc5c..01456d5b0 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -1248,6 +1248,7 @@
"NextExecution": "Next Execution",
"No": "No",
"NoBackupsAreAvailable": "No backups are available",
+ "NoBlocklistItems": "No blocklist items",
"NoChange": "No Change",
"NoChanges": "No Changes",
"NoDelay": "No Delay",
@@ -1259,7 +1260,6 @@
"NoEpisodesInThisSeason": "No episodes in this season",
"NoEventsFound": "No events found",
"NoHistory": "No history",
- "NoHistoryBlocklist": "No history blocklist",
"NoHistoryFound": "No history found",
"NoImportListsFound": "No import lists found",
"NoIndexersFound": "No indexers found",