parent
405ee7473c
commit
89f584d1b3
@ -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 (
|
|
||||||
<Modal
|
|
||||||
style={{ height: height === 'auto' ? 'auto': `${height}px` }}
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<EditAutoTaggingModalContent
|
|
||||||
{...otherProps}
|
|
||||||
onContentHeightChange={onContentHeightChange}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditAutoTaggingModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
@ -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 (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={handleModalClose}>
|
||||||
|
<EditAutoTaggingModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -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 (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
|
|
||||||
<ModalHeader>
|
|
||||||
{id ? translate('EditAutoTag') : translate('AddAutoTag')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isFetching ? <LoadingIndicator />: null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('AddAutoTagError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !error && specificationsPopulated ?
|
|
||||||
<div>
|
|
||||||
<Form
|
|
||||||
validationErrors={validationErrors}
|
|
||||||
validationWarnings={validationWarnings}
|
|
||||||
>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('Name')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="name"
|
|
||||||
{...name}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveTagsAutomatically')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="removeTagsAutomatically"
|
|
||||||
helpText={translate('RemoveTagsAutomaticallyHelpText')}
|
|
||||||
{...removeTagsAutomatically}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Tags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="tags"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...tags}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('Conditions')}>
|
|
||||||
<div className={styles.autoTaggings}>
|
|
||||||
{
|
|
||||||
specifications.map((tag) => {
|
|
||||||
return (
|
|
||||||
<Specification
|
|
||||||
key={tag.id}
|
|
||||||
{...tag}
|
|
||||||
onCloneSpecificationPress={onCloneSpecificationPress}
|
|
||||||
onConfirmDeleteSpecification={onConfirmDeleteSpecification}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className={styles.addSpecification}
|
|
||||||
onPress={onAddSpecificationPress}
|
|
||||||
>
|
|
||||||
<div className={styles.center}>
|
|
||||||
<Icon
|
|
||||||
name={icons.ADD}
|
|
||||||
size={45}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<AddSpecificationModal
|
|
||||||
isOpen={isAddSpecificationModalOpen}
|
|
||||||
onModalClose={onAddSpecificationModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditSpecificationModal
|
|
||||||
isOpen={isEditSpecificationModalOpen}
|
|
||||||
onModalClose={onEditSpecificationModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* <ImportAutoTaggingModal
|
|
||||||
isOpen={isImportAutoTaggingModalOpen}
|
|
||||||
onModalClose={onImportAutoTaggingModalClose}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<div className={styles.rightButtons}>
|
|
||||||
{
|
|
||||||
id ?
|
|
||||||
<Button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onDeleteAutoTaggingPress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</Button> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* <Button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
onPress={onImportPress}
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</Button> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
error={saveError}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditAutoTaggingModalContent.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
tagsFromId: PropTypes.number,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onDeleteAutoTaggingPress: PropTypes.func
|
|
||||||
};
|
|
@ -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 (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? translate('EditAutoTag') : translate('AddAutoTag')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('AddAutoTagError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && !error && specificationsPopulated ? (
|
||||||
|
<div>
|
||||||
|
<Form
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
validationWarnings={validationWarnings}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Name')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="name"
|
||||||
|
{...name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveTagsAutomatically')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="removeTagsAutomatically"
|
||||||
|
helpText={translate('RemoveTagsAutomaticallyHelpText')}
|
||||||
|
{...removeTagsAutomatically}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...tags}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Conditions')}>
|
||||||
|
<div className={styles.autoTaggings}>
|
||||||
|
{specifications.map((specification) => {
|
||||||
|
return (
|
||||||
|
<Specification
|
||||||
|
key={specification.id}
|
||||||
|
{...specification}
|
||||||
|
onCloneSpecificationPress={
|
||||||
|
handleCloneSpecificationPress
|
||||||
|
}
|
||||||
|
onConfirmDeleteSpecification={
|
||||||
|
handleConfirmDeleteSpecification
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={styles.addSpecification}
|
||||||
|
onPress={handleAddSpecificationPress}
|
||||||
|
>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon name={icons.ADD} size={45} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<AddSpecificationModal
|
||||||
|
isOpen={isAddSpecificationModalOpen}
|
||||||
|
onModalClose={handleAddSpecificationModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditSpecificationModal
|
||||||
|
isOpen={isEditSpecificationModalOpen}
|
||||||
|
onModalClose={handleEditSpecificationModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <ImportAutoTaggingModal
|
||||||
|
isOpen={isImportAutoTaggingModalOpen}
|
||||||
|
onModalClose={onImportAutoTaggingModalClose}
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<div className={styles.rightButtons}>
|
||||||
|
{id ? (
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteAutoTaggingPress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* <Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
onPress={onImportPress}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Button> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
@ -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 (
|
|
||||||
<div
|
|
||||||
className={styles.specification}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
onPress={onWrappedSpecificationSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.overlay}>
|
|
||||||
<div className={styles.name}>
|
|
||||||
{implementationName}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
{
|
|
||||||
hasPresets ?
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
size={sizes.SMALL}
|
|
||||||
onPress={onWrappedSpecificationSelect}
|
|
||||||
>
|
|
||||||
{translate('Custom')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Menu className={styles.presetsMenu}>
|
|
||||||
<Button
|
|
||||||
className={styles.presetsMenuButton}
|
|
||||||
size={sizes.SMALL}
|
|
||||||
>
|
|
||||||
{translate('Presets')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<MenuContent>
|
|
||||||
{
|
|
||||||
presets.map((preset, index) => {
|
|
||||||
return (
|
|
||||||
<AddSpecificationPresetMenuItem
|
|
||||||
key={index}
|
|
||||||
name={preset.name}
|
|
||||||
implementation={implementation}
|
|
||||||
onPress={onWrappedSpecificationSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
infoLink ?
|
|
||||||
<Button
|
|
||||||
to={infoLink}
|
|
||||||
size={sizes.SMALL}
|
|
||||||
>
|
|
||||||
{translate('MoreInfo')}
|
|
||||||
</Button> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddSpecificationItem.propTypes = {
|
|
||||||
implementation: PropTypes.string.isRequired,
|
|
||||||
implementationName: PropTypes.string.isRequired,
|
|
||||||
infoLink: PropTypes.string,
|
|
||||||
presets: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
onSpecificationSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
@ -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 (
|
||||||
|
<div className={styles.specification}>
|
||||||
|
<Link className={styles.underlay} onPress={handleSpecificationSelect} />
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.name}>{implementationName}</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
{hasPresets ? (
|
||||||
|
<span>
|
||||||
|
<Button size={sizes.SMALL} onPress={handleSpecificationSelect}>
|
||||||
|
{translate('Custom')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Menu className={styles.presetsMenu}>
|
||||||
|
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
|
||||||
|
{translate('Presets')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<MenuContent>
|
||||||
|
{presets.map((preset, index) => {
|
||||||
|
return (
|
||||||
|
<AddSpecificationPresetMenuItem
|
||||||
|
key={index}
|
||||||
|
name={preset.name}
|
||||||
|
implementation={implementation}
|
||||||
|
onPress={handleSpecificationSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{infoLink ? (
|
||||||
|
<Button to={infoLink} size={sizes.SMALL}>
|
||||||
|
{translate('MoreInfo')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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 (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AddSpecificationModalContent
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddSpecificationModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddSpecificationModal;
|
|
@ -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 (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<AddSpecificationModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddSpecificationModal;
|
@ -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 (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AddCondition')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
{
|
|
||||||
isSchemaFetching ? <LoadingIndicator /> : null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isSchemaFetching && !!schemaError ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('AddConditionError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isSchemaPopulated && !schemaError ?
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
<div>
|
|
||||||
{translate('SupportedAutoTaggingProperties')}
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className={styles.specifications}>
|
|
||||||
{
|
|
||||||
schema.map((specification) => {
|
|
||||||
return (
|
|
||||||
<AddSpecificationItem
|
|
||||||
key={specification.implementation}
|
|
||||||
{...specification}
|
|
||||||
onSpecificationSelect={onSpecificationSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddSpecificationModalContent.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
@ -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 (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('AddCondition')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{isSchemaFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isSchemaFetching && !!schemaError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('AddConditionError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isSchemaPopulated && !schemaError ? (
|
||||||
|
<div>
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
<div>{translate('SupportedAutoTaggingProperties')}</div>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className={styles.specifications}>
|
||||||
|
{schema.map((specification) => {
|
||||||
|
return (
|
||||||
|
<AddSpecificationItem
|
||||||
|
key={specification.implementation}
|
||||||
|
{...specification}
|
||||||
|
onSpecificationSelect={onSpecificationSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
@ -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 (
|
|
||||||
<MenuItem
|
|
||||||
{...otherProps}
|
|
||||||
onPress={onWrappedPress}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddSpecificationPresetMenuItem.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
implementation: PropTypes.string.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
@ -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 (
|
||||||
|
<MenuItem {...otherProps} onPress={handlePress}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
@ -1,190 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
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 translate from 'Utilities/String/translate';
|
|
||||||
import styles from './EditSpecificationModalContent.css';
|
|
||||||
|
|
||||||
function EditSpecificationModalContent(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onDeleteSpecificationPress,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const advancedSettings = useSelector((state) => 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 (
|
|
||||||
<ModalContent onModalClose={onCancelPress}>
|
|
||||||
<ModalHeader>
|
|
||||||
{id ? translate('EditConditionImplementation', { implementationName }) : translate('AddConditionImplementation', { implementationName })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form
|
|
||||||
{...otherFormProps}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
fields && fields.some((x) => x.label === 'Regular Expression') &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('Name')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="name"
|
|
||||||
{...name}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
fields && fields.map((field) => {
|
|
||||||
return (
|
|
||||||
<ProviderFieldFormGroup
|
|
||||||
key={field.name}
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
provider="specifications"
|
|
||||||
providerData={item}
|
|
||||||
{...field}
|
|
||||||
onChange={onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('Negate')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="negate"
|
|
||||||
{...negate}
|
|
||||||
helpText={translate('AutoTaggingNegateHelpText', { implementationName })}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('Required')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="required"
|
|
||||||
{...required}
|
|
||||||
helpText={translate('AutoTaggingRequiredHelpText', { implementationName })}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
id ?
|
|
||||||
<Button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onDeleteSpecificationPress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</Button> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onCancelPress}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={false}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditSpecificationModalContent.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
onDeleteSpecificationPress: PropTypes.func,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditSpecificationModalContent;
|
|
@ -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 (
|
||||||
|
<ModalContent onModalClose={onCancelPress}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id
|
||||||
|
? translate('EditConditionImplementation', { implementationName })
|
||||||
|
: translate('AddConditionImplementation', { implementationName })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form {...otherFormProps}>
|
||||||
|
{fields && fields.some((x) => x.label === 'Regular Expression') && (
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('ConditionUsingRegularExpressions')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('RegularExpressionsTutorialLink', {
|
||||||
|
url: 'https://www.regular-expressions.info/tutorial.html',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('RegularExpressionsCanBeTested', {
|
||||||
|
url: 'http://regexstorm.net/tester',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Name')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="name"
|
||||||
|
{...name}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{fields &&
|
||||||
|
fields.map((field) => {
|
||||||
|
return (
|
||||||
|
<ProviderFieldFormGroup
|
||||||
|
key={field.name}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
provider="specifications"
|
||||||
|
providerData={item}
|
||||||
|
{...field}
|
||||||
|
onChange={onFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Negate')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="negate"
|
||||||
|
{...negate}
|
||||||
|
helpText={translate('AutoTaggingNegateHelpText', {
|
||||||
|
implementationName,
|
||||||
|
})}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Required')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="required"
|
||||||
|
{...required}
|
||||||
|
helpText={translate('AutoTaggingRequiredHelpText', {
|
||||||
|
implementationName,
|
||||||
|
})}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{id ? (
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteSpecificationPress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Button onPress={onCancelPress}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton isSpinning={false} onPress={onSavePress}>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditSpecificationModalContent;
|
@ -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 (
|
|
||||||
<EditSpecificationModalContent
|
|
||||||
{...this.props}
|
|
||||||
onCancelPress={this.onCancelPress}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onFieldChange={this.onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{titleCase(translate('DelayProfileProtocol', { preferredProtocol }))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
enableUsenet ?
|
|
||||||
translate('UsenetDelayTime', { usenetDelay }) :
|
|
||||||
translate('UsenetDisabled')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
enableTorrent ?
|
|
||||||
translate('TorrentDelayTime', { torrentDelay }) :
|
|
||||||
translate('TorrentsDisabled')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{titleCase(translate('DelayProfileProtocol', { preferredProtocol }))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{enableUsenet
|
||||||
|
? translate('UsenetDelayTime', { usenetDelay })
|
||||||
|
: translate('UsenetDisabled')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{enableTorrent
|
||||||
|
? translate('TorrentDelayTime', { torrentDelay })
|
||||||
|
: translate('TorrentsDisabled')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagDetailsDelayProfile;
|
@ -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 (
|
|
||||||
<Modal
|
|
||||||
size={sizes.SMALL}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<TagDetailsModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TagDetailsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TagDetailsModal;
|
|
@ -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 (
|
||||||
|
<Modal size={sizes.SMALL} isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<TagDetailsModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagDetailsModal;
|
@ -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,
|
|
||||||
series,
|
|
||||||
delayProfiles,
|
|
||||||
importLists,
|
|
||||||
notifications,
|
|
||||||
releaseProfiles,
|
|
||||||
indexers,
|
|
||||||
downloadClients,
|
|
||||||
autoTags,
|
|
||||||
onModalClose,
|
|
||||||
onDeleteTagPress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('TagDetails', { label })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
{
|
|
||||||
!isTagUsed &&
|
|
||||||
<div>
|
|
||||||
{translate('TagIsNotUsedAndCanBeDeleted')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
series.length ?
|
|
||||||
<FieldSet legend={translate('Series')}>
|
|
||||||
{
|
|
||||||
series.map((item) => {
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.title}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
delayProfiles.length ?
|
|
||||||
<FieldSet legend={translate('DelayProfile')}>
|
|
||||||
{
|
|
||||||
delayProfiles.map((item) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
preferredProtocol,
|
|
||||||
enableUsenet,
|
|
||||||
enableTorrent,
|
|
||||||
usenetDelay,
|
|
||||||
torrentDelay
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TagDetailsDelayProfile
|
|
||||||
key={id}
|
|
||||||
preferredProtocol={preferredProtocol}
|
|
||||||
enableUsenet={enableUsenet}
|
|
||||||
enableTorrent={enableTorrent}
|
|
||||||
usenetDelay={usenetDelay}
|
|
||||||
torrentDelay={torrentDelay}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
notifications.length ?
|
|
||||||
<FieldSet legend={translate('Connections')}>
|
|
||||||
{
|
|
||||||
notifications.map((item) => {
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
importLists.length ?
|
|
||||||
<FieldSet legend={translate('ImportLists')}>
|
|
||||||
{
|
|
||||||
importLists.map((item) => {
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
releaseProfiles.length ?
|
|
||||||
<FieldSet legend={translate('ReleaseProfiles')}>
|
|
||||||
{
|
|
||||||
releaseProfiles.map((item) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className={styles.restriction}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
item.required.map((r) => {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={r}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
>
|
|
||||||
{r}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
item.ignored.map((i) => {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={i}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
>
|
|
||||||
{i}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
indexers.length ?
|
|
||||||
<FieldSet legend={translate('Indexers')}>
|
|
||||||
{
|
|
||||||
indexers.map((item) => {
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadClients.length ?
|
|
||||||
<FieldSet legend={translate('DownloadClients')}>
|
|
||||||
{
|
|
||||||
downloadClients.map((item) => {
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
autoTags.length ?
|
|
||||||
<FieldSet legend={translate('AutoTagging')}>
|
|
||||||
{
|
|
||||||
autoTags.map((item) => {
|
|
||||||
return (
|
|
||||||
<div key={item.id}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FieldSet> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
<Button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={isTagUsed ? translate('TagCannotBeDeletedWhileInUse') : undefined}
|
|
||||||
isDisabled={isTagUsed}
|
|
||||||
onPress={onDeleteTagPress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TagDetailsModalContent.propTypes = {
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
isTagUsed: PropTypes.bool.isRequired,
|
|
||||||
series: 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;
|
|
@ -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 createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import TagDetailsDelayProfile from './TagDetailsDelayProfile';
|
||||||
|
import styles from './TagDetailsModalContent.css';
|
||||||
|
|
||||||
|
function findMatchingItems<T extends ModelBase>(ids: number[], items: T[]) {
|
||||||
|
return items.filter((s) => {
|
||||||
|
return ids.includes(s.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUnorderedMatchingSeriesSelector(seriesIds: number[]) {
|
||||||
|
return createSelector(createAllSeriesSelector(), (series) =>
|
||||||
|
findMatchingItems(seriesIds, series)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMatchingSeriesSelector(seriesIds: number[]) {
|
||||||
|
return createSelector(
|
||||||
|
createUnorderedMatchingSeriesSelector(seriesIds),
|
||||||
|
(series) => {
|
||||||
|
return series.sort((seriesA, seriesB) => {
|
||||||
|
const sortTitleA = seriesA.sortTitle;
|
||||||
|
const sortTitleB = seriesB.sortTitle;
|
||||||
|
|
||||||
|
if (sortTitleA > sortTitleB) {
|
||||||
|
return 1;
|
||||||
|
} else if (sortTitleA < sortTitleB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMatchingItemSelector<T extends ModelBase>(
|
||||||
|
ids: number[],
|
||||||
|
selector: (state: AppState) => T[]
|
||||||
|
) {
|
||||||
|
return createSelector(selector, (items) => findMatchingItems<T>(ids, items));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagDetailsModalContentProps {
|
||||||
|
label: string;
|
||||||
|
isTagUsed: boolean;
|
||||||
|
delayProfileIds: number[];
|
||||||
|
importListIds: number[];
|
||||||
|
notificationIds: number[];
|
||||||
|
restrictionIds: number[];
|
||||||
|
indexerIds: number[];
|
||||||
|
downloadClientIds: number[];
|
||||||
|
autoTagIds: number[];
|
||||||
|
seriesIds: number[];
|
||||||
|
onModalClose: () => void;
|
||||||
|
onDeleteTagPress: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagDetailsModalContent({
|
||||||
|
label,
|
||||||
|
isTagUsed,
|
||||||
|
delayProfileIds = [],
|
||||||
|
importListIds = [],
|
||||||
|
notificationIds = [],
|
||||||
|
restrictionIds = [],
|
||||||
|
indexerIds = [],
|
||||||
|
downloadClientIds = [],
|
||||||
|
autoTagIds = [],
|
||||||
|
seriesIds = [],
|
||||||
|
onModalClose,
|
||||||
|
onDeleteTagPress,
|
||||||
|
}: TagDetailsModalContentProps) {
|
||||||
|
const series = useSelector(createMatchingSeriesSelector(seriesIds));
|
||||||
|
|
||||||
|
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(
|
||||||
|
restrictionIds,
|
||||||
|
(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 (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('TagDetails', { label })}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{!isTagUsed && <div>{translate('TagIsNotUsedAndCanBeDeleted')}</div>}
|
||||||
|
|
||||||
|
{series.length ? (
|
||||||
|
<FieldSet legend={translate('Series')}>
|
||||||
|
{series.map((item) => {
|
||||||
|
return <div key={item.id}>{item.title}</div>;
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{delayProfiles.length ? (
|
||||||
|
<FieldSet legend={translate('DelayProfile')}>
|
||||||
|
{delayProfiles.map((item) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
preferredProtocol,
|
||||||
|
enableUsenet,
|
||||||
|
enableTorrent,
|
||||||
|
usenetDelay,
|
||||||
|
torrentDelay,
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TagDetailsDelayProfile
|
||||||
|
key={id}
|
||||||
|
preferredProtocol={preferredProtocol}
|
||||||
|
enableUsenet={enableUsenet}
|
||||||
|
enableTorrent={enableTorrent}
|
||||||
|
usenetDelay={usenetDelay}
|
||||||
|
torrentDelay={torrentDelay}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{notifications.length ? (
|
||||||
|
<FieldSet legend={translate('Connections')}>
|
||||||
|
{notifications.map((item) => {
|
||||||
|
return <div key={item.id}>{item.name}</div>;
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{importLists.length ? (
|
||||||
|
<FieldSet legend={translate('ImportLists')}>
|
||||||
|
{importLists.map((item) => {
|
||||||
|
return <div key={item.id}>{item.name}</div>;
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseProfiles.length ? (
|
||||||
|
<FieldSet legend={translate('ReleaseProfiles')}>
|
||||||
|
{releaseProfiles.map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.id} className={styles.restriction}>
|
||||||
|
<div>
|
||||||
|
{item.required.map((r) => {
|
||||||
|
return (
|
||||||
|
<Label key={r} kind={kinds.SUCCESS}>
|
||||||
|
{r}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{item.ignored.map((i) => {
|
||||||
|
return (
|
||||||
|
<Label key={i} kind={kinds.DANGER}>
|
||||||
|
{i}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{indexers.length ? (
|
||||||
|
<FieldSet legend={translate('Indexers')}>
|
||||||
|
{indexers.map((item) => {
|
||||||
|
return <div key={item.id}>{item.name}</div>;
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadClients.length ? (
|
||||||
|
<FieldSet legend={translate('DownloadClients')}>
|
||||||
|
{downloadClients.map((item) => {
|
||||||
|
return <div key={item.id}>{item.name}</div>;
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{autoTags.length ? (
|
||||||
|
<FieldSet legend={translate('AutoTagging')}>
|
||||||
|
{autoTags.map((item) => {
|
||||||
|
return <div key={item.id}>{item.name}</div>;
|
||||||
|
})}
|
||||||
|
</FieldSet>
|
||||||
|
) : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={
|
||||||
|
isTagUsed ? translate('TagCannotBeDeletedWhileInUse') : undefined
|
||||||
|
}
|
||||||
|
isDisabled={isTagUsed}
|
||||||
|
onPress={onDeleteTagPress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagDetailsModalContent;
|
@ -1,121 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
|
||||||
import TagDetailsModalContent from './TagDetailsModalContent';
|
|
||||||
|
|
||||||
function findMatchingItems(ids, items) {
|
|
||||||
return items.filter((s) => {
|
|
||||||
return ids.includes(s.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createUnorderedMatchingSeriesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { seriesIds }) => seriesIds,
|
|
||||||
createAllSeriesSelector(),
|
|
||||||
findMatchingItems
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMatchingSeriesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
createUnorderedMatchingSeriesSelector(),
|
|
||||||
(series) => {
|
|
||||||
return series.sort((seriesA, seriesB) => {
|
|
||||||
const sortTitleA = seriesA.sortTitle;
|
|
||||||
const sortTitleB = seriesB.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 createMatchingImportListsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { importListIds }) => importListIds,
|
|
||||||
(state) => state.settings.importLists.items,
|
|
||||||
findMatchingItems
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMatchingNotificationsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { notificationIds }) => notificationIds,
|
|
||||||
(state) => state.settings.notifications.items,
|
|
||||||
findMatchingItems
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMatchingReleaseProfilesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { restrictionIds }) => restrictionIds,
|
|
||||||
(state) => state.settings.releaseProfiles.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(
|
|
||||||
createMatchingSeriesSelector(),
|
|
||||||
createMatchingDelayProfilesSelector(),
|
|
||||||
createMatchingImportListsSelector(),
|
|
||||||
createMatchingNotificationsSelector(),
|
|
||||||
createMatchingReleaseProfilesSelector(),
|
|
||||||
createMatchingIndexersSelector(),
|
|
||||||
createMatchingDownloadClientsSelector(),
|
|
||||||
createMatchingAutoTagsSelector(),
|
|
||||||
(series, delayProfiles, importLists, notifications, releaseProfiles, indexers, downloadClients, autoTags) => {
|
|
||||||
return {
|
|
||||||
series,
|
|
||||||
delayProfiles,
|
|
||||||
importLists,
|
|
||||||
notifications,
|
|
||||||
releaseProfiles,
|
|
||||||
indexers,
|
|
||||||
downloadClients,
|
|
||||||
autoTags
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(TagDetailsModalContent);
|
|
@ -1,207 +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,
|
|
||||||
restrictionIds,
|
|
||||||
indexerIds,
|
|
||||||
downloadClientIds,
|
|
||||||
autoTagIds,
|
|
||||||
seriesIds
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isDetailsModalOpen,
|
|
||||||
isDeleteTagModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isTagUsed = !!(
|
|
||||||
delayProfileIds.length ||
|
|
||||||
importListIds.length ||
|
|
||||||
notificationIds.length ||
|
|
||||||
restrictionIds.length ||
|
|
||||||
indexerIds.length ||
|
|
||||||
downloadClientIds.length ||
|
|
||||||
autoTagIds.length ||
|
|
||||||
seriesIds.length
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={styles.tag}
|
|
||||||
overlayContent={true}
|
|
||||||
onPress={this.onShowDetailsPress}
|
|
||||||
>
|
|
||||||
<div className={styles.label}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isTagUsed ?
|
|
||||||
<div>
|
|
||||||
<TagInUse
|
|
||||||
label={translate('Series')}
|
|
||||||
count={seriesIds.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagInUse
|
|
||||||
label={translate('DelayProfile')}
|
|
||||||
labelPlural={translate('DelayProfiles')}
|
|
||||||
count={delayProfileIds.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagInUse
|
|
||||||
label={translate('ImportList')}
|
|
||||||
labelPlural={translate('ImportLists')}
|
|
||||||
count={importListIds.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagInUse
|
|
||||||
label={translate('Connection')}
|
|
||||||
labelPlural={translate('Connections')}
|
|
||||||
count={notificationIds.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagInUse
|
|
||||||
label={translate('ReleaseProfile')}
|
|
||||||
labelPlural={translate('ReleaseProfiles')}
|
|
||||||
count={restrictionIds.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagInUse
|
|
||||||
label={translate('Indexer')}
|
|
||||||
labelPlural={translate('Indexers')}
|
|
||||||
count={indexerIds.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagInUse
|
|
||||||
label={translate('DownloadClient')}
|
|
||||||
labelPlural={translate('DownloadClients')}
|
|
||||||
count={downloadClientIds.length}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TagInUse
|
|
||||||
label={translate('AutoTagging')}
|
|
||||||
count={autoTagIds.length}
|
|
||||||
/>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isTagUsed &&
|
|
||||||
<div>
|
|
||||||
{translate('NoLinks')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<TagDetailsModal
|
|
||||||
label={label}
|
|
||||||
isTagUsed={isTagUsed}
|
|
||||||
seriesIds={seriesIds}
|
|
||||||
delayProfileIds={delayProfileIds}
|
|
||||||
importListIds={importListIds}
|
|
||||||
notificationIds={notificationIds}
|
|
||||||
restrictionIds={restrictionIds}
|
|
||||||
indexerIds={indexerIds}
|
|
||||||
downloadClientIds={downloadClientIds}
|
|
||||||
autoTagIds={autoTagIds}
|
|
||||||
isOpen={isDetailsModalOpen}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
onDeleteTagPress={this.onDeleteTagPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isDeleteTagModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('DeleteTag')}
|
|
||||||
message={translate('DeleteTagMessageText', { label })}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
onConfirm={this.onConfirmDeleteTag}
|
|
||||||
onCancel={this.onDeleteTagModalClose}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
downloadClientIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
onConfirmDeleteTag: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Tag.defaultProps = {
|
|
||||||
delayProfileIds: [],
|
|
||||||
importListIds: [],
|
|
||||||
notificationIds: [],
|
|
||||||
restrictionIds: [],
|
|
||||||
indexerIds: [],
|
|
||||||
downloadClientIds: [],
|
|
||||||
autoTagIds: [],
|
|
||||||
seriesIds: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tag;
|
|
@ -0,0 +1,151 @@
|
|||||||
|
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 = [],
|
||||||
|
restrictionIds = [],
|
||||||
|
indexerIds = [],
|
||||||
|
downloadClientIds = [],
|
||||||
|
autoTagIds = [],
|
||||||
|
seriesIds = [],
|
||||||
|
} = useSelector(createTagDetailsSelector(id)) ?? {};
|
||||||
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
const [isDeleteTagModalOpen, setIsDeleteTagModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const isTagUsed = !!(
|
||||||
|
delayProfileIds.length ||
|
||||||
|
importListIds.length ||
|
||||||
|
notificationIds.length ||
|
||||||
|
restrictionIds.length ||
|
||||||
|
indexerIds.length ||
|
||||||
|
downloadClientIds.length ||
|
||||||
|
autoTagIds.length ||
|
||||||
|
seriesIds.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 (
|
||||||
|
<Card
|
||||||
|
className={styles.tag}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={handleShowDetailsPress}
|
||||||
|
>
|
||||||
|
<div className={styles.label}>{label}</div>
|
||||||
|
|
||||||
|
{isTagUsed ? (
|
||||||
|
<div>
|
||||||
|
<TagInUse label={translate('Series')} count={seriesIds.length} />
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label={translate('DelayProfile')}
|
||||||
|
labelPlural={translate('DelayProfiles')}
|
||||||
|
count={delayProfileIds.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label={translate('ImportList')}
|
||||||
|
labelPlural={translate('ImportLists')}
|
||||||
|
count={importListIds.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label={translate('Connection')}
|
||||||
|
labelPlural={translate('Connections')}
|
||||||
|
count={notificationIds.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label={translate('ReleaseProfile')}
|
||||||
|
labelPlural={translate('ReleaseProfiles')}
|
||||||
|
count={restrictionIds.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label={translate('Indexer')}
|
||||||
|
labelPlural={translate('Indexers')}
|
||||||
|
count={indexerIds.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label={translate('DownloadClient')}
|
||||||
|
labelPlural={translate('DownloadClients')}
|
||||||
|
count={downloadClientIds.length}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label={translate('AutoTagging')}
|
||||||
|
count={autoTagIds.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isTagUsed && <div>{translate('NoLinks')}</div>}
|
||||||
|
|
||||||
|
<TagDetailsModal
|
||||||
|
label={label}
|
||||||
|
isTagUsed={isTagUsed}
|
||||||
|
seriesIds={seriesIds}
|
||||||
|
delayProfileIds={delayProfileIds}
|
||||||
|
importListIds={importListIds}
|
||||||
|
notificationIds={notificationIds}
|
||||||
|
restrictionIds={restrictionIds}
|
||||||
|
indexerIds={indexerIds}
|
||||||
|
downloadClientIds={downloadClientIds}
|
||||||
|
autoTagIds={autoTagIds}
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
onModalClose={handeDetailsModalClose}
|
||||||
|
onDeleteTagPress={handleDeleteTagPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteTagModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteTag')}
|
||||||
|
message={translate('DeleteTagMessageText', { label })}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={handleConfirmDeleteTag}
|
||||||
|
onCancel={handleDeleteTagModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tag;
|
@ -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);
|
|
@ -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 (
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('NoTagsHaveBeenAddedYet')}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet
|
|
||||||
legend={translate('Tags')}
|
|
||||||
>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('TagsLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.tags}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<TagConnector
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Tags.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tags;
|
|
@ -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<TagModel, TagsAppState>(
|
||||||
|
'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 (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoTagsHaveBeenAddedYet')}</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('Tags')}>
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage={translate('TagsLoadError')}
|
||||||
|
error={error || detailsError}
|
||||||
|
isFetching={isFetching || isDetailsFetching}
|
||||||
|
isPopulated={isPopulated || isDetailsPopulated}
|
||||||
|
>
|
||||||
|
<div className={styles.tags}>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <Tag key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tags;
|
@ -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,
|
|
||||||
dispatchFetchImportLists: fetchImportLists,
|
|
||||||
dispatchFetchNotifications: fetchNotifications,
|
|
||||||
dispatchFetchReleaseProfiles: fetchReleaseProfiles,
|
|
||||||
dispatchFetchIndexers: fetchIndexers,
|
|
||||||
dispatchFetchDownloadClients: fetchDownloadClients
|
|
||||||
};
|
|
||||||
|
|
||||||
class MetadatasConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
dispatchFetchTags,
|
|
||||||
dispatchFetchTagDetails,
|
|
||||||
dispatchFetchDelayProfiles,
|
|
||||||
dispatchFetchImportLists,
|
|
||||||
dispatchFetchNotifications,
|
|
||||||
dispatchFetchReleaseProfiles,
|
|
||||||
dispatchFetchIndexers,
|
|
||||||
dispatchFetchDownloadClients
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchTags();
|
|
||||||
dispatchFetchTagDetails();
|
|
||||||
dispatchFetchDelayProfiles();
|
|
||||||
dispatchFetchImportLists();
|
|
||||||
dispatchFetchNotifications();
|
|
||||||
dispatchFetchReleaseProfiles();
|
|
||||||
dispatchFetchIndexers();
|
|
||||||
dispatchFetchDownloadClients();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Tags
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MetadatasConnector.propTypes = {
|
|
||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchReleaseProfiles: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchDownloadClients: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
|
@ -1,71 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
|
|
||||||
function selector(id, section) {
|
|
||||||
if (!id) {
|
|
||||||
const item = _.isArray(section.schema) ? section.selectedSchema : section.schema;
|
|
||||||
const settings = selectSettings(Object.assign({ name: '' }, item), section.pendingChanges, section.saveError);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isSchemaFetching: isFetching,
|
|
||||||
isSchemaPopulated: isPopulated,
|
|
||||||
schemaError: error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
pendingChanges
|
|
||||||
} = section;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
pendingChanges,
|
|
||||||
...settings,
|
|
||||||
item: settings.settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
pendingChanges
|
|
||||||
} = section;
|
|
||||||
|
|
||||||
const settings = selectSettings(_.find(section.items, { id }), pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
...settings,
|
|
||||||
item: settings.settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createProviderSettingsSelector(sectionName) {
|
|
||||||
return createSelector(
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.settings[sectionName],
|
|
||||||
(id, section) => selector(id, section)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProviderSettingsSelectorHook(sectionName, id) {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings[sectionName],
|
|
||||||
(section) => selector(id, section)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import {
|
||||||
|
AppSectionProviderState,
|
||||||
|
AppSectionSchemaState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import selectSettings, {
|
||||||
|
ModelBaseSetting,
|
||||||
|
} from 'Store/Selectors/selectSettings';
|
||||||
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
|
||||||
|
function selector<
|
||||||
|
T extends ModelBaseSetting,
|
||||||
|
S extends AppSectionProviderState<T> & AppSectionSchemaState<T>
|
||||||
|
>(id: number | undefined, section: S) {
|
||||||
|
if (!id) {
|
||||||
|
const item = _.isArray(section.schema)
|
||||||
|
? section.selectedSchema
|
||||||
|
: section.schema;
|
||||||
|
const settings = selectSettings(
|
||||||
|
Object.assign({ name: '' }, item),
|
||||||
|
section.pendingChanges ?? {},
|
||||||
|
section.saveError
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isSchemaFetching: isFetching,
|
||||||
|
isSchemaPopulated: isPopulated,
|
||||||
|
schemaError: error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
pendingChanges,
|
||||||
|
} = section;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
...settings,
|
||||||
|
pendingChanges,
|
||||||
|
item: settings.settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
pendingChanges,
|
||||||
|
} = section;
|
||||||
|
|
||||||
|
const item = section.items.find((i) => i.id === id)!;
|
||||||
|
const settings = selectSettings<T>(item, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
...settings,
|
||||||
|
item: settings.settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createProviderSettingsSelector<
|
||||||
|
T extends ModelBase,
|
||||||
|
S extends AppSectionProviderState<T> & AppSectionSchemaState<T>
|
||||||
|
>(sectionName: string) {
|
||||||
|
// @ts-expect-error - This isn't fully typed
|
||||||
|
return createSelector(
|
||||||
|
(_state: AppState, { id }: { id: number }) => id,
|
||||||
|
(state) => state.settings[sectionName] as S,
|
||||||
|
(id: number, section: S) => selector(id, section)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProviderSettingsSelectorHook<
|
||||||
|
T extends ModelBaseSetting,
|
||||||
|
S extends AppSectionProviderState<T> & AppSectionSchemaState<T>
|
||||||
|
>(sectionName: string, id: number | undefined) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings,
|
||||||
|
(state) => {
|
||||||
|
const sectionState = getSectionState(state, sectionName, false) as S;
|
||||||
|
|
||||||
|
return selector<T, S>(id, sectionState);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
@ -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;
|
Loading…
Reference in new issue