From 428569106499b5e3a463f1990ae2996d1ae4ab49 Mon Sep 17 00:00:00 2001 From: The Dark <12370876+CheAle14@users.noreply.github.com> Date: Sun, 3 Mar 2024 05:19:02 +0000 Subject: [PATCH] New: Import list exclusion pagination Closes #6079 --- frontend/src/App/State/AppSectionState.ts | 1 + frontend/src/App/State/SettingsAppState.ts | 11 + .../src/Helpers/Hooks/useModalOpenState.ts | 17 ++ .../EditImportListExclusionModal.js | 27 -- .../EditImportListExclusionModal.tsx | 41 +++ .../EditImportListExclusionModalConnector.js | 43 ---- .../EditImportListExclusionModalContent.js | 139 ----------- .../EditImportListExclusionModalContent.tsx | 188 ++++++++++++++ ...mportListExclusionModalContentConnector.js | 117 --------- .../ImportListExclusion.css | 25 -- .../ImportListExclusion.css.d.ts | 3 - .../ImportListExclusion.js | 112 --------- .../ImportListExclusionRow.css | 6 + .../ImportListExclusionRow.css.d.ts | 7 + .../ImportListExclusionRow.tsx | 68 +++++ .../ImportListExclusions.css | 23 -- .../ImportListExclusions.css.d.ts | 3 - .../ImportListExclusions.js | 105 -------- .../ImportListExclusions.tsx | 234 ++++++++++++++++++ .../ImportListExclusionsConnector.js | 59 ----- .../ImportLists/ImportListSettings.js | 4 +- .../Actions/Settings/importListExclusions.js | 43 +++- .../createSettingsSectionSelector.ts | 51 ++-- frontend/src/typings/ImportListExclusion.ts | 6 + .../Exclusions/ImportListExclusionService.cs | 7 + .../ImportListExclusionController.cs | 13 + 26 files changed, 663 insertions(+), 690 deletions(-) create mode 100644 frontend/src/Helpers/Hooks/useModalOpenState.ts delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js create mode 100644 frontend/src/typings/ImportListExclusion.ts diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index cabc39b1c..5bc7dfbac 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -38,6 +38,7 @@ export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; error: Error; + pendingChanges: Partial; item: T; } diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index a0bea0973..e4322db69 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -3,10 +3,12 @@ import AppSectionState, { AppSectionItemState, AppSectionSaveState, AppSectionSchemaState, + PagedAppSectionState, } from 'App/State/AppSectionState'; import Language from 'Language/Language'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; +import ImportListExclusion from 'typings/ImportListExclusion'; import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; @@ -41,6 +43,14 @@ export interface ImportListOptionsSettingsAppState extends AppSectionItemState, AppSectionSaveState {} +export interface ImportListExclusionsSettingsAppState + extends AppSectionState, + AppSectionSaveState, + PagedAppSectionState, + AppSectionDeleteState { + pendingChanges: Partial; +} + export type IndexerFlagSettingsAppState = AppSectionState; export type LanguageSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionItemState; @@ -48,6 +58,7 @@ export type UiSettingsAppState = AppSectionItemState; interface SettingsAppState { advancedSettings: boolean; downloadClients: DownloadClientAppState; + importListExclusions: ImportListExclusionsSettingsAppState; importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts new file mode 100644 index 000000000..f5b5a96f0 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts @@ -0,0 +1,17 @@ +import { useCallback, useState } from 'react'; + +export default function useModalOpenState( + initialState: boolean +): [boolean, () => void, () => void] { + const [isOpen, setOpen] = useState(initialState); + + const setModalOpen = useCallback(() => { + setOpen(true); + }, [setOpen]); + + const setModalClosed = useCallback(() => { + setOpen(false); + }, [setOpen]); + + return [isOpen, setModalOpen, setModalClosed]; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js deleted file mode 100644 index 57a7b0e2d..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector'; - -function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditImportListExclusionModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditImportListExclusionModal; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx new file mode 100644 index 000000000..9b7afb3ba --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx @@ -0,0 +1,41 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditImportListExclusionModalContent from './EditImportListExclusionModalContent'; + +interface EditImportListExclusionModalProps { + id?: number; + isOpen: boolean; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function EditImportListExclusionModal( + props: EditImportListExclusionModalProps +) { + const { isOpen, onModalClose, ...otherProps } = props; + + const dispatch = useDispatch(); + + const onModalClosePress = useCallback(() => { + dispatch( + clearPendingChanges({ + section: 'settings.importListExclusions', + }) + ); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditImportListExclusionModal; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js deleted file mode 100644 index cd4338621..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditImportListExclusionModal from './EditImportListExclusionModal'; - -function mapStateToProps() { - return {}; -} - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditImportListExclusionModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'settings.importListExclusions' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditImportListExclusionModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js deleted file mode 100644 index 284d1100c..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -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 { inputTypes, kinds } from 'Helpers/Props'; -import { numberSettingShape, stringSettingShape } from 'Helpers/Props/Shapes/settingShape'; -import translate from 'Utilities/String/translate'; -import styles from './EditImportListExclusionModalContent.css'; - -function EditImportListExclusionModalContent(props) { - const { - id, - isFetching, - error, - isSaving, - saveError, - item, - onInputChange, - onSavePress, - onModalClose, - onDeleteImportListExclusionPress, - ...otherProps - } = props; - - const { - title, - tvdbId - } = item; - - return ( - - - {id ? translate('EditImportListExclusion') : translate('AddImportListExclusion')} - - - - { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('AddImportListExclusionError')} - - } - - { - !isFetching && !error && -
- - {translate('Title')} - - - - - - {translate('TvdbId')} - - - -
- } -
- - - { - id && - - } - - - - - {translate('Save')} - - -
- ); -} - -const ImportListExclusionShape = { - title: PropTypes.shape(stringSettingShape).isRequired, - tvdbId: PropTypes.shape(numberSettingShape).isRequired -}; - -EditImportListExclusionModalContent.propTypes = { - id: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.shape(ImportListExclusionShape).isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onDeleteImportListExclusionPress: PropTypes.func -}; - -export default EditImportListExclusionModalContent; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx new file mode 100644 index 000000000..8570d1acf --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -0,0 +1,188 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +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 usePrevious from 'Helpers/Hooks/usePrevious'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { + saveImportListExclusion, + setImportListExclusionValue, +} from 'Store/Actions/settingsActions'; +import selectSettings from 'Store/Selectors/selectSettings'; +import ImportListExclusion from 'typings/ImportListExclusion'; +import { PendingSection } from 'typings/pending'; +import translate from 'Utilities/String/translate'; +import styles from './EditImportListExclusionModalContent.css'; + +const newImportListExclusion = { + title: '', + tvdbId: 0, +}; + +interface EditImportListExclusionModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function createImportListExclusionSelector(id?: number) { + return createSelector( + (state: AppState) => state.settings.importListExclusions, + (importListExclusions) => { + const { isFetching, error, isSaving, saveError, pendingChanges, items } = + importListExclusions; + + const mapping = id + ? items.find((i) => i.id === id) + : newImportListExclusion; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings as PendingSection, + ...settings, + }; + } + ); +} + +function EditImportListExclusionModalContent( + props: EditImportListExclusionModalContentProps +) { + const { id, onModalClose, onDeleteImportListExclusionPress } = props; + + const dispatch = useDispatch(); + + const dispatchSetImportListExclusionValue = (payload: { + name: string; + value: string | number; + }) => { + // @ts-expect-error 'setImportListExclusionValue' isn't typed yet + dispatch(setImportListExclusionValue(payload)); + }; + + const { isFetching, isSaving, item, error, saveError, ...otherProps } = + useSelector(createImportListExclusionSelector(props.id)); + const previousIsSaving = usePrevious(isSaving); + + const { title, tvdbId } = item; + + useEffect(() => { + if (!id) { + Object.keys(newImportListExclusion).forEach((name) => { + dispatchSetImportListExclusionValue({ + name, + value: + newImportListExclusion[name as keyof typeof newImportListExclusion], + }); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (previousIsSaving && !isSaving && !saveError) { + onModalClose(); + } + }); + + const onSavePress = useCallback(() => { + dispatch(saveImportListExclusion({ id })); + }, [dispatch, id]); + + const onInputChange = useCallback( + (payload: { name: string; value: string | number }) => { + // @ts-expect-error 'setImportListExclusionValue' isn't typed yet + dispatch(setImportListExclusionValue(payload)); + }, + [dispatch] + ); + + return ( + + + {id + ? translate('EditImportListExclusion') + : translate('AddImportListExclusion')} + + + + {isFetching && } + + {!isFetching && !!error && ( + + {translate('AddImportListExclusionError')} + + )} + + {!isFetching && !error && ( +
+ + {translate('Title')} + + + + + + {translate('TvdbId')} + + + +
+ )} +
+ + + {id && ( + + )} + + + + + {translate('Save')} + + +
+ ); +} + +export default EditImportListExclusionModalContent; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js deleted file mode 100644 index 059223231..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js +++ /dev/null @@ -1,117 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveImportListExclusion, setImportListExclusionValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditImportListExclusionModalContent from './EditImportListExclusionModalContent'; - -const newImportListExclusion = { - title: '', - tvdbId: 0 -}; - -function createImportListExclusionSelector() { - return createSelector( - (state, { id }) => id, - (state) => state.settings.importListExclusions, - (id, importListExclusions) => { - const { - isFetching, - error, - isSaving, - saveError, - pendingChanges, - items - } = importListExclusions; - - const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion; - const settings = selectSettings(mapping, pendingChanges, saveError); - - return { - id, - isFetching, - error, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -function createMapStateToProps() { - return createSelector( - createImportListExclusionSelector(), - (importListExclusion) => { - return { - ...importListExclusion - }; - } - ); -} - -const mapDispatchToProps = { - setImportListExclusionValue, - saveImportListExclusion -}; - -class EditImportListExclusionModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.id) { - Object.keys(newImportListExclusion).forEach((name) => { - this.props.setImportListExclusionValue({ - name, - value: newImportListExclusion[name] - }); - }); - } - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setImportListExclusionValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveImportListExclusion({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditImportListExclusionModalContentConnector.propTypes = { - id: PropTypes.number, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setImportListExclusionValue: PropTypes.func.isRequired, - saveImportListExclusion: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListExclusionModalContentConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css deleted file mode 100644 index 92e533c7e..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css +++ /dev/null @@ -1,25 +0,0 @@ -.importListExclusion { - display: flex; - align-items: stretch; - margin-bottom: 10px; - height: 30px; - border-bottom: 1px solid var(--borderColor); - line-height: 30px; -} - -.title { - @add-mixin truncate; - - flex: 0 1 600px; -} - -.tvdbId { - flex: 0 0 70px; -} - -.actions { - display: flex; - justify-content: flex-end; - flex: 1 0 auto; - padding-right: 10px; -} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts index 213f9816d..d8ea83dc1 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts @@ -2,9 +2,6 @@ // Please do not change this file! interface CssExports { 'actions': string; - 'importListExclusion': string; - 'title': string; - 'tvdbId': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js deleted file mode 100644 index e95561b82..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js +++ /dev/null @@ -1,112 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector'; -import styles from './ImportListExclusion.css'; - -class ImportListExclusion extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditImportListExclusionModalOpen: false, - isDeleteImportListExclusionModalOpen: false - }; - } - - // - // Listeners - - onEditImportListExclusionPress = () => { - this.setState({ isEditImportListExclusionModalOpen: true }); - }; - - onEditImportListExclusionModalClose = () => { - this.setState({ isEditImportListExclusionModalOpen: false }); - }; - - onDeleteImportListExclusionPress = () => { - this.setState({ - isEditImportListExclusionModalOpen: false, - isDeleteImportListExclusionModalOpen: true - }); - }; - - onDeleteImportListExclusionModalClose = () => { - this.setState({ isDeleteImportListExclusionModalOpen: false }); - }; - - onConfirmDeleteImportListExclusion = () => { - this.props.onConfirmDeleteImportListExclusion(this.props.id); - }; - - // - // Render - - render() { - const { - id, - title, - tvdbId - } = this.props; - - return ( -
-
{title}
-
{tvdbId}
- -
- - - -
- - - - -
- ); - } -} - -ImportListExclusion.propTypes = { - id: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - tvdbId: PropTypes.number.isRequired, - onConfirmDeleteImportListExclusion: PropTypes.func.isRequired -}; - -ImportListExclusion.defaultProps = { - // The drag preview will not connect the drag handle. - connectDragSource: (node) => node -}; - -export default ImportListExclusion; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css new file mode 100644 index 000000000..c154fa5a3 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css @@ -0,0 +1,6 @@ +.actions { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 35px; + white-space: nowrap; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts new file mode 100644 index 000000000..d8ea83dc1 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'actions': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx new file mode 100644 index 000000000..37de7940a --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons, kinds } from 'Helpers/Props'; +import ImportListExclusion from 'typings/ImportListExclusion'; +import translate from 'Utilities/String/translate'; +import EditImportListExclusionModal from './EditImportListExclusionModal'; +import styles from './ImportListExclusionRow.css'; + +interface ImportListExclusionRowProps extends ImportListExclusion { + onConfirmDeleteImportListExclusion: (id: number) => void; +} + +function ImportListExclusionRow(props: ImportListExclusionRowProps) { + const { id, title, tvdbId, onConfirmDeleteImportListExclusion } = props; + + const [ + isEditImportListExclusionModalOpen, + setEditImportListExclusionModalOpen, + setEditImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const [ + isDeleteImportListExclusionModalOpen, + setDeleteImportListExclusionModalOpen, + setDeleteImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const onConfirmDeleteImportListExclusionPress = useCallback(() => { + onConfirmDeleteImportListExclusion(id); + }, [id, onConfirmDeleteImportListExclusion]); + + return ( + + {title} + {tvdbId} + + + + + + + + + + ); +} + +export default ImportListExclusionRow; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css deleted file mode 100644 index ecb080585..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css +++ /dev/null @@ -1,23 +0,0 @@ -.importListExclusionsHeader { - display: flex; - margin-bottom: 10px; - font-weight: bold; -} - -.title { - flex: 0 1 600px; -} - -.tvdbId { - flex: 0 0 70px; -} - -.addImportListExclusion { - display: flex; - justify-content: flex-end; - padding-right: 10px; -} - -.addButton { - text-align: center; -} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts index 6cb93f7ce..626717e71 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts @@ -3,9 +3,6 @@ interface CssExports { 'addButton': string; 'addImportListExclusion': string; - 'importListExclusionsHeader': string; - 'title': string; - 'tvdbId': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js deleted file mode 100644 index 9bb7814d9..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js +++ /dev/null @@ -1,105 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector'; -import ImportListExclusion from './ImportListExclusion'; -import styles from './ImportListExclusions.css'; - -class ImportListExclusions extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddImportListExclusionModalOpen: false - }; - } - - // - // Listeners - - onAddImportListExclusionPress = () => { - this.setState({ isAddImportListExclusionModalOpen: true }); - }; - - onModalClose = () => { - this.setState({ isAddImportListExclusionModalOpen: false }); - }; - - // - // Render - - render() { - const { - items, - onConfirmDeleteImportListExclusion, - ...otherProps - } = this.props; - - return ( -
- -
-
- {translate('Title')} -
-
- {translate('TvdbId')} -
-
- -
- { - items.map((item, index) => { - return ( - - ); - }) - } -
- -
- - - -
- - - -
-
- ); - } -} - -ImportListExclusions.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteImportListExclusion: PropTypes.func.isRequired -}; - -export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx new file mode 100644 index 000000000..7a15bca91 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -0,0 +1,234 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import IconButton from 'Components/Link/IconButton'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import * as importListExclusionActions from 'Store/Actions/Settings/importListExclusions'; +import { + registerPagePopulator, + unregisterPagePopulator, +} from 'Utilities/pagePopulator'; +import translate from 'Utilities/String/translate'; +import EditImportListExclusionModal from './EditImportListExclusionModal'; +import ImportListExclusionRow from './ImportListExclusionRow'; + +const COLUMNS = [ + { + name: 'title', + label: () => translate('Title'), + isVisible: true, + isSortable: true, + }, + { + name: 'tvdbid', + label: () => translate('TvdbId'), + isVisible: true, + isSortable: true, + }, + { + name: 'actions', + isVisible: true, + isSortable: false, + }, +]; + +interface ImportListExclusionsProps { + useCurrentPage: number; + totalRecords: number; +} + +function createImportListExlucionsSelector() { + return createSelector( + (state: AppState) => state.settings.importListExclusions, + (importListExclusions) => { + return { + ...importListExclusions, + }; + } + ); +} + +function ImportListExclusions(props: ImportListExclusionsProps) { + const { useCurrentPage, totalRecords } = props; + + const dispatch = useDispatch(); + + const fetchImportListExclusions = useCallback(() => { + dispatch(importListExclusionActions.fetchImportListExclusions()); + }, [dispatch]); + + const deleteImportListExclusion = useCallback( + (payload: { id: number }) => { + dispatch(importListExclusionActions.deleteImportListExclusion(payload)); + }, + [dispatch] + ); + + const gotoImportListExclusionFirstPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionFirstPage()); + }, [dispatch]); + + const gotoImportListExclusionPreviousPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionPreviousPage()); + }, [dispatch]); + + const gotoImportListExclusionNextPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionNextPage()); + }, [dispatch]); + + const gotoImportListExclusionLastPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionLastPage()); + }, [dispatch]); + + const gotoImportListExclusionPage = useCallback( + (page: number) => { + dispatch( + importListExclusionActions.gotoImportListExclusionPage({ page }) + ); + }, + [dispatch] + ); + + const setImportListExclusionSort = useCallback( + (sortKey: { sortKey: string }) => { + dispatch( + importListExclusionActions.setImportListExclusionSort({ sortKey }) + ); + }, + [dispatch] + ); + + const setImportListTableOption = useCallback( + (payload: { pageSize: number }) => { + dispatch( + importListExclusionActions.setImportListExclusionTableOption(payload) + ); + + if (payload.pageSize) { + dispatch(importListExclusionActions.gotoImportListExclusionFirstPage()); + } + }, + [dispatch] + ); + + const repopulate = useCallback(() => { + gotoImportListExclusionFirstPage(); + }, [gotoImportListExclusionFirstPage]); + + useEffect(() => { + registerPagePopulator(repopulate); + + if (useCurrentPage) { + fetchImportListExclusions(); + } else { + gotoImportListExclusionFirstPage(); + } + + return () => unregisterPagePopulator(repopulate); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onConfirmDeleteImportListExclusion = useCallback( + (id: number) => { + deleteImportListExclusion({ id }); + repopulate(); + }, + [deleteImportListExclusion, repopulate] + ); + + const selected = useSelector(createImportListExlucionsSelector()); + + const { + isFetching, + isPopulated, + items, + pageSize, + sortKey, + error, + sortDirection, + ...otherProps + } = selected; + + const [ + isAddImportListExclusionModalOpen, + setAddImportListExclusionModalOpen, + setAddImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const isFetchingForFirstTime = isFetching && !isPopulated; + + return ( +
+ + + + {items.map((item) => { + return ( + + ); + })} + + + + + + + + + + +
+ + + + +
+
+ ); +} + +export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js deleted file mode 100644 index 184788cec..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js +++ /dev/null @@ -1,59 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteImportListExclusion, fetchImportListExclusions } from 'Store/Actions/settingsActions'; -import ImportListExclusions from './ImportListExclusions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.importListExclusions, - (importListExclusions) => { - return { - ...importListExclusions - }; - } - ); -} - -const mapDispatchToProps = { - fetchImportListExclusions, - deleteImportListExclusion -}; - -class ImportListExclusionsConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchImportListExclusions(); - } - - // - // Listeners - - onConfirmDeleteImportListExclusion = (id) => { - this.props.deleteImportListExclusion({ id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -ImportListExclusionsConnector.propTypes = { - fetchImportListExclusions: PropTypes.func.isRequired, - deleteImportListExclusion: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ImportListExclusionsConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index de1d486b6..1ec50526e 100644 --- a/frontend/src/Settings/ImportLists/ImportListSettings.js +++ b/frontend/src/Settings/ImportLists/ImportListSettings.js @@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import { icons } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector'; +import ImportListsExclusions from './ImportListExclusions/ImportListExclusions'; import ImportListsConnector from './ImportLists/ImportListsConnector'; import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal'; import ImportListOptions from './Options/ImportListOptions'; @@ -113,7 +113,7 @@ class ImportListSettings extends Component { onChildStateChange={this.onChildStateChange} /> - + ; -type GetSectionState = AppState['settings'][Name]; -type GetSettingsSectionItemType = - GetSectionState extends AppSectionItemState - ? R - : GetSectionState extends AppSectionState - ? R +type SectionsWithItemNames = { + [K in keyof SettingsAppState]: SettingsAppState[K] extends AppSectionItemState + ? K : never; +}[keyof SettingsAppState]; -type AppStateWithPending = { - item?: GetSettingsSectionItemType; - pendingChanges?: Partial>; - saveError?: Error; -} & GetSectionState; +type GetSectionState = + SettingsAppState[Name]; +type GetSettingsSectionItemType = + GetSectionState extends AppSectionItemState ? R : never; -function createSettingsSectionSelector( - section: Name -) { +function createSettingsSectionSelector< + Name extends SectionsWithItemNames, + T extends GetSettingsSectionItemType +>(section: Name) { return createSelector( (state: AppState) => state.settings[section], (sectionSettings) => { - const { item, pendingChanges, saveError, ...other } = - sectionSettings as AppStateWithPending; + const { item, pendingChanges, ...other } = sectionSettings; - const { settings, ...rest } = selectSettings( - item, - pendingChanges, - saveError - ); + const saveError = + 'saveError' in sectionSettings ? sectionSettings.saveError : undefined; + + const { + settings, + pendingChanges: selectedPendingChanges, + ...rest + } = selectSettings(item, pendingChanges, saveError); return { ...other, saveError, - settings: settings as PendingSection>, + settings: settings as PendingSection, + pendingChanges: selectedPendingChanges as Partial, ...rest, }; } diff --git a/frontend/src/typings/ImportListExclusion.ts b/frontend/src/typings/ImportListExclusion.ts new file mode 100644 index 000000000..ec9add4dd --- /dev/null +++ b/frontend/src/typings/ImportListExclusion.ts @@ -0,0 +1,6 @@ +import ModelBase from 'App/ModelBase'; + +export default interface ImportListExclusion extends ModelBase { + tvdbId: number; + title: string; +} diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs index 09871fef3..2a9f0a9ec 100644 --- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; @@ -9,6 +10,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions { ImportListExclusion Add(ImportListExclusion importListExclusion); List All(); + PagingSpec Paged(PagingSpec pagingSpec); void Delete(int id); ImportListExclusion Get(int id); ImportListExclusion FindByTvdbId(int tvdbId); @@ -54,6 +56,11 @@ namespace NzbDrone.Core.ImportLists.Exclusions return _repo.All().ToList(); } + public PagingSpec Paged(PagingSpec pagingSpec) + { + return _repo.GetPaged(pagingSpec); + } + public void HandleAsync(SeriesDeletedEvent message) { if (!message.AddImportListExclusion) diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index 4a701347c..d9cd55c03 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.ImportLists.Exclusions; using Sonarr.Http; +using Sonarr.Http.Extensions; using Sonarr.Http.REST; using Sonarr.Http.REST.Attributes; @@ -29,11 +31,22 @@ namespace Sonarr.Api.V3.ImportLists [HttpGet] [Produces("application/json")] + [Obsolete("Deprecated")] public List GetImportListExclusions() { return _importListExclusionService.All().ToResource(); } + [HttpGet("paged")] + [Produces("application/json")] + public PagingResource GetImportListExclusionsPaged([FromQuery] PagingRequestResource paging) + { + var pagingResource = new PagingResource(paging); + var pageSpec = pagingResource.MapToPagingSpec(); + + return pageSpec.ApplyToPage(_importListExclusionService.Paged, ImportListExclusionResourceMapper.ToResource); + } + [RestPostById] [Consumes("application/json")] public ActionResult AddImportListExclusion(ImportListExclusionResource resource)