mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-30 11:48:26 -07:00
New: Auto tagging of artists
(cherry picked from commit 335fc05dd1595b6db912ebdde51ef4667963b37d)
This commit is contained in:
parent
d5ac008747
commit
362bd42cb8
61 changed files with 2843 additions and 124 deletions
|
@ -37,6 +37,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.TEXT;
|
||||
case 'oAuth':
|
||||
return inputTypes.OAUTH;
|
||||
case 'rootFolder':
|
||||
return inputTypes.ROOT_FOLDER_SELECT;
|
||||
default:
|
||||
return inputTypes.TEXT;
|
||||
}
|
||||
|
|
38
frontend/src/Settings/Tags/AutoTagging/AutoTagging.css
Normal file
38
frontend/src/Settings/Tags/AutoTagging/AutoTagging.css
Normal file
|
@ -0,0 +1,38 @@
|
|||
.autoTagging {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.formats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
12
frontend/src/Settings/Tags/AutoTagging/AutoTagging.css.d.ts
vendored
Normal file
12
frontend/src/Settings/Tags/AutoTagging/AutoTagging.css.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'autoTagging': string;
|
||||
'cloneButton': string;
|
||||
'formats': string;
|
||||
'name': string;
|
||||
'nameContainer': string;
|
||||
'tooltipLabel': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
136
frontend/src/Settings/Tags/AutoTagging/AutoTagging.js
Normal file
136
frontend/src/Settings/Tags/AutoTagging/AutoTagging.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditAutoTaggingModal from './EditAutoTaggingModal';
|
||||
import styles from './AutoTagging.css';
|
||||
|
||||
export default function AutoTagging(props) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
tagList,
|
||||
specifications,
|
||||
isDeleting,
|
||||
onConfirmDeleteAutoTagging,
|
||||
onCloneAutoTaggingPress
|
||||
} = props;
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const onEditPress = useCallback(() => {
|
||||
setIsEditModalOpen(true);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onEditModalClose = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
setIsDeleteModalOpen(true);
|
||||
}, [setIsEditModalOpen, setIsDeleteModalOpen]);
|
||||
|
||||
const onDeleteModalClose = useCallback(() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
}, [setIsDeleteModalOpen]);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
onConfirmDeleteAutoTagging(id);
|
||||
}, [id, onConfirmDeleteAutoTagging]);
|
||||
|
||||
const onClonePress = useCallback(() => {
|
||||
onCloneAutoTaggingPress(id);
|
||||
}, [id, onCloneAutoTaggingPress]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.autoTagging}
|
||||
overlayContent={true}
|
||||
onPress={onEditPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneAutoTag')}
|
||||
name={icons.CLONE}
|
||||
onPress={onClonePress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{
|
||||
specifications.map((item, index) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
if (item.required) {
|
||||
kind = kinds.SUCCESS;
|
||||
}
|
||||
if (item.negate) {
|
||||
kind = kinds.DANGER;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={index}
|
||||
kind={kind}
|
||||
>
|
||||
{item.name}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditAutoTaggingModal
|
||||
id={id}
|
||||
isOpen={isEditModalOpen}
|
||||
onModalClose={onEditModalClose}
|
||||
onDeleteAutoTaggingPress={onDeletePress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteAutoTag')}
|
||||
message={translate('DeleteAutoTagHelpText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onDeleteModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
AutoTagging.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
specifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteAutoTagging: PropTypes.func.isRequired,
|
||||
onCloneAutoTaggingPress: PropTypes.func.isRequired
|
||||
};
|
21
frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css
Normal file
21
frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.autoTaggings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addAutoTagging {
|
||||
composes: autoTagging from '~./AutoTagging.css';
|
||||
|
||||
background-color: var(--cardAlternateBackgroundColor);
|
||||
color: var(--gray);
|
||||
text-align: center;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--cardCenterBackgroundColor);
|
||||
}
|
9
frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css.d.ts
vendored
Normal file
9
frontend/src/Settings/Tags/AutoTagging/AutoTaggings.css.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'addAutoTagging': string;
|
||||
'autoTaggings': string;
|
||||
'center': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
108
frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js
Normal file
108
frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import {
|
||||
cloneAutoTagging,
|
||||
deleteAutoTagging,
|
||||
fetchAutoTaggings,
|
||||
fetchRootFolders
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AutoTagging from './AutoTagging';
|
||||
import EditAutoTaggingModal from './EditAutoTaggingModal';
|
||||
import styles from './AutoTaggings.css';
|
||||
|
||||
export default function AutoTaggings() {
|
||||
const {
|
||||
error,
|
||||
items,
|
||||
isDeleting,
|
||||
isFetching,
|
||||
isPopulated
|
||||
} = useSelector(
|
||||
createSortedSectionSelector('settings.autoTaggings', sortByName)
|
||||
);
|
||||
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
const dispatch = useDispatch();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [tagsFromId, setTagsFromId] = useState(undefined);
|
||||
|
||||
const onClonePress = useCallback((id) => {
|
||||
dispatch(cloneAutoTagging({ id }));
|
||||
|
||||
setTagsFromId(id);
|
||||
setIsEditModalOpen(true);
|
||||
}, [dispatch, setIsEditModalOpen]);
|
||||
|
||||
const onEditPress = useCallback(() => {
|
||||
setIsEditModalOpen(true);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onEditModalClose = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onConfirmDelete = useCallback((id) => {
|
||||
dispatch(deleteAutoTagging({ id }));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAutoTaggings());
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('AutoTagging')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('AutoTaggingLoadError')}
|
||||
error={error}
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
>
|
||||
<div className={styles.autoTaggings}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AutoTagging
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
tagList={tagList}
|
||||
onConfirmDeleteAutoTagging={onConfirmDelete}
|
||||
onCloneAutoTaggingPress={onClonePress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addAutoTagging}
|
||||
onPress={onEditPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<EditAutoTaggingModal
|
||||
isOpen={isEditModalOpen}
|
||||
tagsFromId={tagsFromId}
|
||||
onModalClose={onEditModalClose}
|
||||
/>
|
||||
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
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,27 @@
|
|||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
justify-content: flex-end;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.addSpecification {
|
||||
composes: autoTagging from '~./AutoTagging.css';
|
||||
|
||||
background-color: var(--cardAlternateBackgroundColor);
|
||||
color: var(--gray);
|
||||
text-align: center;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--cardCenterBackgroundColor);
|
||||
}
|
10
frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts
vendored
Normal file
10
frontend/src/Settings/Tags/AutoTagging/EditAutoTaggingModalContent.css.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'addSpecification': string;
|
||||
'center': string;
|
||||
'deleteButton': string;
|
||||
'rightButtons': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,269 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
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 ?
|
||||
<div>
|
||||
{translate('AddAutoTagError')}
|
||||
</div> :
|
||||
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,44 @@
|
|||
.specification {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
@add-mixin cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@add-mixin linkOverlay;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
font-weight: lighter;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.presetsMenu {
|
||||
composes: menu from '~Components/Menu/Menu.css';
|
||||
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.presetsMenuButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
&::after {
|
||||
margin-left: 5px;
|
||||
content: '\25BE';
|
||||
}
|
||||
}
|
13
frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css.d.ts
vendored
Normal file
13
frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationItem.css.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'name': string;
|
||||
'overlay': string;
|
||||
'presetsMenu': string;
|
||||
'presetsMenuButton': string;
|
||||
'specification': string;
|
||||
'underlay': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,101 @@
|
|||
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,25 @@
|
|||
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,5 @@
|
|||
.specifications {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
7
frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css.d.ts
vendored
Normal file
7
frontend/src/Settings/Tags/AutoTagging/Specifications/AddSpecificationModalContent.css.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'specifications': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,106 @@
|
|||
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 ?
|
||||
<div>
|
||||
{translate('AddConditionError')}
|
||||
</div> :
|
||||
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,34 @@
|
|||
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,36 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditSpecificationModalContent from './EditSpecificationModalContent';
|
||||
|
||||
function EditSpecificationModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onWrappedModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'settings.autoTaggingSpecifications' }));
|
||||
onModalClose();
|
||||
}, [onModalClose, dispatch]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditSpecificationModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onWrappedModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditSpecificationModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditSpecificationModal;
|
|
@ -0,0 +1,5 @@
|
|||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
7
frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css.d.ts
vendored
Normal file
7
frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.css.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteButton': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,190 @@
|
|||
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')} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
||||
</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,78 @@
|
|||
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);
|
|
@ -0,0 +1,38 @@
|
|||
.autoTagging {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.labels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
12
frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css.d.ts
vendored
Normal file
12
frontend/src/Settings/Tags/AutoTagging/Specifications/Specification.css.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'autoTagging': string;
|
||||
'cloneButton': string;
|
||||
'labels': string;
|
||||
'name': string;
|
||||
'nameContainer': string;
|
||||
'tooltipLabel': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,122 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditSpecificationModal from './EditSpecificationModal';
|
||||
import styles from './Specification.css';
|
||||
|
||||
export default function Specification(props) {
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
required,
|
||||
negate,
|
||||
onConfirmDeleteSpecification,
|
||||
onCloneSpecificationPress
|
||||
} = props;
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const onEditPress = useCallback(() => {
|
||||
setIsEditModalOpen(true);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onEditModalClose = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
setIsDeleteModalOpen(true);
|
||||
}, [setIsEditModalOpen, setIsDeleteModalOpen]);
|
||||
|
||||
const onDeleteModalClose = useCallback(() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
}, [setIsDeleteModalOpen]);
|
||||
|
||||
const onConfirmDelete = useCallback(() => {
|
||||
onConfirmDeleteSpecification(id);
|
||||
}, [id, onConfirmDeleteSpecification]);
|
||||
|
||||
const onClonePress = useCallback(() => {
|
||||
onCloneSpecificationPress(id);
|
||||
}, [id, onCloneSpecificationPress]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.autoTagging}
|
||||
overlayContent={true}
|
||||
onPress={onEditPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('Clone')}
|
||||
name={icons.CLONE}
|
||||
onPress={onClonePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.labels}>
|
||||
<Label kind={kinds.DEFAULT}>
|
||||
{implementationName}
|
||||
</Label>
|
||||
|
||||
{
|
||||
negate ?
|
||||
<Label kind={kinds.DANGER}>
|
||||
{translate('Negated')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
required ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('Required')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditSpecificationModal
|
||||
id={id}
|
||||
isOpen={isEditModalOpen}
|
||||
onModalClose={onEditModalClose}
|
||||
onDeleteSpecificationPress={onDeletePress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteSpecification')}
|
||||
message={translate('DeleteSpecificationHelpText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onDeleteModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Specification.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
negate: PropTypes.bool.isRequired,
|
||||
required: PropTypes.bool.isRequired,
|
||||
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteSpecification: PropTypes.func.isRequired,
|
||||
onCloneSpecificationPress: PropTypes.func.isRequired
|
||||
};
|
|
@ -23,6 +23,7 @@ function TagDetailsModalContent(props) {
|
|||
releaseProfiles,
|
||||
indexers,
|
||||
downloadClients,
|
||||
autoTags,
|
||||
onModalClose,
|
||||
onDeleteTagPress
|
||||
} = props;
|
||||
|
@ -197,6 +198,22 @@ function TagDetailsModalContent(props) {
|
|||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
autoTags.length ?
|
||||
<FieldSet legend={translate('AutoTagging')}>
|
||||
{
|
||||
autoTags.map((item) => {
|
||||
return (
|
||||
<div key={item.id}>
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</FieldSet> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
@ -232,6 +249,7 @@ TagDetailsModalContent.propTypes = {
|
|||
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
|
||||
};
|
||||
|
|
|
@ -85,6 +85,14 @@ function createMatchingDownloadClientsSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
function createMatchingAutoTagsSelector() {
|
||||
return createSelector(
|
||||
(state, { autoTagIds }) => autoTagIds,
|
||||
(state) => state.settings.autoTaggings.items,
|
||||
findMatchingItems
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMatchingArtistSelector(),
|
||||
|
@ -94,7 +102,8 @@ function createMapStateToProps() {
|
|||
createMatchingReleaseProfilesSelector(),
|
||||
createMatchingIndexersSelector(),
|
||||
createMatchingDownloadClientsSelector(),
|
||||
(artist, delayProfiles, importLists, notifications, releaseProfiles, indexers, downloadClients) => {
|
||||
createMatchingAutoTagsSelector(),
|
||||
(artist, delayProfiles, importLists, notifications, releaseProfiles, indexers, downloadClients, autoTags) => {
|
||||
return {
|
||||
artist,
|
||||
delayProfiles,
|
||||
|
@ -102,7 +111,8 @@ function createMapStateToProps() {
|
|||
notifications,
|
||||
releaseProfiles,
|
||||
indexers,
|
||||
downloadClients
|
||||
downloadClients,
|
||||
autoTags
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@ 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 {
|
||||
|
@ -57,9 +58,10 @@ class Tag extends Component {
|
|||
importListIds,
|
||||
notificationIds,
|
||||
restrictionIds,
|
||||
artistIds,
|
||||
indexerIds,
|
||||
downloadClientIds
|
||||
downloadClientIds,
|
||||
autoTagIds,
|
||||
artistIds
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -72,9 +74,10 @@ class Tag extends Component {
|
|||
importListIds.length ||
|
||||
notificationIds.length ||
|
||||
restrictionIds.length ||
|
||||
artistIds.length ||
|
||||
indexerIds.length ||
|
||||
downloadClientIds.length
|
||||
downloadClientIds.length ||
|
||||
autoTagIds.length ||
|
||||
artistIds.length
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -88,63 +91,56 @@ class Tag extends Component {
|
|||
</div>
|
||||
|
||||
{
|
||||
isTagUsed &&
|
||||
isTagUsed ?
|
||||
<div>
|
||||
{
|
||||
artistIds.length ?
|
||||
<div>
|
||||
{artistIds.length} artists
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<TagInUse
|
||||
label={translate('Artist')}
|
||||
labelPlural={translate('Artists')}
|
||||
count={artistIds.length}
|
||||
/>
|
||||
|
||||
{
|
||||
delayProfileIds.length ?
|
||||
<div>
|
||||
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<TagInUse
|
||||
label={translate('DelayProfile')}
|
||||
labelPlural={translate('DelayProfiles')}
|
||||
count={delayProfileIds.length}
|
||||
/>
|
||||
|
||||
{
|
||||
importListIds.length ?
|
||||
<div>
|
||||
{importListIds.length} import list{importListIds.length > 1 && 's'}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<TagInUse
|
||||
label={translate('ImportList')}
|
||||
labelPlural={translate('ImportLists')}
|
||||
count={importListIds.length}
|
||||
/>
|
||||
|
||||
{
|
||||
notificationIds.length ?
|
||||
<div>
|
||||
{notificationIds.length} connection{notificationIds.length > 1 && 's'}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<TagInUse
|
||||
label={translate('Connection')}
|
||||
labelPlural={translate('Connections')}
|
||||
count={notificationIds.length}
|
||||
/>
|
||||
|
||||
{
|
||||
restrictionIds.length ?
|
||||
<div>
|
||||
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
{
|
||||
indexerIds.length ?
|
||||
<div>
|
||||
{indexerIds.length} indexer{indexerIds.length > 1 && 's'}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<TagInUse
|
||||
label={translate('ReleaseProfile')}
|
||||
labelPlural={translate('ReleaseProfiles')}
|
||||
count={restrictionIds.length}
|
||||
/>
|
||||
|
||||
{
|
||||
downloadClientIds.length ?
|
||||
<div>
|
||||
{downloadClientIds.length} download client{indexerIds.length > 1 && 's'}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<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
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -164,6 +160,7 @@ class Tag extends Component {
|
|||
restrictionIds={restrictionIds}
|
||||
indexerIds={indexerIds}
|
||||
downloadClientIds={downloadClientIds}
|
||||
autoTagIds={autoTagIds}
|
||||
isOpen={isDetailsModalOpen}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
onDeleteTagPress={this.onDeleteTagPress}
|
||||
|
@ -173,7 +170,7 @@ class Tag extends Component {
|
|||
isOpen={isDeleteTagModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteTag')}
|
||||
message={translate('DeleteTagMessageText', [label])}
|
||||
message={translate('DeleteTagMessageText', { label })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteTag}
|
||||
onCancel={this.onDeleteTagModalClose}
|
||||
|
@ -190,9 +187,10 @@ Tag.propTypes = {
|
|||
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
downloadClientIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onConfirmDeleteTag: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
@ -201,9 +199,10 @@ Tag.defaultProps = {
|
|||
importListIds: [],
|
||||
notificationIds: [],
|
||||
restrictionIds: [],
|
||||
artistIds: [],
|
||||
indexerIds: [],
|
||||
downloadClientIds: []
|
||||
downloadClientIds: [],
|
||||
autoTagIds: [],
|
||||
artistIds: []
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
|
34
frontend/src/Settings/Tags/TagInUse.js
Normal file
34
frontend/src/Settings/Tags/TagInUse.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default function TagInUse(props) {
|
||||
const {
|
||||
label,
|
||||
labelPlural,
|
||||
count
|
||||
} = props;
|
||||
|
||||
if (count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count > 1 && labelPlural ) {
|
||||
return (
|
||||
<div>
|
||||
{count} {labelPlural.toLowerCase()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{count} {label.toLowerCase()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TagInUse.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
labelPlural: PropTypes.string,
|
||||
count: PropTypes.number.isRequired
|
||||
};
|
|
@ -3,6 +3,7 @@ import PageContent from 'Components/Page/PageContent';
|
|||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AutoTaggings from './AutoTagging/AutoTaggings';
|
||||
import TagsConnector from './TagsConnector';
|
||||
|
||||
function TagSettings() {
|
||||
|
@ -14,6 +15,7 @@ function TagSettings() {
|
|||
|
||||
<PageContentBody>
|
||||
<TagsConnector />
|
||||
<AutoTaggings />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
193
frontend/src/Store/Actions/Settings/autoTaggingSpecifications.js
Normal file
193
frontend/src/Store/Actions/Settings/autoTaggingSpecifications.js
Normal file
|
@ -0,0 +1,193 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getNextId from 'Utilities/State/getNextId';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import { removeItem, set, update, updateItem } from '../baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.autoTaggingSpecifications';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_AUTO_TAGGING_SPECIFICATIONS = 'settings/autoTaggingSpecifications/fetchAutoTaggingSpecifications';
|
||||
export const FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA = 'settings/autoTaggingSpecifications/fetchAutoTaggingSpecificationSchema';
|
||||
export const SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA = 'settings/autoTaggingSpecifications/selectAutoTaggingSpecificationSchema';
|
||||
export const SET_AUTO_TAGGING_SPECIFICATION_VALUE = 'settings/autoTaggingSpecifications/setAutoTaggingSpecificationValue';
|
||||
export const SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE = 'settings/autoTaggingSpecifications/setAutoTaggingSpecificationFieldValue';
|
||||
export const SAVE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/saveAutoTaggingSpecification';
|
||||
export const DELETE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/deleteAutoTaggingSpecification';
|
||||
export const DELETE_ALL_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/deleteAllAutoTaggingSpecification';
|
||||
export const CLONE_AUTO_TAGGING_SPECIFICATION = 'settings/autoTaggingSpecifications/cloneAutoTaggingSpecification';
|
||||
export const CLEAR_AUTO_TAGGING_SPECIFICATIONS = 'settings/autoTaggingSpecifications/clearAutoTaggingSpecifications';
|
||||
export const CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING = 'settings/autoTaggingSpecifications/clearAutoTaggingSpecificationPending';
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchAutoTaggingSpecifications = createThunk(FETCH_AUTO_TAGGING_SPECIFICATIONS);
|
||||
export const fetchAutoTaggingSpecificationSchema = createThunk(FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA);
|
||||
export const selectAutoTaggingSpecificationSchema = createAction(SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA);
|
||||
|
||||
export const saveAutoTaggingSpecification = createThunk(SAVE_AUTO_TAGGING_SPECIFICATION);
|
||||
export const deleteAutoTaggingSpecification = createThunk(DELETE_AUTO_TAGGING_SPECIFICATION);
|
||||
export const deleteAllAutoTaggingSpecification = createThunk(DELETE_ALL_AUTO_TAGGING_SPECIFICATION);
|
||||
|
||||
export const setAutoTaggingSpecificationValue = createAction(SET_AUTO_TAGGING_SPECIFICATION_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const setAutoTaggingSpecificationFieldValue = createAction(SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneAutoTaggingSpecification = createAction(CLONE_AUTO_TAGGING_SPECIFICATION);
|
||||
|
||||
export const clearAutoTaggingSpecification = createAction(CLEAR_AUTO_TAGGING_SPECIFICATIONS);
|
||||
|
||||
export const clearAutoTaggingSpecificationPending = createThunk(CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: [],
|
||||
selectedSchema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_AUTO_TAGGING_SPECIFICATION_SCHEMA]: createFetchSchemaHandler(section, '/autoTagging/schema'),
|
||||
|
||||
[FETCH_AUTO_TAGGING_SPECIFICATIONS]: (getState, payload, dispatch) => {
|
||||
let tags = [];
|
||||
if (payload.id) {
|
||||
const cfState = getSectionState(getState(), 'settings.autoTaggings', true);
|
||||
const cf = cfState.items[cfState.itemMap[payload.id]];
|
||||
tags = cf.specifications.map((tag, i) => {
|
||||
return {
|
||||
id: i + 1,
|
||||
...tag
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
update({ section, data: tags }),
|
||||
set({
|
||||
section,
|
||||
isPopulated: true
|
||||
})
|
||||
]));
|
||||
},
|
||||
|
||||
[SAVE_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => {
|
||||
const {
|
||||
id,
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
|
||||
|
||||
// we have to set id since not actually posting to server yet
|
||||
if (!saveData.id) {
|
||||
saveData.id = getNextId(getState().settings.autoTaggingSpecifications.items);
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
updateItem({ section, ...saveData }),
|
||||
set({
|
||||
section,
|
||||
pendingChanges: {}
|
||||
})
|
||||
]));
|
||||
},
|
||||
|
||||
[DELETE_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => {
|
||||
const id = payload.id;
|
||||
return dispatch(removeItem({ section, id }));
|
||||
},
|
||||
|
||||
[DELETE_ALL_AUTO_TAGGING_SPECIFICATION]: (getState, payload, dispatch) => {
|
||||
return dispatch(set({
|
||||
section,
|
||||
items: []
|
||||
}));
|
||||
},
|
||||
|
||||
[CLEAR_AUTO_TAGGING_SPECIFICATION_PENDING]: (getState, payload, dispatch) => {
|
||||
return dispatch(set({
|
||||
section,
|
||||
pendingChanges: {}
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_AUTO_TAGGING_SPECIFICATION_VALUE]: createSetSettingValueReducer(section),
|
||||
[SET_AUTO_TAGGING_SPECIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||
|
||||
[SELECT_AUTO_TAGGING_SPECIFICATION_SCHEMA]: (state, { payload }) => {
|
||||
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||
return selectedSchema;
|
||||
});
|
||||
},
|
||||
|
||||
[CLONE_AUTO_TAGGING_SPECIFICATION]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const items = newState.items;
|
||||
const item = items.find((i) => i.id === id);
|
||||
const newId = getNextId(newState.items);
|
||||
const newItem = {
|
||||
...item,
|
||||
id: newId,
|
||||
name: `${item.name} - Copy`
|
||||
};
|
||||
newState.items = [...items, newItem];
|
||||
newState.itemMap[newId] = newState.items.length - 1;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
},
|
||||
|
||||
[CLEAR_AUTO_TAGGING_SPECIFICATIONS]: createClearReducer(section, {
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
})
|
||||
}
|
||||
};
|
109
frontend/src/Store/Actions/Settings/autoTaggings.js
Normal file
109
frontend/src/Store/Actions/Settings/autoTaggings.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { set } from 'Store/Actions/baseActions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.autoTaggings';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_AUTO_TAGGINGS = 'settings/autoTaggings/fetchAutoTaggings';
|
||||
export const SAVE_AUTO_TAGGING = 'settings/autoTaggings/saveAutoTagging';
|
||||
export const DELETE_AUTO_TAGGING = 'settings/autoTaggings/deleteAutoTagging';
|
||||
export const SET_AUTO_TAGGING_VALUE = 'settings/autoTaggings/setAutoTaggingValue';
|
||||
export const CLONE_AUTO_TAGGING = 'settings/autoTaggings/cloneAutoTagging';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchAutoTaggings = createThunk(FETCH_AUTO_TAGGINGS);
|
||||
export const saveAutoTagging = createThunk(SAVE_AUTO_TAGGING);
|
||||
export const deleteAutoTagging = createThunk(DELETE_AUTO_TAGGING);
|
||||
|
||||
export const setAutoTaggingValue = createAction(SET_AUTO_TAGGING_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneAutoTagging = createAction(CLONE_AUTO_TAGGING);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
schema: {
|
||||
removeTagsAutomatically: false,
|
||||
tags: []
|
||||
},
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_AUTO_TAGGINGS]: createFetchHandler(section, '/autoTagging'),
|
||||
|
||||
[DELETE_AUTO_TAGGING]: createRemoveItemHandler(section, '/autoTagging'),
|
||||
|
||||
[SAVE_AUTO_TAGGING]: (getState, payload, dispatch) => {
|
||||
// move the format tags in as a pending change
|
||||
const state = getState();
|
||||
const pendingChanges = state.settings.autoTaggings.pendingChanges;
|
||||
pendingChanges.specifications = state.settings.autoTaggingSpecifications.items;
|
||||
dispatch(set({
|
||||
section,
|
||||
pendingChanges
|
||||
}));
|
||||
|
||||
createSaveProviderHandler(section, '/autoTagging')(getState, payload, dispatch);
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_AUTO_TAGGING_VALUE]: createSetSettingValueReducer(section),
|
||||
|
||||
[CLONE_AUTO_TAGGING]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const item = newState.items.find((i) => i.id === id);
|
||||
const pendingChanges = { ...item, id: 0 };
|
||||
delete pendingChanges.id;
|
||||
|
||||
pendingChanges.name = `${pendingChanges.name} - Copy`;
|
||||
newState.pendingChanges = pendingChanges;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
|
@ -1,6 +1,8 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import autoTaggings from './Settings/autoTaggings';
|
||||
import autoTaggingSpecifications from './Settings/autoTaggingSpecifications';
|
||||
import customFormats from './Settings/customFormats';
|
||||
import customFormatSpecifications from './Settings/customFormatSpecifications';
|
||||
import delayProfiles from './Settings/delayProfiles';
|
||||
|
@ -26,6 +28,8 @@ import remotePathMappings from './Settings/remotePathMappings';
|
|||
import rootFolders from './Settings/rootFolders';
|
||||
import ui from './Settings/ui';
|
||||
|
||||
export * from './Settings/autoTaggingSpecifications';
|
||||
export * from './Settings/autoTaggings';
|
||||
export * from './Settings/customFormatSpecifications.js';
|
||||
export * from './Settings/customFormats';
|
||||
export * from './Settings/delayProfiles';
|
||||
|
@ -61,7 +65,8 @@ export const section = 'settings';
|
|||
|
||||
export const defaultState = {
|
||||
advancedSettings: false,
|
||||
|
||||
autoTaggingSpecifications: autoTaggingSpecifications.defaultState,
|
||||
autoTaggings: autoTaggings.defaultState,
|
||||
customFormatSpecifications: customFormatSpecifications.defaultState,
|
||||
customFormats: customFormats.defaultState,
|
||||
delayProfiles: delayProfiles.defaultState,
|
||||
|
@ -106,6 +111,8 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
|
|||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
...autoTaggingSpecifications.actionHandlers,
|
||||
...autoTaggings.actionHandlers,
|
||||
...customFormatSpecifications.actionHandlers,
|
||||
...customFormats.actionHandlers,
|
||||
...delayProfiles.actionHandlers,
|
||||
|
@ -141,6 +148,8 @@ export const reducers = createHandleActions({
|
|||
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
||||
},
|
||||
|
||||
...autoTaggingSpecifications.reducers,
|
||||
...autoTaggings.reducers,
|
||||
...customFormatSpecifications.reducers,
|
||||
...customFormats.reducers,
|
||||
...delayProfiles.reducers,
|
||||
|
|
|
@ -2,62 +2,70 @@ import _ from 'lodash';
|
|||
import { createSelector } from 'reselect';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
|
||||
function createProviderSettingsSelector(sectionName) {
|
||||
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) => {
|
||||
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
|
||||
};
|
||||
}
|
||||
(id, section) => selector(id, section)
|
||||
);
|
||||
}
|
||||
|
||||
export function createProviderSettingsSelectorHook(sectionName, id) {
|
||||
return createSelector(
|
||||
(state) => state.settings[sectionName],
|
||||
(section) => selector(id, section)
|
||||
);
|
||||
}
|
||||
|
||||
export default createProviderSettingsSelector;
|
||||
|
|
|
@ -29,6 +29,10 @@ export default function translate(
|
|||
) {
|
||||
const translation = translations[key] || key;
|
||||
|
||||
if (!(key in translations)) {
|
||||
console.warn(`MISSING ${key}`);
|
||||
}
|
||||
|
||||
if (tokens) {
|
||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||
Object.values(tokens).forEach((value, index) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue