diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 4ea3a95a1d..a970afb8be 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -7,7 +7,9 @@ import AppSectionState, { PagedAppSectionState, } from 'App/State/AppSectionState'; import Language from 'Language/Language'; +import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging'; import CustomFormat from 'typings/CustomFormat'; +import DelayProfile from 'typings/DelayProfile'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; import ImportListExclusion from 'typings/ImportListExclusion'; @@ -29,6 +31,22 @@ type Presets = T & { presets: T[]; }; +export interface AutoTaggingAppState + extends AppSectionState, + AppSectionDeleteState, + AppSectionSaveState {} + +export interface AutoTaggingSpecificationAppState + extends AppSectionState, + AppSectionDeleteState, + AppSectionSaveState, + AppSectionSchemaState {} + +export interface DelayProfileAppState + extends AppSectionState, + AppSectionDeleteState, + AppSectionSaveState {} + export interface DownloadClientAppState extends AppSectionState, AppSectionDeleteState, @@ -107,7 +125,10 @@ export type UiSettingsAppState = AppSectionItemState; interface SettingsAppState { advancedSettings: boolean; + autoTaggings: AutoTaggingAppState; + autoTaggingSpecifications: AutoTaggingSpecificationAppState; customFormats: CustomFormatAppState; + delayProfiles: DelayProfileAppState; downloadClients: DownloadClientAppState; general: GeneralAppState; importListExclusions: ImportListExclusionsSettingsAppState; diff --git a/frontend/src/App/State/TagsAppState.ts b/frontend/src/App/State/TagsAppState.ts index f22873606b..9b66303316 100644 --- a/frontend/src/App/State/TagsAppState.ts +++ b/frontend/src/App/State/TagsAppState.ts @@ -17,7 +17,7 @@ export interface TagDetail extends ModelBase { indexerIds: number[]; movieIds: number[]; notificationIds: number[]; - restrictionIds: number[]; + releaseProfileIds: number[]; } export interface TagDetailAppState diff --git a/frontend/src/Components/Page/PageSectionContent.tsx b/frontend/src/Components/Page/PageSectionContent.tsx index 808de3e38e..f3470fe7e3 100644 --- a/frontend/src/Components/Page/PageSectionContent.tsx +++ b/frontend/src/Components/Page/PageSectionContent.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Error } from 'App/State/AppSectionState'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import { kinds } from 'Helpers/Props'; @@ -6,7 +7,7 @@ import { kinds } from 'Helpers/Props'; interface PageSectionContentProps { isFetching: boolean; isPopulated: boolean; - error?: object; + error?: Error; errorMessage: string; children: React.ReactNode; } @@ -18,7 +19,7 @@ function PageSectionContent({ errorMessage, children, }: PageSectionContentProps) { - if (isFetching) { + if (isFetching && !isPopulated) { return ; } diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js b/frontend/src/Settings/Tags/AutoTagging/AutoTagging.tsx similarity index 63% rename from frontend/src/Settings/Tags/AutoTagging/AutoTagging.js rename to frontend/src/Settings/Tags/AutoTagging/AutoTagging.tsx index 760273cb3c..cd484a33f9 100644 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTagging.js +++ b/frontend/src/Settings/Tags/AutoTagging/AutoTagging.tsx @@ -1,27 +1,38 @@ -import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; +import { Tag } from 'App/State/TagsAppState'; import Card from 'Components/Card'; import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TagList from 'Components/TagList'; import { icons, kinds } from 'Helpers/Props'; +import { Kind } from 'Helpers/Props/kinds'; +import { AutoTaggingSpecification } from 'typings/AutoTagging'; import translate from 'Utilities/String/translate'; import EditAutoTaggingModal from './EditAutoTaggingModal'; import styles from './AutoTagging.css'; -export default function AutoTagging(props) { - const { - id, - name, - tags, - tagList, - specifications, - isDeleting, - onConfirmDeleteAutoTagging, - onCloneAutoTaggingPress - } = props; +interface AutoTaggingProps { + id: number; + name: string; + specifications: AutoTaggingSpecification[]; + tags: number[]; + tagList: Tag[]; + isDeleting: boolean; + onConfirmDeleteAutoTagging: (id: number) => void; + onCloneAutoTaggingPress: (id: number) => void; +} +export default function AutoTagging({ + id, + name, + tags, + tagList, + specifications, + isDeleting, + onConfirmDeleteAutoTagging, + onCloneAutoTaggingPress, +}: AutoTaggingProps) { const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -57,9 +68,7 @@ export default function AutoTagging(props) { onPress={onEditPress} >
-
- {name} -
+
{name}
- +
- { - specifications.map((item, index) => { - if (!item) { - return null; - } + {specifications.map((item, index) => { + if (!item) { + return null; + } - let kind = kinds.DEFAULT; - if (item.required) { - kind = kinds.SUCCESS; - } - if (item.negate) { - kind = kinds.DANGER; - } + let kind: Kind = 'default'; - return ( - - ); - }) - } + if (item.required) { + kind = 'success'; + } + if (item.negate) { + kind = 'danger'; + } + + return ( + + ); + })}
); } - -AutoTagging.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - specifications: PropTypes.arrayOf(PropTypes.object).isRequired, - tags: PropTypes.arrayOf(PropTypes.number).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - isDeleting: PropTypes.bool.isRequired, - onConfirmDeleteAutoTagging: PropTypes.func.isRequired, - onCloneAutoTaggingPress: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.tsx similarity index 60% rename from frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js rename to frontend/src/Settings/Tags/AutoTagging/AutoTaggings.tsx index f27dc3b5a2..01685af9c3 100644 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js +++ b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.tsx @@ -1,14 +1,20 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { AutoTaggingAppState } 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 { icons } from 'Helpers/Props'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; -import { cloneAutoTagging, deleteAutoTagging, fetchAutoTaggings } from 'Store/Actions/settingsActions'; +import { + cloneAutoTagging, + deleteAutoTagging, + fetchAutoTaggings, +} from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import AutoTaggingModel from 'typings/AutoTagging'; import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import AutoTagging from './AutoTagging'; @@ -16,27 +22,27 @@ import EditAutoTaggingModal from './EditAutoTaggingModal'; import styles from './AutoTaggings.css'; export default function AutoTaggings() { - const { - error, - items, - isDeleting, - isFetching, - isPopulated - } = useSelector( - createSortedSectionSelector('settings.autoTaggings', sortByProp('name')) + const { error, items, isDeleting, isFetching, isPopulated } = useSelector( + createSortedSectionSelector( + 'settings.autoTaggings', + sortByProp('name') + ) ); const tagList = useSelector(createTagsSelector()); const dispatch = useDispatch(); const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [tagsFromId, setTagsFromId] = useState(undefined); + const [tagsFromId, setTagsFromId] = useState(); - const onClonePress = useCallback((id) => { - dispatch(cloneAutoTagging({ id })); + const onClonePress = useCallback( + (id: number) => { + dispatch(cloneAutoTagging({ id })); - setTagsFromId(id); - setIsEditModalOpen(true); - }, [dispatch, setIsEditModalOpen]); + setTagsFromId(id); + setIsEditModalOpen(true); + }, + [dispatch, setIsEditModalOpen] + ); const onEditPress = useCallback(() => { setIsEditModalOpen(true); @@ -46,9 +52,12 @@ export default function AutoTaggings() { setIsEditModalOpen(false); }, [setIsEditModalOpen]); - const onConfirmDelete = useCallback((id) => { - dispatch(deleteAutoTagging({ id })); - }, [dispatch]); + const onConfirmDelete = useCallback( + (id: number) => { + dispatch(deleteAutoTagging({ id })); + }, + [dispatch] + ); useEffect(() => { dispatch(fetchAutoTaggings()); @@ -64,30 +73,22 @@ export default function AutoTaggings() { isPopulated={isPopulated} >
- { - items.map((item) => { - return ( - - ); - }) - } - - -
- { + return ( + + ); + })} + + +
+
@@ -97,7 +98,6 @@ export default function AutoTaggings() { tagsFromId={tagsFromId} onModalClose={onEditModalClose} /> - ); diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.js b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.js deleted file mode 100644 index c6f810785c..0000000000 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.js +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useState } 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 EditAutoTaggingModalContent from './EditAutoTaggingModalContent'; - -export default function EditAutoTaggingModal(props) { - const { - isOpen, - onModalClose: onOriginalModalClose, - ...otherProps - } = props; - - const dispatch = useDispatch(); - const [height, setHeight] = useState('auto'); - - const onContentHeightChange = useCallback((h) => { - if (height === 'auto' || h > height) { - setHeight(h); - } - }, [height, setHeight]); - - const onModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section: 'settings.autoTaggings' })); - onOriginalModalClose(); - }, [dispatch, onOriginalModalClose]); - - return ( - - - - ); -} - -EditAutoTaggingModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.tsx b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.tsx new file mode 100644 index 0000000000..96bd117d64 --- /dev/null +++ b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModal.tsx @@ -0,0 +1,35 @@ +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 EditAutoTaggingModalContent, { + EditAutoTaggingModalContentProps, +} from './EditAutoTaggingModalContent'; + +interface EditAutoTaggingModalProps extends EditAutoTaggingModalContentProps { + isOpen: boolean; + onModalClose: () => void; +} + +export default function EditAutoTaggingModal({ + isOpen, + onModalClose, + ...otherProps +}: EditAutoTaggingModalProps) { + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch(clearPendingChanges({ section: 'settings.autoTaggings' })); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js deleted file mode 100644 index 811c98461d..0000000000 --- a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.js +++ /dev/null @@ -1,270 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Alert from 'Components/Alert'; -import Card from 'Components/Card'; -import FieldSet from 'Components/FieldSet'; -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 Icon from 'Components/Icon'; -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 { icons, inputTypes, kinds } from 'Helpers/Props'; -import { - cloneAutoTaggingSpecification, - deleteAutoTaggingSpecification, - fetchAutoTaggingSpecifications, - saveAutoTagging, - setAutoTaggingValue -} from 'Store/Actions/settingsActions'; -import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector'; -import translate from 'Utilities/String/translate'; -import AddSpecificationModal from './Specifications/AddSpecificationModal'; -import EditSpecificationModal from './Specifications/EditSpecificationModal'; -import Specification from './Specifications/Specification'; -import styles from './EditAutoTaggingModalContent.css'; - -export default function EditAutoTaggingModalContent(props) { - const { - id, - tagsFromId, - onModalClose, - onDeleteAutoTaggingPress - } = props; - - const { - error, - item, - isFetching, - isSaving, - saveError, - validationErrors, - validationWarnings - } = useSelector(createProviderSettingsSelectorHook('autoTaggings', id)); - - const { - isPopulated: specificationsPopulated, - items: specifications - } = useSelector((state) => state.settings.autoTaggingSpecifications); - - const dispatch = useDispatch(); - const [isAddSpecificationModalOpen, setIsAddSpecificationModalOpen] = useState(false); - const [isEditSpecificationModalOpen, setIsEditSpecificationModalOpen] = useState(false); - // const [isImportAutoTaggingModalOpen, setIsImportAutoTaggingModalOpen] = useState(false); - - const onAddSpecificationPress = useCallback(() => { - setIsAddSpecificationModalOpen(true); - }, [setIsAddSpecificationModalOpen]); - - const onAddSpecificationModalClose = useCallback(({ specificationSelected = false } = {}) => { - setIsAddSpecificationModalOpen(false); - setIsEditSpecificationModalOpen(specificationSelected); - }, [setIsAddSpecificationModalOpen]); - - const onEditSpecificationModalClose = useCallback(() => { - setIsEditSpecificationModalOpen(false); - }, [setIsEditSpecificationModalOpen]); - - const onInputChange = useCallback(({ name, value }) => { - dispatch(setAutoTaggingValue({ name, value })); - }, [dispatch]); - - const onSavePress = useCallback(() => { - dispatch(saveAutoTagging({ id })); - }, [dispatch, id]); - - const onCloneSpecificationPress = useCallback((specId) => { - dispatch(cloneAutoTaggingSpecification({ id: specId })); - }, [dispatch]); - - const onConfirmDeleteSpecification = useCallback((specId) => { - dispatch(deleteAutoTaggingSpecification({ id: specId })); - }, [dispatch]); - - useEffect(() => { - dispatch(fetchAutoTaggingSpecifications({ id: tagsFromId || id })); - }, [id, tagsFromId, dispatch]); - - const isSavingRef = useRef(); - - useEffect(() => { - if (isSavingRef.current && !isSaving && !saveError) { - onModalClose(); - } - - isSavingRef.current = isSaving; - }, [isSaving, saveError, onModalClose]); - - const { - name, - removeTagsAutomatically, - tags - } = item; - - return ( - - - - {id ? translate('EditAutoTag') : translate('AddAutoTag')} - - - -
- { - isFetching ? : null - } - - { - !isFetching && !!error ? - - {translate('AddAutoTagError')} - : - null - } - - { - !isFetching && !error && specificationsPopulated ? -
-
- - - {translate('Name')} - - - - - - - {translate('RemoveTagsAutomatically')} - - - - - - {translate('Tags')} - - - -
- -
-
- { - specifications.map((tag) => { - return ( - - ); - }) - } - - -
- -
-
-
-
- - - - - - {/* */} - -
: - null - } -
-
- -
- { - id ? - : - null - } - - {/* */} -
- - - - - {translate('Save')} - -
-
- ); -} - -EditAutoTaggingModalContent.propTypes = { - id: PropTypes.number, - tagsFromId: PropTypes.number, - onModalClose: PropTypes.func.isRequired, - onDeleteAutoTaggingPress: PropTypes.func -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.tsx b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.tsx new file mode 100644 index 0000000000..6317a5b617 --- /dev/null +++ b/frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.tsx @@ -0,0 +1,258 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import Card from 'Components/Card'; +import FieldSet from 'Components/FieldSet'; +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 Icon from 'Components/Icon'; +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 { icons, inputTypes, kinds } from 'Helpers/Props'; +import { + cloneAutoTaggingSpecification, + deleteAutoTaggingSpecification, + fetchAutoTaggingSpecifications, + saveAutoTagging, + setAutoTaggingValue, +} from 'Store/Actions/settingsActions'; +import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import AddSpecificationModal from './Specifications/AddSpecificationModal'; +import EditSpecificationModal from './Specifications/EditSpecificationModal'; +import Specification from './Specifications/Specification'; +import styles from './EditAutoTaggingModalContent.css'; + +export interface EditAutoTaggingModalContentProps { + id?: number; + tagsFromId?: number; + onModalClose: () => void; + onDeleteAutoTaggingPress?: () => void; +} + +export default function EditAutoTaggingModalContent({ + id, + tagsFromId, + onModalClose, + onDeleteAutoTaggingPress, +}: EditAutoTaggingModalContentProps) { + const { + error, + item, + isFetching, + isSaving, + saveError, + validationErrors, + validationWarnings, + } = useSelector(createProviderSettingsSelectorHook('autoTaggings', id)); + + const { isPopulated: specificationsPopulated, items: specifications } = + useSelector((state: AppState) => state.settings.autoTaggingSpecifications); + + const dispatch = useDispatch(); + const [isAddSpecificationModalOpen, setIsAddSpecificationModalOpen] = + useState(false); + const [isEditSpecificationModalOpen, setIsEditSpecificationModalOpen] = + useState(false); + + const handleAddSpecificationPress = useCallback(() => { + setIsAddSpecificationModalOpen(true); + }, [setIsAddSpecificationModalOpen]); + + const handleAddSpecificationModalClose = useCallback( + ({ specificationSelected = false } = {}) => { + setIsAddSpecificationModalOpen(false); + setIsEditSpecificationModalOpen(specificationSelected); + }, + [setIsAddSpecificationModalOpen] + ); + + const handleEditSpecificationModalClose = useCallback(() => { + setIsEditSpecificationModalOpen(false); + }, [setIsEditSpecificationModalOpen]); + + const handleInputChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error - actions are not typed + dispatch(setAutoTaggingValue({ name, value })); + }, + [dispatch] + ); + + const handleSavePress = useCallback(() => { + dispatch(saveAutoTagging({ id })); + }, [dispatch, id]); + + const handleCloneSpecificationPress = useCallback( + (specId: number) => { + dispatch(cloneAutoTaggingSpecification({ id: specId })); + }, + [dispatch] + ); + + const handleConfirmDeleteSpecification = useCallback( + (specId: number) => { + dispatch(deleteAutoTaggingSpecification({ id: specId })); + }, + [dispatch] + ); + + useEffect(() => { + dispatch(fetchAutoTaggingSpecifications({ id: tagsFromId || id })); + }, [id, tagsFromId, dispatch]); + + const isSavingRef = useRef(false); + + useEffect(() => { + if (isSavingRef.current && !isSaving && !saveError) { + onModalClose(); + } + + isSavingRef.current = isSaving; + }, [isSaving, saveError, onModalClose]); + + const { name, removeTagsAutomatically, tags } = item; + + return ( + + + {id ? translate('EditAutoTag') : translate('AddAutoTag')} + + + +
+ {isFetching ? : null} + + {!isFetching && !!error ? ( + {translate('AddAutoTagError')} + ) : null} + + {!isFetching && !error && specificationsPopulated ? ( +
+
+ + {translate('Name')} + + + + + + {translate('RemoveTagsAutomatically')} + + + + + + {translate('Tags')} + + + +
+ +
+
+ {specifications.map((specification) => { + return ( + + ); + })} + + +
+ +
+
+
+
+ + + + + + {/* */} +
+ ) : null} +
+
+ +
+ {id ? ( + + ) : null} + + {/* */} +
+ + + + + {translate('Save')} + +
+
+ ); +} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.js deleted file mode 100644 index f6f2b134e6..0000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback } from 'react'; -import Button from 'Components/Link/Button'; -import Link from 'Components/Link/Link'; -import Menu from 'Components/Menu/Menu'; -import MenuContent from 'Components/Menu/MenuContent'; -import { sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import AddSpecificationPresetMenuItem from './AddSpecificationPresetMenuItem'; -import styles from './AddSpecificationItem.css'; - -export default function AddSpecificationItem(props) { - const { - implementation, - implementationName, - infoLink, - presets, - onSpecificationSelect - } = props; - - const onWrappedSpecificationSelect = useCallback(() => { - onSpecificationSelect({ implementation }); - }, [implementation, onSpecificationSelect]); - - const hasPresets = !!presets && !!presets.length; - - return ( -
- - -
-
- {implementationName} -
- -
- { - hasPresets ? - - - - - - - - { - presets.map((preset, index) => { - return ( - - ); - }) - } - - - : - null - } - - { - infoLink ? - : - null - } -
-
-
- ); -} - -AddSpecificationItem.propTypes = { - implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, - infoLink: PropTypes.string, - presets: PropTypes.arrayOf(PropTypes.object), - onSpecificationSelect: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.tsx b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.tsx new file mode 100644 index 0000000000..774f367e95 --- /dev/null +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.tsx @@ -0,0 +1,81 @@ +import React, { useCallback } from 'react'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import { sizes } from 'Helpers/Props'; +import { AutoTaggingSpecification } from 'typings/AutoTagging'; +import translate from 'Utilities/String/translate'; +import AddSpecificationPresetMenuItem from './AddSpecificationPresetMenuItem'; +import styles from './AddSpecificationItem.css'; + +interface AddSpecificationItemProps { + implementation: string; + implementationName: string; + infoLink?: string; + presets?: AutoTaggingSpecification[]; + onSpecificationSelect: ({ + implementation, + }: { + implementation: string; + }) => void; +} + +export default function AddSpecificationItem({ + implementation, + implementationName, + infoLink, + presets, + onSpecificationSelect, +}: AddSpecificationItemProps) { + const handleSpecificationSelect = useCallback(() => { + onSpecificationSelect({ implementation }); + }, [implementation, onSpecificationSelect]); + + const hasPresets = !!presets && !!presets.length; + + return ( +
+ + +
+
{implementationName}
+ +
+ {hasPresets ? ( + + + + + + + + {presets.map((preset, index) => { + return ( + + ); + })} + + + + ) : null} + + {infoLink ? ( + + ) : null} +
+
+
+ ); +} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.js deleted file mode 100644 index 1a8c115f01..0000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.js +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import AddSpecificationModalContent from './AddSpecificationModalContent'; - -function AddSpecificationModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -AddSpecificationModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AddSpecificationModal; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.tsx b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.tsx new file mode 100644 index 0000000000..e564866252 --- /dev/null +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModal.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddSpecificationModalContent from './AddSpecificationModalContent'; + +interface AddSpecificationModalProps { + isOpen: boolean; + onModalClose: (options?: { specificationSelected: boolean }) => void; +} + +function AddSpecificationModal({ + isOpen, + onModalClose, +}: AddSpecificationModalProps) { + return ( + + + + ); +} + +export default AddSpecificationModal; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js deleted file mode 100644 index 9e06e815be..0000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import Alert from 'Components/Alert'; -import Button from 'Components/Link/Button'; -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 { kinds } from 'Helpers/Props'; -import { - fetchAutoTaggingSpecificationSchema, - selectAutoTaggingSpecificationSchema -} from 'Store/Actions/settingsActions'; -import translate from 'Utilities/String/translate'; -import AddSpecificationItem from './AddSpecificationItem'; -import styles from './AddSpecificationModalContent.css'; - -export default function AddSpecificationModalContent(props) { - const { - onModalClose - } = props; - - const { - isSchemaFetching, - isSchemaPopulated, - schemaError, - schema - } = useSelector( - (state) => state.settings.autoTaggingSpecifications - ); - - const dispatch = useDispatch(); - - const onSpecificationSelect = useCallback(({ implementation, name }) => { - dispatch(selectAutoTaggingSpecificationSchema({ implementation, presetName: name })); - onModalClose({ specificationSelected: true }); - }, [dispatch, onModalClose]); - - useEffect(() => { - dispatch(fetchAutoTaggingSpecificationSchema()); - }, [dispatch]); - - return ( - - - {translate('AddCondition')} - - - - { - isSchemaFetching ? : null - } - - { - !isSchemaFetching && !!schemaError ? - - {translate('AddConditionError')} - : - null - } - - { - isSchemaPopulated && !schemaError ? -
- - -
- {translate('SupportedAutoTaggingProperties')} -
-
- -
- { - schema.map((specification) => { - return ( - - ); - }) - } -
- -
: - null - } -
- - - - -
- ); -} - -AddSpecificationModalContent.propTypes = { - onModalClose: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.tsx b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.tsx new file mode 100644 index 0000000000..12b8373ffb --- /dev/null +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.tsx @@ -0,0 +1,90 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +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 { kinds } from 'Helpers/Props'; +import { + fetchAutoTaggingSpecificationSchema, + selectAutoTaggingSpecificationSchema, +} from 'Store/Actions/settingsActions'; +import translate from 'Utilities/String/translate'; +import AddSpecificationItem from './AddSpecificationItem'; +import styles from './AddSpecificationModalContent.css'; + +interface AddSpecificationModalContentProps { + onModalClose: (options?: { specificationSelected: boolean }) => void; +} + +export default function AddSpecificationModalContent({ + onModalClose, +}: AddSpecificationModalContentProps) { + const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = + useSelector((state: AppState) => state.settings.autoTaggingSpecifications); + + const dispatch = useDispatch(); + + const onSpecificationSelect = useCallback( + ({ implementation }: { implementation: string }) => { + dispatch( + selectAutoTaggingSpecificationSchema({ + implementation, + presetName: name, + }) + ); + onModalClose({ specificationSelected: true }); + }, + [dispatch, onModalClose] + ); + + const handleModalClose = useCallback(() => { + onModalClose(); + }, [onModalClose]); + + useEffect(() => { + dispatch(fetchAutoTaggingSpecificationSchema()); + }, [dispatch]); + + return ( + + {translate('AddCondition')} + + + {isSchemaFetching ? : null} + + {!isSchemaFetching && !!schemaError ? ( + {translate('AddConditionError')} + ) : null} + + {isSchemaPopulated && !schemaError ? ( +
+ +
{translate('SupportedAutoTaggingProperties')}
+
+ +
+ {schema.map((specification) => { + return ( + + ); + })} +
+
+ ) : null} +
+ + + + +
+ ); +} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.js deleted file mode 100644 index b043ddf060..0000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback } from 'react'; -import MenuItem from 'Components/Menu/MenuItem'; - -export default function AddSpecificationPresetMenuItem(props) { - const { - name, - implementation, - onPress, - ...otherProps - } = props; - - const onWrappedPress = useCallback(() => { - onPress({ - name, - implementation - }); - }, [name, implementation, onPress]); - - return ( - - {name} - - ); -} - -AddSpecificationPresetMenuItem.propTypes = { - name: PropTypes.string.isRequired, - implementation: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.tsx b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.tsx new file mode 100644 index 0000000000..06cd3b0271 --- /dev/null +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationPresetMenuItem.tsx @@ -0,0 +1,34 @@ +import React, { useCallback } from 'react'; +import MenuItem from 'Components/Menu/MenuItem'; + +interface AddSpecificationPresetMenuItemProps { + name: string; + implementation: string; + onPress: ({ + name, + implementation, + }: { + name: string; + implementation: string; + }) => void; +} + +export default function AddSpecificationPresetMenuItem({ + name, + implementation, + onPress, + ...otherProps +}: AddSpecificationPresetMenuItemProps) { + const handlePress = useCallback(() => { + onPress({ + name, + implementation, + }); + }, [name, implementation, onPress]); + + return ( + + {name} + + ); +} diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.tsx similarity index 50% rename from frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.js rename to frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.tsx index 16ed4daec8..5b4041e623 100644 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.js +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModal.tsx @@ -1,25 +1,34 @@ -import PropTypes from 'prop-types'; 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 EditSpecificationModalContent from './EditSpecificationModalContent'; +import EditSpecificationModalContent, { + EditSpecificationModalContentProps, +} from './EditSpecificationModalContent'; -function EditSpecificationModal({ isOpen, onModalClose, ...otherProps }) { +interface EditSpecificationModalProps + extends EditSpecificationModalContentProps { + isOpen: boolean; + onModalClose: () => void; +} + +function EditSpecificationModal({ + isOpen, + onModalClose, + ...otherProps +}: EditSpecificationModalProps) { const dispatch = useDispatch(); const onWrappedModalClose = useCallback(() => { - dispatch(clearPendingChanges({ section: 'settings.autoTaggingSpecifications' })); + dispatch( + clearPendingChanges({ section: 'settings.autoTaggingSpecifications' }) + ); onModalClose(); }, [onModalClose, dispatch]); return ( - + state.settings.advancedSettings); - - const { - item, - ...otherFormProps - } = useSelector( - createProviderSettingsSelectorHook('autoTaggingSpecifications', id) - ); - - const dispatch = useDispatch(); - - const onInputChange = useCallback(({ name, value }) => { - dispatch(setAutoTaggingSpecificationValue({ name, value })); - }, [dispatch]); - - const onFieldChange = useCallback(({ name, value }) => { - dispatch(setAutoTaggingSpecificationFieldValue({ name, value })); - }, [dispatch]); - - const onCancelPress = useCallback(({ name, value }) => { - dispatch(clearAutoTaggingSpecificationPending()); - onModalClose(); - }, [dispatch, onModalClose]); - - const onSavePress = useCallback(({ name, value }) => { - dispatch(saveAutoTaggingSpecification({ id })); - onModalClose(); - }, [dispatch, id, onModalClose]); - - const { - implementationName, - name, - negate, - required, - fields - } = item; - - return ( - - - {id ? translate('EditConditionImplementation', { implementationName }) : translate('AddConditionImplementation', { implementationName })} - - - -
- { - fields && fields.some((x) => x.label === 'Regular Expression') && - -
- -
-
- -
-
- -
-
- } - - - - {translate('Name')} - - - - - - { - fields && fields.map((field) => { - return ( - - ); - }) - } - - - - {translate('Negate')} - - - - - - - - {translate('Required')} - - - - - -
- - { - id ? - : - null - } - - - - - {translate('Save')} - - -
- ); -} - -EditSpecificationModalContent.propTypes = { - id: PropTypes.number, - onDeleteSpecificationPress: PropTypes.func, - onModalClose: PropTypes.func.isRequired -}; - -export default EditSpecificationModalContent; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.tsx b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.tsx new file mode 100644 index 0000000000..da316d0e29 --- /dev/null +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.tsx @@ -0,0 +1,192 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import { AutoTaggingSpecificationAppState } from 'App/State/SettingsAppState'; +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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +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 { + clearAutoTaggingSpecificationPending, + saveAutoTaggingSpecification, + setAutoTaggingSpecificationFieldValue, + setAutoTaggingSpecificationValue, +} from 'Store/Actions/settingsActions'; +import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector'; +import { AutoTaggingSpecification } from 'typings/AutoTagging'; +import { InputChanged } from 'typings/inputs'; +import translate from 'Utilities/String/translate'; +import styles from './EditSpecificationModalContent.css'; + +export interface EditSpecificationModalContentProps { + id?: number; + onDeleteSpecificationPress?: () => void; + onModalClose: () => void; +} + +function EditSpecificationModalContent({ + id, + onDeleteSpecificationPress, + onModalClose, +}: EditSpecificationModalContentProps) { + const advancedSettings = useSelector( + (state: AppState) => state.settings.advancedSettings + ); + + const { item, ...otherFormProps } = useSelector( + createProviderSettingsSelectorHook< + AutoTaggingSpecification, + AutoTaggingSpecificationAppState + >('autoTaggingSpecifications', id) + ); + + const dispatch = useDispatch(); + + const onInputChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error - actions are not typed + dispatch(setAutoTaggingSpecificationValue({ name, value })); + }, + [dispatch] + ); + + const onFieldChange = useCallback( + ({ name, value }: InputChanged) => { + // @ts-expect-error - actions are not typed + dispatch(setAutoTaggingSpecificationFieldValue({ name, value })); + }, + [dispatch] + ); + + const onCancelPress = useCallback(() => { + dispatch(clearAutoTaggingSpecificationPending()); + onModalClose(); + }, [dispatch, onModalClose]); + + const onSavePress = useCallback(() => { + dispatch(saveAutoTaggingSpecification({ id })); + onModalClose(); + }, [dispatch, id, onModalClose]); + + const { implementationName, name, negate, required, fields } = item; + + return ( + + + {id + ? translate('EditConditionImplementation', { implementationName }) + : translate('AddConditionImplementation', { implementationName })} + + + +
+ {fields && fields.some((x) => x.label === 'Regular Expression') && ( + +
+ +
+
+ +
+
+ +
+
+ )} + + + {translate('Name')} + + + + + {fields && + fields.map((field) => { + return ( + + ); + })} + + + {translate('Negate')} + + + + + + {translate('Required')} + + + + +
+ + {id ? ( + + ) : null} + + + + + {translate('Save')} + + +
+ ); +} + +export default EditSpecificationModalContent; diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContentConnector.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContentConnector.js deleted file mode 100644 index 8f27b74e0f..0000000000 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContentConnector.js +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearAutoTaggingSpecificationPending, saveAutoTaggingSpecification, setAutoTaggingSpecificationFieldValue, setAutoTaggingSpecificationValue } from 'Store/Actions/settingsActions'; -import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; -import EditSpecificationModalContent from './EditSpecificationModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - createProviderSettingsSelector('autoTaggingSpecifications'), - (advancedSettings, specification) => { - return { - advancedSettings, - ...specification - }; - } - ); -} - -const mapDispatchToProps = { - setAutoTaggingSpecificationValue, - setAutoTaggingSpecificationFieldValue, - saveAutoTaggingSpecification, - clearAutoTaggingSpecificationPending -}; - -class EditSpecificationModalContentConnector extends Component { - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setAutoTaggingSpecificationValue({ name, value }); - }; - - onFieldChange = ({ name, value }) => { - this.props.setAutoTaggingSpecificationFieldValue({ name, value }); - }; - - onCancelPress = () => { - this.props.clearAutoTaggingSpecificationPending(); - this.props.onModalClose(); - }; - - onSavePress = () => { - this.props.saveAutoTaggingSpecification({ id: this.props.id }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditSpecificationModalContentConnector.propTypes = { - id: PropTypes.number, - item: PropTypes.object.isRequired, - setAutoTaggingSpecificationValue: PropTypes.func.isRequired, - setAutoTaggingSpecificationFieldValue: PropTypes.func.isRequired, - clearAutoTaggingSpecificationPending: PropTypes.func.isRequired, - saveAutoTaggingSpecification: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditSpecificationModalContentConnector); diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.tsx similarity index 65% rename from frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.js rename to frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.tsx index 21977e1603..099dfd5d80 100644 --- a/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.js +++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.tsx @@ -1,25 +1,35 @@ -import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import Card from 'Components/Card'; import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import { icons, kinds } from 'Helpers/Props'; +import Field from 'typings/Field'; import translate from 'Utilities/String/translate'; import EditSpecificationModal from './EditSpecificationModal'; import styles from './Specification.css'; -export default function Specification(props) { - const { - id, - implementationName, - name, - required, - negate, - onConfirmDeleteSpecification, - onCloneSpecificationPress - } = props; +interface SpecificationProps { + id: number; + implementation: string; + implementationName: string; + name: string; + negate: boolean; + required: boolean; + fields: Field[]; + onConfirmDeleteSpecification: (specId: number) => void; + onCloneSpecificationPress: (specId: number) => void; +} +export default function Specification({ + id, + implementationName, + name, + required, + negate, + onConfirmDeleteSpecification, + onCloneSpecificationPress, +}: SpecificationProps) { const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -55,9 +65,7 @@ export default function Specification(props) { onPress={onEditPress} >
-
- {name} -
+
{name}
- + - { - negate ? - : - null - } + {negate ? ( + + ) : null} - { - required ? - : - null - } + {required ? ( + + ) : null}
); } - -Specification.propTypes = { - id: PropTypes.number.isRequired, - implementation: PropTypes.string.isRequired, - implementationName: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - negate: PropTypes.bool.isRequired, - required: PropTypes.bool.isRequired, - fields: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteSpecification: PropTypes.func.isRequired, - onCloneSpecificationPress: PropTypes.func.isRequired -}; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.js b/frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.js deleted file mode 100644 index d3482e94f7..0000000000 --- a/frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; - -function TagDetailsDelayProfile(props) { - const { - preferredProtocol, - enableUsenet, - enableTorrent, - usenetDelay, - torrentDelay - } = props; - - return ( -
-
- {titleCase(translate('DelayProfileProtocol', { preferredProtocol }))} -
- -
- { - enableUsenet ? - translate('UsenetDelayTime', { usenetDelay }) : - translate('UsenetDisabled') - } -
- -
- { - enableTorrent ? - translate('TorrentDelayTime', { torrentDelay }) : - translate('TorrentsDisabled') - } -
-
- ); -} - -TagDetailsDelayProfile.propTypes = { - preferredProtocol: PropTypes.string.isRequired, - enableUsenet: PropTypes.bool.isRequired, - enableTorrent: PropTypes.bool.isRequired, - usenetDelay: PropTypes.number.isRequired, - torrentDelay: PropTypes.number.isRequired -}; - -export default TagDetailsDelayProfile; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.tsx b/frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.tsx new file mode 100644 index 0000000000..7aa5a9b8b2 --- /dev/null +++ b/frontend/src/Settings/Tags/Details/TagDetailsDelayProfile.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; + +interface TagDetailsDelayProfileProps { + preferredProtocol: string; + enableUsenet: boolean; + enableTorrent: boolean; + usenetDelay: number; + torrentDelay: number; +} + +function TagDetailsDelayProfile({ + preferredProtocol, + enableUsenet, + enableTorrent, + usenetDelay, + torrentDelay, +}: TagDetailsDelayProfileProps) { + return ( +
+
+ {titleCase(translate('DelayProfileProtocol', { preferredProtocol }))} +
+ +
+ {enableUsenet + ? translate('UsenetDelayTime', { usenetDelay }) + : translate('UsenetDisabled')} +
+ +
+ {enableTorrent + ? translate('TorrentDelayTime', { torrentDelay }) + : translate('TorrentsDisabled')} +
+
+ ); +} + +export default TagDetailsDelayProfile; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModal.js b/frontend/src/Settings/Tags/Details/TagDetailsModal.js deleted file mode 100644 index 4195c64db0..0000000000 --- a/frontend/src/Settings/Tags/Details/TagDetailsModal.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import TagDetailsModalContentConnector from './TagDetailsModalContentConnector'; - -function TagDetailsModal(props) { - const { - isOpen, - onModalClose, - ...otherProps - } = props; - - return ( - - - - ); -} - -TagDetailsModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default TagDetailsModal; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModal.tsx b/frontend/src/Settings/Tags/Details/TagDetailsModal.tsx new file mode 100644 index 0000000000..e398a17981 --- /dev/null +++ b/frontend/src/Settings/Tags/Details/TagDetailsModal.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import TagDetailsModalContent, { + TagDetailsModalContentProps, +} from './TagDetailsModalContent'; + +interface TagDetailsModalProps extends TagDetailsModalContentProps { + isOpen: boolean; + onModalClose: () => void; +} + +function TagDetailsModal({ + isOpen, + onModalClose, + ...otherProps +}: TagDetailsModalProps) { + return ( + + + + ); +} + +export default TagDetailsModal; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js deleted file mode 100644 index bb281cfe26..0000000000 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js +++ /dev/null @@ -1,257 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import Label from 'Components/Label'; -import Button from 'Components/Link/Button'; -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 { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import TagDetailsDelayProfile from './TagDetailsDelayProfile'; -import styles from './TagDetailsModalContent.css'; - -function TagDetailsModalContent(props) { - const { - label, - isTagUsed, - movies, - delayProfiles, - importLists, - notifications, - releaseProfiles, - indexers, - downloadClients, - autoTags, - onModalClose, - onDeleteTagPress - } = props; - - return ( - - - {translate('TagDetails', { label })} - - - - { - !isTagUsed && -
- {translate('TagIsNotUsedAndCanBeDeleted')} -
- } - - { - movies.length ? -
- { - movies.map((item) => { - return ( -
- {item.title} -
- ); - }) - } -
: - null - } - - { - delayProfiles.length ? -
- { - delayProfiles.map((item) => { - const { - id, - preferredProtocol, - enableUsenet, - enableTorrent, - usenetDelay, - torrentDelay - } = item; - - return ( - - ); - }) - } -
: - null - } - - { - notifications.length ? -
- { - notifications.map((item) => { - return ( -
- {item.name} -
- ); - }) - } -
: - null - } - - { - importLists.length ? -
- { - importLists.map((item) => { - return ( -
- {item.name} -
- ); - }) - } -
: - null - } - - { - releaseProfiles.length ? -
- { - releaseProfiles.map((item) => { - return ( -
-
- { - item.required.map((r) => { - return ( - - ); - }) - } -
- -
- { - item.ignored.map((i) => { - return ( - - ); - }) - } -
-
- ); - }) - } -
: - null - } - - { - indexers.length ? -
- { - indexers.map((item) => { - return ( -
- {item.name} -
- ); - }) - } -
: - null - } - - { - downloadClients.length ? -
- { - downloadClients.map((item) => { - return ( -
- {item.name} -
- ); - }) - } -
: - null - } - - { - autoTags.length ? -
- { - autoTags.map((item) => { - return ( -
- {item.name} -
- ); - }) - } -
: - null - } -
- - - { - - } - - - -
- ); -} - -TagDetailsModalContent.propTypes = { - label: PropTypes.string.isRequired, - isTagUsed: PropTypes.bool.isRequired, - movies: PropTypes.arrayOf(PropTypes.object).isRequired, - delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired, - importLists: PropTypes.arrayOf(PropTypes.object).isRequired, - notifications: PropTypes.arrayOf(PropTypes.object).isRequired, - releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired, - indexers: PropTypes.arrayOf(PropTypes.object).isRequired, - downloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, - autoTags: PropTypes.arrayOf(PropTypes.object).isRequired, - onModalClose: PropTypes.func.isRequired, - onDeleteTagPress: PropTypes.func.isRequired -}; - -export default TagDetailsModalContent; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.tsx b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.tsx new file mode 100644 index 0000000000..5f0bd58052 --- /dev/null +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.tsx @@ -0,0 +1,269 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import ModelBase from 'App/ModelBase'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import Label from 'Components/Label'; +import Button from 'Components/Link/Button'; +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 { kinds } from 'Helpers/Props'; +import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; +import translate from 'Utilities/String/translate'; +import TagDetailsDelayProfile from './TagDetailsDelayProfile'; +import styles from './TagDetailsModalContent.css'; + +function findMatchingItems(ids: number[], items: T[]) { + return items.filter((s) => { + return ids.includes(s.id); + }); +} + +function createUnorderedMatchingMoviesSelector(movieIds: number[]) { + return createSelector(createAllMoviesSelector(), (movies) => + findMatchingItems(movieIds, movies) + ); +} + +function createMatchingMoviesSelector(movieIds: number[]) { + return createSelector( + createUnorderedMatchingMoviesSelector(movieIds), + (movies) => { + return movies.sort((movieA, movieB) => { + const sortTitleA = movieA.sortTitle; + const sortTitleB = movieB.sortTitle; + + if (sortTitleA > sortTitleB) { + return 1; + } else if (sortTitleA < sortTitleB) { + return -1; + } + + return 0; + }); + } + ); +} + +function createMatchingItemSelector( + ids: number[], + selector: (state: AppState) => T[] +) { + return createSelector(selector, (items) => findMatchingItems(ids, items)); +} + +export interface TagDetailsModalContentProps { + label: string; + isTagUsed: boolean; + delayProfileIds: number[]; + importListIds: number[]; + notificationIds: number[]; + releaseProfileIds: number[]; + indexerIds: number[]; + downloadClientIds: number[]; + autoTagIds: number[]; + movieIds: number[]; + onModalClose: () => void; + onDeleteTagPress: () => void; +} + +function TagDetailsModalContent({ + label, + isTagUsed, + delayProfileIds = [], + importListIds = [], + notificationIds = [], + releaseProfileIds = [], + indexerIds = [], + downloadClientIds = [], + autoTagIds = [], + movieIds = [], + onModalClose, + onDeleteTagPress, +}: TagDetailsModalContentProps) { + const movies = useSelector(createMatchingMoviesSelector(movieIds)); + + const delayProfiles = useSelector( + createMatchingItemSelector( + delayProfileIds, + (state: AppState) => state.settings.delayProfiles.items + ) + ); + + const importLists = useSelector( + createMatchingItemSelector( + importListIds, + (state: AppState) => state.settings.importLists.items + ) + ); + + const notifications = useSelector( + createMatchingItemSelector( + notificationIds, + (state: AppState) => state.settings.notifications.items + ) + ); + + const releaseProfiles = useSelector( + createMatchingItemSelector( + releaseProfileIds, + (state: AppState) => state.settings.releaseProfiles.items + ) + ); + + const indexers = useSelector( + createMatchingItemSelector( + indexerIds, + (state: AppState) => state.settings.indexers.items + ) + ); + + const downloadClients = useSelector( + createMatchingItemSelector( + downloadClientIds, + (state: AppState) => state.settings.downloadClients.items + ) + ); + + const autoTags = useSelector( + createMatchingItemSelector( + autoTagIds, + (state: AppState) => state.settings.autoTaggings.items + ) + ); + + return ( + + {translate('TagDetails', { label })} + + + {!isTagUsed &&
{translate('TagIsNotUsedAndCanBeDeleted')}
} + + {movies.length ? ( +
+ {movies.map((item) => { + return
{item.title}
; + })} +
+ ) : null} + + {delayProfiles.length ? ( +
+ {delayProfiles.map((item) => { + const { + id, + preferredProtocol, + enableUsenet, + enableTorrent, + usenetDelay, + torrentDelay, + } = item; + + return ( + + ); + })} +
+ ) : null} + + {notifications.length ? ( +
+ {notifications.map((item) => { + return
{item.name}
; + })} +
+ ) : null} + + {importLists.length ? ( +
+ {importLists.map((item) => { + return
{item.name}
; + })} +
+ ) : null} + + {releaseProfiles.length ? ( +
+ {releaseProfiles.map((item) => { + return ( +
+
+ {item.required.map((r) => { + return ( + + ); + })} +
+ +
+ {item.ignored.map((i) => { + return ( + + ); + })} +
+
+ ); + })} +
+ ) : null} + + {indexers.length ? ( +
+ {indexers.map((item) => { + return
{item.name}
; + })} +
+ ) : null} + + {downloadClients.length ? ( +
+ {downloadClients.map((item) => { + return
{item.name}
; + })} +
+ ) : null} + + {autoTags.length ? ( +
+ {autoTags.map((item) => { + return
{item.name}
; + })} +
+ ) : null} +
+ + + + + + +
+ ); +} + +export default TagDetailsModalContent; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js deleted file mode 100644 index e8f8ff810b..0000000000 --- a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js +++ /dev/null @@ -1,121 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; -import TagDetailsModalContent from './TagDetailsModalContent'; - -function findMatchingItems(ids, items) { - return items.filter((s) => { - return ids.includes(s.id); - }); -} - -function createUnorderedMatchingMoviesSelector() { - return createSelector( - (state, { movieIds }) => movieIds, - createAllMoviesSelector(), - findMatchingItems - ); -} - -function createMatchingMoviesSelector() { - return createSelector( - createUnorderedMatchingMoviesSelector(), - (movies) => { - return movies.sort((movieA, movieB) => { - const sortTitleA = movieA.sortTitle; - const sortTitleB = movieB.sortTitle; - - if (sortTitleA > sortTitleB) { - return 1; - } else if (sortTitleA < sortTitleB) { - return -1; - } - - return 0; - }); - } - ); -} - -function createMatchingDelayProfilesSelector() { - return createSelector( - (state, { delayProfileIds }) => delayProfileIds, - (state) => state.settings.delayProfiles.items, - findMatchingItems - ); -} - -function createMatchingNotificationsSelector() { - return createSelector( - (state, { notificationIds }) => notificationIds, - (state) => state.settings.notifications.items, - findMatchingItems - ); -} - -function createMatchingReleaseProfilesSelector() { - return createSelector( - (state, { releaseProfileIds }) => releaseProfileIds, - (state) => state.settings.releaseProfiles.items, - findMatchingItems - ); -} - -function createMatchingImportListsSelector() { - return createSelector( - (state, { importListIds }) => importListIds, - (state) => state.settings.importLists.items, - findMatchingItems - ); -} - -function createMatchingIndexersSelector() { - return createSelector( - (state, { indexerIds }) => indexerIds, - (state) => state.settings.indexers.items, - findMatchingItems - ); -} - -function createMatchingDownloadClientsSelector() { - return createSelector( - (state, { downloadClientIds }) => downloadClientIds, - (state) => state.settings.downloadClients.items, - findMatchingItems - ); -} - -function createMatchingAutoTagsSelector() { - return createSelector( - (state, { autoTagIds }) => autoTagIds, - (state) => state.settings.autoTaggings.items, - findMatchingItems - ); -} - -function createMapStateToProps() { - return createSelector( - createMatchingMoviesSelector(), - createMatchingDelayProfilesSelector(), - createMatchingNotificationsSelector(), - createMatchingReleaseProfilesSelector(), - createMatchingImportListsSelector(), - createMatchingIndexersSelector(), - createMatchingDownloadClientsSelector(), - createMatchingAutoTagsSelector(), - (movies, delayProfiles, notifications, releaseProfiles, importLists, indexers, downloadClients, autoTags) => { - return { - movies, - delayProfiles, - notifications, - releaseProfiles, - importLists, - indexers, - downloadClients, - autoTags - }; - } - ); -} - -export default connect(createMapStateToProps)(TagDetailsModalContent); diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js deleted file mode 100644 index ebdebcbe27..0000000000 --- a/frontend/src/Settings/Tags/Tag.js +++ /dev/null @@ -1,196 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import TagDetailsModal from './Details/TagDetailsModal'; -import TagInUse from './TagInUse'; -import styles from './Tag.css'; - -class Tag extends Component { - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isDetailsModalOpen: false, - isDeleteTagModalOpen: false - }; - } - - // - // Listeners - - onShowDetailsPress = () => { - this.setState({ isDetailsModalOpen: true }); - }; - - onDetailsModalClose = () => { - this.setState({ isDetailsModalOpen: false }); - }; - - onDeleteTagPress = () => { - this.setState({ - isDetailsModalOpen: false, - isDeleteTagModalOpen: true - }); - }; - - onDeleteTagModalClose = () => { - this.setState({ isDeleteTagModalOpen: false }); - }; - - onConfirmDeleteTag = () => { - this.props.onConfirmDeleteTag({ id: this.props.id }); - }; - - // - // Render - - render() { - const { - label, - delayProfileIds, - importListIds, - notificationIds, - releaseProfileIds, - indexerIds, - downloadClientIds, - autoTagIds, - movieIds - } = this.props; - - const { isDetailsModalOpen, isDeleteTagModalOpen } = this.state; - - const isTagUsed = !!( - delayProfileIds.length || - importListIds.length || - notificationIds.length || - releaseProfileIds.length || - indexerIds.length || - downloadClientIds.length || - autoTagIds.length || - movieIds.length - ); - - return ( - -
{label}
- - { - isTagUsed ? -
- - - - - - - - - - - - - -
: - null - } - - { - !isTagUsed && -
- {translate('NoLinks')} -
- } - - - - -
- ); - } -} - -Tag.propTypes = { - id: PropTypes.number.isRequired, - label: PropTypes.string.isRequired, - delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired, - importListIds: PropTypes.arrayOf(PropTypes.number).isRequired, - notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired, - releaseProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired, - indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired, - downloadClientIds: PropTypes.arrayOf(PropTypes.number).isRequired, - autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired, - movieIds: PropTypes.arrayOf(PropTypes.number).isRequired, - onConfirmDeleteTag: PropTypes.func.isRequired -}; - -Tag.defaultProps = { - delayProfileIds: [], - importListIds: [], - notificationIds: [], - releaseProfileIds: [], - indexerIds: [], - downloadClientIds: [], - autoTagIds: [], - movieIds: [] -}; - -export default Tag; diff --git a/frontend/src/Settings/Tags/Tag.tsx b/frontend/src/Settings/Tags/Tag.tsx new file mode 100644 index 0000000000..a50a737123 --- /dev/null +++ b/frontend/src/Settings/Tags/Tag.tsx @@ -0,0 +1,155 @@ +import React, { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Card from 'Components/Card'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import { kinds } from 'Helpers/Props'; +import { deleteTag } from 'Store/Actions/tagActions'; +import createTagDetailsSelector from 'Store/Selectors/createTagDetailsSelector'; +import translate from 'Utilities/String/translate'; +import TagDetailsModal from './Details/TagDetailsModal'; +import TagInUse from './TagInUse'; +import styles from './Tag.css'; + +interface TagProps { + id: number; + label: string; +} + +function Tag({ id, label }: TagProps) { + const dispatch = useDispatch(); + const { + delayProfileIds = [], + importListIds = [], + notificationIds = [], + releaseProfileIds = [], + indexerIds = [], + downloadClientIds = [], + autoTagIds = [], + movieIds = [], + } = useSelector(createTagDetailsSelector(id)) ?? {}; + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false); + const [isDeleteTagModalOpen, setIsDeleteTagModalOpen] = useState(false); + + const isTagUsed = !!( + delayProfileIds.length || + importListIds.length || + notificationIds.length || + releaseProfileIds.length || + indexerIds.length || + downloadClientIds.length || + autoTagIds.length || + movieIds.length + ); + + const handleShowDetailsPress = useCallback(() => { + setIsDetailsModalOpen(true); + }, []); + + const handeDetailsModalClose = useCallback(() => { + setIsDetailsModalOpen(false); + }, []); + + const handleDeleteTagPress = useCallback(() => { + setIsDetailsModalOpen(false); + setIsDeleteTagModalOpen(true); + }, []); + + const handleConfirmDeleteTag = useCallback(() => { + setIsDeleteTagModalOpen(false); + }, []); + + const handleDeleteTagModalClose = useCallback(() => { + dispatch(deleteTag({ id })); + }, [id, dispatch]); + + return ( + +
{label}
+ + {isTagUsed ? ( +
+ + + + + + + + + + + + + + + +
+ ) : null} + + {!isTagUsed &&
{translate('NoLinks')}
} + + + + +
+ ); +} + +export default Tag; diff --git a/frontend/src/Settings/Tags/TagConnector.js b/frontend/src/Settings/Tags/TagConnector.js deleted file mode 100644 index 986acc8e80..0000000000 --- a/frontend/src/Settings/Tags/TagConnector.js +++ /dev/null @@ -1,22 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteTag } from 'Store/Actions/tagActions'; -import createTagDetailsSelector from 'Store/Selectors/createTagDetailsSelector'; -import Tag from './Tag'; - -function createMapStateToProps() { - return createSelector( - createTagDetailsSelector(), - (tagDetails) => { - return { - ...tagDetails - }; - } - ); -} - -const mapStateToProps = { - onConfirmDeleteTag: deleteTag -}; - -export default connect(createMapStateToProps, mapStateToProps)(Tag); diff --git a/frontend/src/Settings/Tags/TagInUse.js b/frontend/src/Settings/Tags/TagInUse.tsx similarity index 50% rename from frontend/src/Settings/Tags/TagInUse.js rename to frontend/src/Settings/Tags/TagInUse.tsx index 27228fa2e8..0f8090a85d 100644 --- a/frontend/src/Settings/Tags/TagInUse.js +++ b/frontend/src/Settings/Tags/TagInUse.tsx @@ -1,13 +1,12 @@ -import PropTypes from 'prop-types'; import React from 'react'; -export default function TagInUse(props) { - const { - label, - labelPlural, - count - } = props; +interface TagInUseProps { + label: string; + labelPlural?: string; + count: number; +} +export default function TagInUse({ label, labelPlural, count }: TagInUseProps) { if (count === 0) { return null; } @@ -26,9 +25,3 @@ export default function TagInUse(props) {
); } - -TagInUse.propTypes = { - label: PropTypes.string.isRequired, - labelPlural: PropTypes.string, - count: PropTypes.number.isRequired -}; diff --git a/frontend/src/Settings/Tags/TagSettings.js b/frontend/src/Settings/Tags/TagSettings.tsx similarity index 80% rename from frontend/src/Settings/Tags/TagSettings.js rename to frontend/src/Settings/Tags/TagSettings.tsx index b37b185f73..68edf765b8 100644 --- a/frontend/src/Settings/Tags/TagSettings.js +++ b/frontend/src/Settings/Tags/TagSettings.tsx @@ -4,17 +4,15 @@ import PageContentBody from 'Components/Page/PageContentBody'; import SettingsToolbar from 'Settings/SettingsToolbar'; import translate from 'Utilities/String/translate'; import AutoTaggings from './AutoTagging/AutoTaggings'; -import TagsConnector from './TagsConnector'; +import Tags from './Tags'; function TagSettings() { return ( - + - + diff --git a/frontend/src/Settings/Tags/Tags.js b/frontend/src/Settings/Tags/Tags.js deleted file mode 100644 index 8f5a9918a2..0000000000 --- a/frontend/src/Settings/Tags/Tags.js +++ /dev/null @@ -1,54 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import TagConnector from './TagConnector'; -import styles from './Tags.css'; - -function Tags(props) { - const { - items, - ...otherProps - } = props; - - if (!items.length) { - return ( - - {translate('NoTagsHaveBeenAddedYet')} - - ); - } - - return ( -
- -
- { - items.map((item) => { - return ( - - ); - }) - } -
-
-
- ); -} - -Tags.propTypes = { - items: PropTypes.arrayOf(PropTypes.object).isRequired -}; - -export default Tags; diff --git a/frontend/src/Settings/Tags/Tags.tsx b/frontend/src/Settings/Tags/Tags.tsx new file mode 100644 index 0000000000..85768d5656 --- /dev/null +++ b/frontend/src/Settings/Tags/Tags.tsx @@ -0,0 +1,73 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import TagsAppState, { Tag as TagModel } from 'App/State/TagsAppState'; +import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import { kinds } from 'Helpers/Props'; +import { + fetchDelayProfiles, + fetchDownloadClients, + fetchImportLists, + fetchIndexers, + fetchNotifications, + fetchReleaseProfiles, +} from 'Store/Actions/settingsActions'; +import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; +import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; +import translate from 'Utilities/String/translate'; +import Tag from './Tag'; +import styles from './Tags.css'; + +function Tags() { + const dispatch = useDispatch(); + const { items, isFetching, isPopulated, error, details } = useSelector( + createSortedSectionSelector( + 'tags', + sortByProp('label') + ) + ); + + const { + isFetching: isDetailsFetching, + isPopulated: isDetailsPopulated, + error: detailsError, + } = details; + + useEffect(() => { + dispatch(fetchTags()); + dispatch(fetchTagDetails()); + dispatch(fetchDelayProfiles()); + dispatch(fetchImportLists()); + dispatch(fetchNotifications()); + dispatch(fetchReleaseProfiles()); + dispatch(fetchIndexers()); + dispatch(fetchDownloadClients()); + }, [dispatch]); + + if (!items.length) { + return ( + {translate('NoTagsHaveBeenAddedYet')} + ); + } + + return ( +
+ +
+ {items.map((item) => { + return ; + })} +
+
+
+ ); +} + +export default Tags; diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js deleted file mode 100644 index 1ff551837c..0000000000 --- a/frontend/src/Settings/Tags/TagsConnector.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; -import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; -import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByProp from 'Utilities/Array/sortByProp'; -import Tags from './Tags'; - -function createMapStateToProps() { - return createSelector( - createSortedSectionSelector('tags', sortByProp('label')), - (tags) => { - const isFetching = tags.isFetching || tags.details.isFetching; - const error = tags.error || tags.details.error; - const isPopulated = tags.isPopulated && tags.details.isPopulated; - - return { - ...tags, - isFetching, - error, - isPopulated - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchTags: fetchTags, - dispatchFetchTagDetails: fetchTagDetails, - dispatchFetchDelayProfiles: fetchDelayProfiles, - dispatchFetchNotifications: fetchNotifications, - dispatchFetchReleaseProfiles: fetchReleaseProfiles, - dispatchFetchImportLists: fetchImportLists, - dispatchFetchIndexers: fetchIndexers, - dispatchFetchDownloadClients: fetchDownloadClients -}; - -class MetadatasConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - dispatchFetchTags, - dispatchFetchTagDetails, - dispatchFetchDelayProfiles, - dispatchFetchNotifications, - dispatchFetchReleaseProfiles, - dispatchFetchImportLists, - dispatchFetchIndexers, - dispatchFetchDownloadClients - } = this.props; - - dispatchFetchTags(); - dispatchFetchTagDetails(); - dispatchFetchDelayProfiles(); - dispatchFetchNotifications(); - dispatchFetchReleaseProfiles(); - dispatchFetchImportLists(); - dispatchFetchIndexers(); - dispatchFetchDownloadClients(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -MetadatasConnector.propTypes = { - dispatchFetchTags: PropTypes.func.isRequired, - dispatchFetchTagDetails: PropTypes.func.isRequired, - dispatchFetchDelayProfiles: PropTypes.func.isRequired, - dispatchFetchNotifications: PropTypes.func.isRequired, - dispatchFetchReleaseProfiles: PropTypes.func.isRequired, - dispatchFetchImportLists: PropTypes.func.isRequired, - dispatchFetchIndexers: PropTypes.func.isRequired, - dispatchFetchDownloadClients: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector); diff --git a/frontend/src/Store/Selectors/createTagDetailsSelector.ts b/frontend/src/Store/Selectors/createTagDetailsSelector.ts index 2a271cafe7..aa330a657d 100644 --- a/frontend/src/Store/Selectors/createTagDetailsSelector.ts +++ b/frontend/src/Store/Selectors/createTagDetailsSelector.ts @@ -1,11 +1,10 @@ import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; -function createTagDetailsSelector() { +function createTagDetailsSelector(id: number) { return createSelector( - (_: AppState, { id }: { id: number }) => id, (state: AppState) => state.tags.details.items, - (id, tagDetails) => { + (tagDetails) => { return tagDetails.find((t) => t.id === id); } ); diff --git a/frontend/src/typings/AutoTagging.ts b/frontend/src/typings/AutoTagging.ts new file mode 100644 index 0000000000..fab9759d3e --- /dev/null +++ b/frontend/src/typings/AutoTagging.ts @@ -0,0 +1,21 @@ +import ModelBase from 'App/ModelBase'; +import Field from './Field'; + +export interface AutoTaggingSpecification { + id: number; + name: string; + implementation: string; + implementationName: string; + negate: boolean; + required: boolean; + fields: Field[]; +} + +interface AutoTagging extends ModelBase { + name: string; + removeTagsAutomatically: boolean; + tags: number[]; + specifications: AutoTaggingSpecification[]; +} + +export default AutoTagging; diff --git a/frontend/src/typings/DelayProfile.ts b/frontend/src/typings/DelayProfile.ts new file mode 100644 index 0000000000..0e087b1427 --- /dev/null +++ b/frontend/src/typings/DelayProfile.ts @@ -0,0 +1,17 @@ +import ModelBase from 'App/ModelBase'; + +interface DelayProfile extends ModelBase { + name: string; + enableUsenet: boolean; + enableTorrent: boolean; + preferredProtocol: string; + usenetDelay: number; + torrentDelay: number; + bypassIfHighestQuality: boolean; + bypassIfAboveCustomFormatScore: boolean; + minimumCustomFormatScore: number; + order: number; + tags: number[]; +} + +export default DelayProfile; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 0a1c13b9ca..272638fb0d 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1579,6 +1579,7 @@ "ReleaseGroupFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Release Group:30}`) or the beginning (e.g. `{Release Group:-30}`) are both supported.`).", "ReleaseGroups": "Release Groups", "ReleaseHash": "Release Hash", + "ReleaseProfile": "Release Profile", "ReleaseProfileIndexerHelpText": "Specify what indexer the profile applies to", "ReleaseProfileIndexerHelpTextWarning": "Setting a specific indexer on a release profile will cause this profile to only apply to releases from that indexer.", "ReleaseProfileTagMovieHelpText": "Release profiles will apply to movies with at least one matching tag. Leave blank to apply to all movies",