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