From e6e1078c1511f7e6262be3c782981fc6a36f4248 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Sep 2024 08:52:32 +0300 Subject: [PATCH] Convert Release Profiles to TypeScript --- frontend/src/App/State/SettingsAppState.ts | 8 + .../src/Components/Form/FormInputGroup.js | 4 + .../EditImportListExclusionModal.tsx | 6 +- .../EditImportListExclusionModalContent.tsx | 42 ++-- frontend/src/Settings/Profiles/Profiles.js | 4 +- .../Release/EditReleaseProfileModal.js | 27 --- .../Release/EditReleaseProfileModal.tsx | 41 ++++ .../EditReleaseProfileModalConnector.js | 39 ---- ....js => EditReleaseProfileModalContent.tsx} | 164 +++++++++----- ...EditReleaseProfileModalContentConnector.js | 112 ---------- .../Profiles/Release/ReleaseProfile.js | 206 ------------------ ...leaseProfile.css => ReleaseProfileRow.css} | 0 ...le.css.d.ts => ReleaseProfileRow.css.d.ts} | 0 .../Profiles/Release/ReleaseProfileRow.tsx | 132 +++++++++++ .../Profiles/Release/ReleaseProfiles.css | 2 +- .../Profiles/Release/ReleaseProfiles.js | 102 --------- .../Profiles/Release/ReleaseProfiles.tsx | 81 +++++++ .../Release/ReleaseProfilesConnector.js | 70 ------ .../src/typings/Settings/ReleaseProfile.ts | 12 + src/NzbDrone.Core/Localization/Core/en.json | 2 +- 20 files changed, 412 insertions(+), 642 deletions(-) delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js create mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js rename frontend/src/Settings/Profiles/Release/{EditReleaseProfileModalContent.js => EditReleaseProfileModalContent.tsx} (51%) delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfile.js rename frontend/src/Settings/Profiles/Release/{ReleaseProfile.css => ReleaseProfileRow.css} (100%) rename frontend/src/Settings/Profiles/Release/{ReleaseProfile.css.d.ts => ReleaseProfileRow.css.d.ts} (100%) create mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfiles.js create mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js create mode 100644 frontend/src/typings/Settings/ReleaseProfile.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index a3704d10e..cbf9d8de2 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -16,6 +16,7 @@ import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; import General from 'typings/Settings/General'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState @@ -49,6 +50,12 @@ export interface QualityProfilesAppState extends AppSectionState, AppSectionItemSchemaState {} +export interface ReleaseProfilesAppState + extends AppSectionState, + AppSectionSaveState { + pendingChanges: Partial; +} + export interface CustomFormatAppState extends AppSectionState, AppSectionDeleteState, @@ -83,6 +90,7 @@ interface SettingsAppState { languages: LanguageSettingsAppState; notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; + releaseProfiles: ReleaseProfilesAppState; ui: UiSettingsAppState; } diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 7a3191cdc..e3bccaf7c 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -272,6 +272,8 @@ FormInputGroup.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.any, values: PropTypes.arrayOf(PropTypes.any), + placeholder: PropTypes.string, + delimiters: PropTypes.arrayOf(PropTypes.string), isDisabled: PropTypes.bool, type: PropTypes.string.isRequired, kind: PropTypes.oneOf(kinds.all), @@ -284,8 +286,10 @@ FormInputGroup.propTypes = { helpTextWarning: PropTypes.string, helpLink: PropTypes.string, autoFocus: PropTypes.bool, + canEdit: PropTypes.bool, includeNoChange: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool, + includeAny: PropTypes.bool, selectedValueOptions: PropTypes.object, indexerFlags: PropTypes.number, pending: PropTypes.bool, diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx index b889a8105..7f5feafab 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx @@ -19,7 +19,7 @@ function EditImportListExclusionModal( const dispatch = useDispatch(); - const onModalClosePress = useCallback(() => { + const handleModalClose = useCallback(() => { dispatch( clearPendingChanges({ section: 'settings.importListExclusions', @@ -29,10 +29,10 @@ function EditImportListExclusionModal( }, [dispatch, onModalClose]); return ( - + ); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx index 8570d1acf..2fb7da1b7 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -31,12 +31,6 @@ const newImportListExclusion = { tvdbId: 0, }; -interface EditImportListExclusionModalContentProps { - id?: number; - onModalClose: () => void; - onDeleteImportListExclusionPress?: () => void; -} - function createImportListExclusionSelector(id?: number) { return createSelector( (state: AppState) => state.settings.importListExclusions, @@ -62,12 +56,24 @@ function createImportListExclusionSelector(id?: number) { ); } -function EditImportListExclusionModalContent( - props: EditImportListExclusionModalContentProps -) { - const { id, onModalClose, onDeleteImportListExclusionPress } = props; +interface EditImportListExclusionModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function EditImportListExclusionModalContent({ + id, + onModalClose, + onDeleteImportListExclusionPress, +}: EditImportListExclusionModalContentProps) { + const { isFetching, isSaving, item, error, saveError, ...otherProps } = + useSelector(createImportListExclusionSelector(id)); + + const { title, tvdbId } = item; const dispatch = useDispatch(); + const previousIsSaving = usePrevious(isSaving); const dispatchSetImportListExclusionValue = (payload: { name: string; @@ -77,20 +83,10 @@ function EditImportListExclusionModalContent( 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], - }); + Object.entries(newImportListExclusion).forEach(([name, value]) => { + dispatchSetImportListExclusionValue({ name, value }); }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -100,7 +96,7 @@ function EditImportListExclusionModalContent( if (previousIsSaving && !isSaving && !saveError) { onModalClose(); } - }); + }, [previousIsSaving, isSaving, saveError, onModalClose]); const onSavePress = useCallback(() => { dispatch(saveImportListExclusion({ id })); diff --git a/frontend/src/Settings/Profiles/Profiles.js b/frontend/src/Settings/Profiles/Profiles.js index 330591ed6..e54c6fdbd 100644 --- a/frontend/src/Settings/Profiles/Profiles.js +++ b/frontend/src/Settings/Profiles/Profiles.js @@ -7,7 +7,7 @@ import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; import DelayProfilesConnector from './Delay/DelayProfilesConnector'; import QualityProfilesConnector from './Quality/QualityProfilesConnector'; -import ReleaseProfilesConnector from './Release/ReleaseProfilesConnector'; +import ReleaseProfiles from './Release/ReleaseProfiles'; // Only a single DragDrop Context can exist so it's done here to allow editing // quality profiles and reordering delay profiles to work. @@ -26,7 +26,7 @@ class Profiles extends Component { - + diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js deleted file mode 100644 index a948ab123..000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.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 EditReleaseProfileModalContentConnector from './EditReleaseProfileModalContentConnector'; - -function EditReleaseProfileModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditReleaseProfileModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditReleaseProfileModal; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx new file mode 100644 index 000000000..cb7c2cef1 --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.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 EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; + +interface EditReleaseProfileModalProps { + id?: number; + isOpen: boolean; + onModalClose: () => void; + onDeleteReleaseProfilePress?: () => void; +} + +function EditReleaseProfileModal({ + isOpen, + onModalClose, + ...otherProps +}: EditReleaseProfileModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch( + clearPendingChanges({ + section: 'settings.releaseProfiles', + }) + ); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditReleaseProfileModal; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js deleted file mode 100644 index e846ff6ff..000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditReleaseProfileModal from './EditReleaseProfileModal'; - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditReleaseProfileModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'settings.releaseProfiles' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditReleaseProfileModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(null, mapDispatchToProps)(EditReleaseProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx similarity index 51% rename from frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js rename to frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index 99442839c..930064974 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -1,5 +1,7 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -10,33 +12,97 @@ 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 { + saveReleaseProfile, + setReleaseProfileValue, +} from 'Store/Actions/Settings/releaseProfiles'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { PendingSection } from 'typings/pending'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import translate from 'Utilities/String/translate'; import styles from './EditReleaseProfileModalContent.css'; const tagInputDelimiters = ['Tab', 'Enter']; -function EditReleaseProfileModalContent(props) { - const { - isSaving, - saveError, - item, - onInputChange, - onModalClose, - onSavePress, - onDeleteReleaseProfilePress, - ...otherProps - } = props; +const newReleaseProfile = { + enabled: true, + required: [], + ignored: [], + tags: [], + indexerId: 0, +}; - const { - id, - name, - enabled, - required, - ignored, - tags, - indexerId - } = item; +function createReleaseProfileSelector(id?: number) { + return createSelector( + (state: AppState) => state.settings.releaseProfiles, + (releaseProfiles) => { + const { items, isFetching, error, isSaving, saveError, pendingChanges } = + releaseProfiles; + + const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings as PendingSection, + ...settings, + }; + } + ); +} + +interface EditReleaseProfileModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteReleaseProfilePress?: () => void; +} + +function EditReleaseProfileModalContent({ + id, + onModalClose, + onDeleteReleaseProfilePress, +}: EditReleaseProfileModalContentProps) { + const { item, isFetching, isSaving, error, saveError, ...otherProps } = + useSelector(createReleaseProfileSelector(id)); + + const { name, enabled, required, ignored, tags, indexerId } = item; + + const dispatch = useDispatch(); + const previousIsSaving = usePrevious(isSaving); + + useEffect(() => { + if (!id) { + Object.entries(newReleaseProfile).forEach(([name, value]) => { + // @ts-expect-error 'setReleaseProfileValue' isn't typed yet + dispatch(setReleaseProfileValue({ name, value })); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (previousIsSaving && !isSaving && !saveError) { + onModalClose(); + } + }, [previousIsSaving, isSaving, saveError, onModalClose]); + + const handleSavePress = useCallback(() => { + dispatch(saveReleaseProfile({ id })); + }, [dispatch, id]); + + const handleInputChange = useCallback( + (payload: { name: string; value: string | number }) => { + // @ts-expect-error 'setReleaseProfileValue' isn't typed yet + dispatch(setReleaseProfileValue(payload)); + }, + [dispatch] + ); return ( @@ -46,7 +112,6 @@ function EditReleaseProfileModalContent(props) {
- {translate('Name')} @@ -56,7 +121,7 @@ function EditReleaseProfileModalContent(props) { {...name} placeholder={translate('OptionalName')} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -68,7 +133,7 @@ function EditReleaseProfileModalContent(props) { name="enabled" helpText={translate('EnableProfileHelpText')} {...enabled} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -85,7 +150,7 @@ function EditReleaseProfileModalContent(props) { placeholder={translate('AddNewRestriction')} delimiters={tagInputDelimiters} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -102,7 +167,7 @@ function EditReleaseProfileModalContent(props) { placeholder={translate('AddNewRestriction')} delimiters={tagInputDelimiters} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -113,10 +178,12 @@ function EditReleaseProfileModalContent(props) { type={inputTypes.INDEXER_SELECT} name="indexerId" helpText={translate('ReleaseProfileIndexerHelpText')} - helpTextWarning={translate('ReleaseProfileIndexerHelpTextWarning')} + helpTextWarning={translate( + 'ReleaseProfileIndexerHelpTextWarning' + )} {...indexerId} includeAny={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -128,33 +195,28 @@ function EditReleaseProfileModalContent(props) { name="tags" helpText={translate('ReleaseProfileTagSeriesHelpText')} {...tags} - onChange={onInputChange} + onChange={handleInputChange} />
- { - id && - - } + {id ? ( + + ) : null} - + {translate('Save')} @@ -163,14 +225,4 @@ function EditReleaseProfileModalContent(props) { ); } -EditReleaseProfileModalContent.propTypes = { - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onDeleteReleaseProfilePress: PropTypes.func -}; - export default EditReleaseProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js deleted file mode 100644 index 0371a1a7a..000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js +++ /dev/null @@ -1,112 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveReleaseProfile, setReleaseProfileValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; - -const newReleaseProfile = { - enabled: true, - required: [], - ignored: [], - tags: [], - indexerId: 0 -}; - -function createMapStateToProps() { - return createSelector( - (state, { id }) => id, - (state) => state.settings.releaseProfiles, - (id, releaseProfiles) => { - const { - isFetching, - error, - isSaving, - saveError, - pendingChanges, - items - } = releaseProfiles; - - const profile = id ? items.find((i) => i.id === id) : newReleaseProfile; - const settings = selectSettings(profile, pendingChanges, saveError); - - return { - id, - isFetching, - error, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -const mapDispatchToProps = { - setReleaseProfileValue, - saveReleaseProfile -}; - -class EditReleaseProfileModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.id) { - Object.keys(newReleaseProfile).forEach((name) => { - this.props.setReleaseProfileValue({ - name, - value: newReleaseProfile[name] - }); - }); - } - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setReleaseProfileValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveReleaseProfile({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditReleaseProfileModalContentConnector.propTypes = { - id: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setReleaseProfileValue: PropTypes.func.isRequired, - saveReleaseProfile: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditReleaseProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js b/frontend/src/Settings/Profiles/Release/ReleaseProfile.js deleted file mode 100644 index 7ec97bc80..000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js +++ /dev/null @@ -1,206 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import MiddleTruncate from 'react-middle-truncate'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TagList from 'Components/TagList'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector'; -import styles from './ReleaseProfile.css'; - -class ReleaseProfile extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditReleaseProfileModalOpen: false, - isDeleteReleaseProfileModalOpen: false - }; - } - - // - // Listeners - - onEditReleaseProfilePress = () => { - this.setState({ isEditReleaseProfileModalOpen: true }); - }; - - onEditReleaseProfileModalClose = () => { - this.setState({ isEditReleaseProfileModalOpen: false }); - }; - - onDeleteReleaseProfilePress = () => { - this.setState({ - isEditReleaseProfileModalOpen: false, - isDeleteReleaseProfileModalOpen: true - }); - }; - - onDeleteReleaseProfileModalClose = () => { - this.setState({ isDeleteReleaseProfileModalOpen: false }); - }; - - onConfirmDeleteReleaseProfile = () => { - this.props.onConfirmDeleteReleaseProfile(this.props.id); - }; - - // - // Render - - render() { - const { - id, - name, - enabled, - required, - ignored, - tags, - indexerId, - tagList, - indexerList - } = this.props; - - const { - isEditReleaseProfileModalOpen, - isDeleteReleaseProfileModalOpen - } = this.state; - - const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId); - - return ( - - { - name ? -
- {name} -
: - null - } - -
- { - required.map((item) => { - if (!item) { - return null; - } - - return ( - - ); - }) - } -
- -
- { - ignored.map((item) => { - if (!item) { - return null; - } - - return ( - - ); - }) - } -
- - - -
- { - !enabled && - - } - - { - indexer && - - } -
- - - - -
- ); - } -} - -ReleaseProfile.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string, - enabled: PropTypes.bool.isRequired, - required: PropTypes.arrayOf(PropTypes.string).isRequired, - ignored: PropTypes.arrayOf(PropTypes.string).isRequired, - tags: PropTypes.arrayOf(PropTypes.number).isRequired, - indexerId: PropTypes.number.isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - indexerList: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteReleaseProfile: PropTypes.func.isRequired -}; - -ReleaseProfile.defaultProps = { - enabled: true, - required: [], - ignored: [], - indexerId: 0 -}; - -export default ReleaseProfile; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.css b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfile.css rename to frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.css.d.ts b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfile.css.d.ts rename to frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx new file mode 100644 index 000000000..9ff1eb9aa --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx @@ -0,0 +1,132 @@ +import React, { useCallback } from 'react'; +// @ts-expect-error 'MiddleTruncate' isn't typed +import MiddleTruncate from 'react-middle-truncate'; +import { useDispatch } from 'react-redux'; +import { Tag } from 'App/State/TagsAppState'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagList from 'Components/TagList'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { kinds } from 'Helpers/Props'; +import { deleteReleaseProfile } from 'Store/Actions/Settings/releaseProfiles'; +import Indexer from 'typings/Indexer'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; +import translate from 'Utilities/String/translate'; +import EditReleaseProfileModal from './EditReleaseProfileModal'; +import styles from './ReleaseProfileRow.css'; + +interface ReleaseProfileProps extends ReleaseProfile { + tagList: Tag[]; + indexerList: Indexer[]; +} + +function ReleaseProfileRow(props: ReleaseProfileProps) { + const { + id, + name, + enabled = true, + required = [], + ignored = [], + tags, + indexerId = 0, + tagList, + indexerList, + } = props; + + const dispatch = useDispatch(); + + const [ + isEditReleaseProfileModalOpen, + setEditReleaseProfileModalOpen, + setEditReleaseProfileModalClosed, + ] = useModalOpenState(false); + + const [ + isDeleteReleaseProfileModalOpen, + setDeleteReleaseProfileModalOpen, + setDeleteReleaseProfileModalClosed, + ] = useModalOpenState(false); + + const handleDeletePress = useCallback(() => { + dispatch(deleteReleaseProfile({ id })); + }, [id, dispatch]); + + const indexer = + indexerId !== 0 && indexerList.find((i) => i.id === indexerId); + + return ( + + {name ?
{name}
: null} + +
+ {required.map((item) => { + if (!item) { + return null; + } + + return ( + + ); + })} +
+ +
+ {ignored.map((item) => { + if (!item) { + return null; + } + + return ( + + ); + })} +
+ + + +
+ {enabled ? null : ( + + )} + + {indexer ? ( + + ) : null} +
+ + + + +
+ ); +} + +export default ReleaseProfileRow; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css index 9e9715e77..43f17b9dc 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css @@ -4,7 +4,7 @@ } .addReleaseProfile { - composes: releaseProfile from '~./ReleaseProfile.css'; + composes: releaseProfile from '~./ReleaseProfileRow.css'; background-color: var(--cardAlternateBackgroundColor); color: var(--gray); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js deleted file mode 100644 index 51aa57b73..000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js +++ /dev/null @@ -1,102 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector'; -import ReleaseProfile from './ReleaseProfile'; -import styles from './ReleaseProfiles.css'; - -class ReleaseProfiles extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddReleaseProfileModalOpen: false - }; - } - - // - // Listeners - - onAddReleaseProfilePress = () => { - this.setState({ isAddReleaseProfileModalOpen: true }); - }; - - onAddReleaseProfileModalClose = () => { - this.setState({ isAddReleaseProfileModalOpen: false }); - }; - - // - // Render - - render() { - const { - items, - tagList, - indexerList, - onConfirmDeleteReleaseProfile, - ...otherProps - } = this.props; - - return ( -
- -
- -
- -
-
- - { - items.map((item) => { - return ( - - ); - }) - } -
- - -
-
- ); - } -} - -ReleaseProfiles.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - indexerList: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteReleaseProfile: PropTypes.func.isRequired -}; - -export default ReleaseProfiles; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx new file mode 100644 index 000000000..98300b1af --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import { ReleaseProfilesAppState } from 'App/State/SettingsAppState'; +import Card from 'Components/Card'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import { fetchIndexers } from 'Store/Actions/Settings/indexers'; +import { fetchReleaseProfiles } from 'Store/Actions/Settings/releaseProfiles'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import translate from 'Utilities/String/translate'; +import EditReleaseProfileModal from './EditReleaseProfileModal'; +import ReleaseProfileRow from './ReleaseProfileRow'; +import styles from './ReleaseProfiles.css'; + +function ReleaseProfiles() { + const { items, isFetching, isPopulated, error }: ReleaseProfilesAppState = + useSelector(createClientSideCollectionSelector('settings.releaseProfiles')); + + const tagList = useSelector(createTagsSelector()); + const indexerList = useSelector( + (state: AppState) => state.settings.indexers.items + ); + + const dispatch = useDispatch(); + + const [ + isAddReleaseProfileModalOpen, + setAddReleaseProfileModalOpen, + setAddReleaseProfileModalClosed, + ] = useModalOpenState(false); + + useEffect(() => { + dispatch(fetchReleaseProfiles()); + dispatch(fetchIndexers()); + }, [dispatch]); + + return ( +
+ +
+ +
+ +
+
+ + {items.map((item) => { + return ( + + ); + })} +
+ + +
+
+ ); +} + +export default ReleaseProfiles; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js b/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js deleted file mode 100644 index 0c0d81c77..000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import ReleaseProfiles from './ReleaseProfiles'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.releaseProfiles, - (state) => state.settings.indexers, - createTagsSelector(), - (releaseProfiles, indexers, tagList) => { - return { - ...releaseProfiles, - tagList, - isIndexersPopulated: indexers.isPopulated, - indexerList: indexers.items - }; - } - ); -} - -const mapDispatchToProps = { - fetchIndexers, - fetchReleaseProfiles, - deleteReleaseProfile -}; - -class ReleaseProfilesConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchReleaseProfiles(); - if (!this.props.isIndexersPopulated) { - this.props.fetchIndexers(); - } - } - - // - // Listeners - - onConfirmDeleteReleaseProfile = (id) => { - this.props.deleteReleaseProfile({ id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -ReleaseProfilesConnector.propTypes = { - isIndexersPopulated: PropTypes.bool.isRequired, - fetchReleaseProfiles: PropTypes.func.isRequired, - deleteReleaseProfile: PropTypes.func.isRequired, - fetchIndexers: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector); diff --git a/frontend/src/typings/Settings/ReleaseProfile.ts b/frontend/src/typings/Settings/ReleaseProfile.ts new file mode 100644 index 000000000..847e7d54e --- /dev/null +++ b/frontend/src/typings/Settings/ReleaseProfile.ts @@ -0,0 +1,12 @@ +import ModelBase from 'App/ModelBase'; + +interface ReleaseProfile extends ModelBase { + name: string; + enabled: boolean; + required: string[]; + ignored: string[]; + indexerId: number; + tags: number[]; +} + +export default ReleaseProfile; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 364ffe5c2..abf08cbc3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -361,7 +361,7 @@ "DeleteQualityProfile": "Delete Quality Profile", "DeleteQualityProfileMessageText": "Are you sure you want to delete the quality profile '{name}'?", "DeleteReleaseProfile": "Delete Release Profile", - "DeleteReleaseProfileMessageText": "Are you sure you want to delete this release profile '{name}'?", + "DeleteReleaseProfileMessageText": "Are you sure you want to delete the release profile '{name}'?", "DeleteRemotePathMapping": "Delete Remote Path Mapping", "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRootFolder": "Delete Root Folder",