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..4163bc9ca
--- /dev/null
+++ b/frontend/src/Activity/Blocklist/Blocklist.tsx
@@ -0,0 +1,329 @@
+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,
+ pageSize,
+ 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.css b/frontend/src/Activity/Blocklist/BlocklistRow.css
index c7d31a886..aa008a6ce 100644
--- a/frontend/src/Activity/Blocklist/BlocklistRow.css
+++ b/frontend/src/Activity/Blocklist/BlocklistRow.css
@@ -1,4 +1,4 @@
-.language,
+.languages,
.quality {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.css.d.ts b/frontend/src/Activity/Blocklist/BlocklistRow.css.d.ts
index 4f4907a93..cc16b7e9e 100644
--- a/frontend/src/Activity/Blocklist/BlocklistRow.css.d.ts
+++ b/frontend/src/Activity/Blocklist/BlocklistRow.css.d.ts
@@ -3,7 +3,7 @@
interface CssExports {
'actions': string;
'indexer': string;
- 'language': string;
+ 'languages': string;
'quality': string;
}
export const cssExports: CssExports;
diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.js b/frontend/src/Activity/Blocklist/BlocklistRow.js
deleted file mode 100644
index ce95ecf11..000000000
--- a/frontend/src/Activity/Blocklist/BlocklistRow.js
+++ /dev/null
@@ -1,213 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import IconButton from 'Components/Link/IconButton';
-import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
-import TableRow from 'Components/Table/TableRow';
-import { icons, kinds } from 'Helpers/Props';
-import MovieFormats from 'Movie/MovieFormats';
-import MovieLanguages from 'Movie/MovieLanguages';
-import MovieQuality from 'Movie/MovieQuality';
-import MovieTitleLink from 'Movie/MovieTitleLink';
-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,
- movie,
- sourceTitle,
- quality,
- customFormats,
- languages,
- date,
- protocol,
- indexer,
- message,
- isSelected,
- columns,
- onSelectedChange,
- onRemovePress
- } = this.props;
-
- if (!movie) {
- return null;
- }
-
- return (
-
-
-
- {
- columns.map((column) => {
- const {
- name,
- isVisible
- } = column;
-
- if (!isVisible) {
- return null;
- }
-
- if (name === 'movieMetadata.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,
- movie: PropTypes.object.isRequired,
- sourceTitle: PropTypes.string.isRequired,
- quality: PropTypes.object.isRequired,
- customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
- languages: 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..555cea3b5
--- /dev/null
+++ b/frontend/src/Activity/Blocklist/BlocklistRow.tsx
@@ -0,0 +1,160 @@
+import React, { useCallback, useState } from 'react';
+import { useDispatch } from 'react-redux';
+import IconButton from 'Components/Link/IconButton';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
+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 { icons, kinds } from 'Helpers/Props';
+import MovieFormats from 'Movie/MovieFormats';
+import MovieLanguages from 'Movie/MovieLanguages';
+import MovieQuality from 'Movie/MovieQuality';
+import MovieTitleLink from 'Movie/MovieTitleLink';
+import useMovie from 'Movie/useMovie';
+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,
+ movieId,
+ sourceTitle,
+ languages,
+ quality,
+ customFormats,
+ date,
+ protocol,
+ indexer,
+ message,
+ isSelected,
+ columns,
+ onSelectedChange,
+ } = props;
+
+ const movie = useMovie(movieId);
+ 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 (!movie) {
+ return null;
+ }
+
+ return (
+
+
+
+ {columns.map((column) => {
+ const { name, isVisible } = column;
+
+ if (!isVisible) {
+ return null;
+ }
+
+ if (name === 'movieMetadata.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 06091009a..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 createMovieSelector from 'Store/Selectors/createMovieSelector';
-import BlocklistRow from './BlocklistRow';
-
-function createMapStateToProps() {
- return createSelector(
- createMovieSelector(),
- (movie) => {
- return {
- movie
- };
- }
- );
-}
-
-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 882e5d539..de41ccd4e 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 AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
@@ -120,7 +120,7 @@ function AppRoutes(props) {
{/*
diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts
index a0bed6b2d..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: {
@@ -23,8 +24,13 @@ export interface PagedAppSectionState {
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 e49d67c44..24674c3fc 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/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/Movie/useMovie.ts b/frontend/src/Movie/useMovie.ts
new file mode 100644
index 000000000..c1f8cedb8
--- /dev/null
+++ b/frontend/src/Movie/useMovie.ts
@@ -0,0 +1,19 @@
+import { useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+
+export function createMovieSelector(movieId?: number) {
+ return createSelector(
+ (state: AppState) => state.movies.itemMap,
+ (state: AppState) => state.movies.items,
+ (itemMap, allMovies) => {
+ return movieId ? allMovies[itemMap[movieId]] : undefined;
+ }
+ );
+}
+
+function useMovie(movieId?: number) {
+ return useSelector(createMovieSelector(movieId));
+}
+
+export default useMovie;
diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js
index ab093ad78..f95d49f42 100644
--- a/frontend/src/Store/Actions/blocklistActions.js
+++ b/frontend/src/Store/Actions/blocklistActions.js
@@ -119,10 +119,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';
@@ -135,10 +131,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);
@@ -157,10 +149,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 846c6fab7..15655d0c2 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;
movieId?: number;
+ indexer?: string;
+ message?: string;
}
export default Blocklist;
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 3ad1dfdab..6bd870da3 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -1014,6 +1014,7 @@
"No": "No",
"NoAltTitle": "No alternative titles.",
"NoBackupsAreAvailable": "No backups are available",
+ "NoBlocklistItems": "No blocklist items",
"NoChange": "No Change",
"NoChanges": "No Changes",
"NoCollections": "No collections found, to get started you'll want to add a new movie, or import some existing ones",
@@ -1023,7 +1024,6 @@
"NoEventsFound": "No events found",
"NoExtraFilesToManage": "No extra files to manage.",
"NoHistory": "No history",
- "NoHistoryBlocklist": "No history blocklist",
"NoHistoryFound": "No history found",
"NoImportListsFound": "No import lists found",
"NoIndexersFound": "No indexers found",