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