New: Auto tagging of artists

(cherry picked from commit 335fc05dd1595b6db912ebdde51ef4667963b37d)
This commit is contained in:
Mark McDowall 2022-12-05 22:58:53 -08:00 committed by Bogdan
parent d5ac008747
commit 362bd42cb8
61 changed files with 2843 additions and 124 deletions

View file

@ -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;
}

View 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;
}

View 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;

View 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
};

View 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);
}

View 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;

View 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>
);
}

View file

@ -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
};

View file

@ -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);
}

View 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;

View file

@ -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
};

View file

@ -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';
}
}

View 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;

View file

@ -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
};

View file

@ -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;

View file

@ -0,0 +1,5 @@
.specifications {
display: flex;
justify-content: center;
flex-wrap: wrap;
}

View 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;

View file

@ -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
};

View file

@ -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
};

View file

@ -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;

View file

@ -0,0 +1,5 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

View 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;

View file

@ -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;

View file

@ -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);

View 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;
}
.labels {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
pointer-events: all;
}
.tooltipLabel {
composes: label from '~Components/Label.css';
margin: 0;
border: none;
}

View 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;

View file

@ -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
};

View file

@ -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
};

View file

@ -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
};
}
);

View file

@ -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;

View 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
};

View file

@ -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>
);

View 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: []
})
}
};

View 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);
}
}
};

View file

@ -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,

View file

@ -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;

View file

@ -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) => {