New: Import List Exclusions (#608)
* New: Import List Exclusions * Fixed: ImportExclusion ForeignId Checks, Unique. RefreshArtist Duplicate * Fixed: Copy/Paste typospull/6/head
parent
b9cc94aa46
commit
42c16c227e
@ -0,0 +1,27 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector';
|
||||||
|
|
||||||
|
function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<EditImportListExclusionModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListExclusionModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditImportListExclusionModal;
|
@ -0,0 +1,43 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditImportListExclusionModal from './EditImportListExclusionModal';
|
||||||
|
|
||||||
|
function mapStateToProps() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearPendingChanges
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditImportListExclusionModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.clearPendingChanges({ section: 'settings.importListExclusions' });
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditImportListExclusionModal
|
||||||
|
{...this.props}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListExclusionModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector);
|
@ -0,0 +1,11 @@
|
|||||||
|
.body {
|
||||||
|
composes: modalBody from 'Components/Modal/ModalBody.css';
|
||||||
|
|
||||||
|
flex: 1 1 430px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
composes: button from 'Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import { stringSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import styles from './EditImportListExclusionModalContent.css';
|
||||||
|
|
||||||
|
function EditImportListExclusionModalContent(props) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item,
|
||||||
|
onInputChange,
|
||||||
|
onSavePress,
|
||||||
|
onModalClose,
|
||||||
|
onDeleteImportListExclusionPress,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
artistName,
|
||||||
|
foreignId
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? 'Edit Import List Exclusion' : 'Add Import List Exclusion'}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody className={styles.body}>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to add a new import list exclusion, please try again.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error &&
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Artist Name</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="artistName"
|
||||||
|
helpText="The name of the artist to exclude (can be anything meaningful)"
|
||||||
|
{...artistName}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Musicbrainz Id</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="foreignId"
|
||||||
|
helpText="The Musicbrainz Id of the artist to exclude"
|
||||||
|
{...foreignId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteImportListExclusionPress}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportListExclusionShape = {
|
||||||
|
artistName: PropTypes.shape(stringSettingShape).isRequired,
|
||||||
|
foreignId: PropTypes.shape(stringSettingShape).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
EditImportListExclusionModalContent.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.shape(ImportListExclusionShape).isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onDeleteImportListExclusionPress: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditImportListExclusionModalContent;
|
@ -0,0 +1,118 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import { setImportListExclusionValue, saveImportListExclusion } from 'Store/Actions/settingsActions';
|
||||||
|
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
|
||||||
|
|
||||||
|
const newImportListExclusion = {
|
||||||
|
artistName: '',
|
||||||
|
foreignId: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
function createImportListExclusionSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { id }) => id,
|
||||||
|
(state) => state.settings.importListExclusions,
|
||||||
|
(id, importListExclusions) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
pendingChanges,
|
||||||
|
items
|
||||||
|
} = importListExclusions;
|
||||||
|
|
||||||
|
const mapping = id ? _.find(items, { id }) : newImportListExclusion;
|
||||||
|
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item: settings.settings,
|
||||||
|
...settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createImportListExclusionSelector(),
|
||||||
|
(importListExclusion) => {
|
||||||
|
return {
|
||||||
|
...importListExclusion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setImportListExclusionValue,
|
||||||
|
saveImportListExclusion
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditImportListExclusionModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.id) {
|
||||||
|
Object.keys(newImportListExclusion).forEach((name) => {
|
||||||
|
this.props.setImportListExclusionValue({
|
||||||
|
name,
|
||||||
|
value: newImportListExclusion[name]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setImportListExclusionValue({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.saveImportListExclusion({ id: this.props.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditImportListExclusionModalContent
|
||||||
|
{...this.props}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListExclusionModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
setImportListExclusionValue: PropTypes.func.isRequired,
|
||||||
|
saveImportListExclusion: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListExclusionModalContentConnector);
|
@ -0,0 +1,23 @@
|
|||||||
|
.importListExclusion {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
height: 30px;
|
||||||
|
border-bottom: 1px solid $borderColor;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artistName {
|
||||||
|
flex: 0 0 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foreignId {
|
||||||
|
flex: 0 0 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
||||||
|
import styles from './ImportListExclusion.css';
|
||||||
|
|
||||||
|
class ImportListExclusion extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditImportListExclusionModalOpen: false,
|
||||||
|
isDeleteImportListExclusionModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditImportListExclusionPress = () => {
|
||||||
|
this.setState({ isEditImportListExclusionModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditImportListExclusionModalClose = () => {
|
||||||
|
this.setState({ isEditImportListExclusionModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteImportListExclusionPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditImportListExclusionModalOpen: false,
|
||||||
|
isDeleteImportListExclusionModalOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteImportListExclusionModalClose = () => {
|
||||||
|
this.setState({ isDeleteImportListExclusionModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDeleteImportListExclusion = () => {
|
||||||
|
this.props.onConfirmDeleteImportListExclusion(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
artistName,
|
||||||
|
foreignId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.importListExclusion,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.artistName}>{artistName}</div>
|
||||||
|
<div className={styles.foreignId}>{foreignId}</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Link
|
||||||
|
onPress={this.onEditImportListExclusionPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.EDIT} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditImportListExclusionModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isEditImportListExclusionModalOpen}
|
||||||
|
onModalClose={this.onEditImportListExclusionModalClose}
|
||||||
|
onDeleteImportListExclusionPress={this.onDeleteImportListExclusionPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isDeleteImportListExclusionModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete Import List Exclusion"
|
||||||
|
message="Are you sure you want to delete this import list exclusion?"
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={this.onConfirmDeleteImportListExclusion}
|
||||||
|
onCancel={this.onDeleteImportListExclusionModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportListExclusion.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
artistName: PropTypes.string.isRequired,
|
||||||
|
foreignId: PropTypes.string.isRequired,
|
||||||
|
onConfirmDeleteImportListExclusion: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportListExclusion.defaultProps = {
|
||||||
|
// The drag preview will not connect the drag handle.
|
||||||
|
connectDragSource: (node) => node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportListExclusion;
|
@ -0,0 +1,23 @@
|
|||||||
|
.importListExclusionsHeader {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host {
|
||||||
|
flex: 0 0 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
flex: 0 0 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addImportListExclusion {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
text-align: center;
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import ImportListExclusion from './ImportListExclusion';
|
||||||
|
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
||||||
|
import styles from './ImportListExclusions.css';
|
||||||
|
|
||||||
|
class ImportListExclusions extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAddImportListExclusionModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAddImportListExclusionPress = () => {
|
||||||
|
this.setState({ isAddImportListExclusionModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.setState({ isAddImportListExclusionModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
onConfirmDeleteImportListExclusion,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend="Import List Exclusions">
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage="Unable to load Import List Exclusions"
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.importListExclusionsHeader}>
|
||||||
|
<div className={styles.host}>Name</div>
|
||||||
|
<div className={styles.path}>Foreign Id</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<ImportListExclusion
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
{...otherProps}
|
||||||
|
index={index}
|
||||||
|
onConfirmDeleteImportListExclusion={onConfirmDeleteImportListExclusion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.addImportListExclusion}>
|
||||||
|
<Link
|
||||||
|
className={styles.addButton}
|
||||||
|
onPress={this.onAddImportListExclusionPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.ADD} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditImportListExclusionModalConnector
|
||||||
|
isOpen={this.state.isAddImportListExclusionModalOpen}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportListExclusions.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onConfirmDeleteImportListExclusion: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportListExclusions;
|
@ -0,0 +1,59 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchImportListExclusions, deleteImportListExclusion } from 'Store/Actions/settingsActions';
|
||||||
|
import ImportListExclusions from './ImportListExclusions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.importListExclusions,
|
||||||
|
(importListExclusions) => {
|
||||||
|
return {
|
||||||
|
...importListExclusions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchImportListExclusions,
|
||||||
|
deleteImportListExclusion
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportListExclusionsConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchImportListExclusions();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onConfirmDeleteImportListExclusion = (id) => {
|
||||||
|
this.props.deleteImportListExclusion({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ImportListExclusions
|
||||||
|
{...this.state}
|
||||||
|
{...this.props}
|
||||||
|
onConfirmDeleteImportListExclusion={this.onConfirmDeleteImportListExclusion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportListExclusionsConnector.propTypes = {
|
||||||
|
fetchImportListExclusions: PropTypes.func.isRequired,
|
||||||
|
deleteImportListExclusion: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListExclusionsConnector);
|
@ -0,0 +1,69 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.importListExclusions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions';
|
||||||
|
export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion';
|
||||||
|
export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion';
|
||||||
|
export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS);
|
||||||
|
export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION);
|
||||||
|
export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION);
|
||||||
|
|
||||||
|
export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_IMPORT_LIST_EXCLUSIONS]: createFetchHandler(section, '/importlistexclusion'),
|
||||||
|
[SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'),
|
||||||
|
[DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
using Lidarr.Http;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListExclusionModule : LidarrRestModule<ImportListExclusionResource>
|
||||||
|
{
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
|
|
||||||
|
public ImportListExclusionModule(IImportListExclusionService importListExclusionService,
|
||||||
|
ImportListExclusionExistsValidator importListExclusionExistsValidator,
|
||||||
|
GuidValidator guidValidator)
|
||||||
|
{
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
|
|
||||||
|
GetResourceById = GetImportListExclusion;
|
||||||
|
GetResourceAll = GetImportListExclusions;
|
||||||
|
CreateResource = AddImportListExclusion;
|
||||||
|
UpdateResource = UpdateImportListExclusion;
|
||||||
|
DeleteResource = DeleteImportListExclusionResource;
|
||||||
|
|
||||||
|
SharedValidator.RuleFor(c => c.ForeignId).NotEmpty().SetValidator(guidValidator).SetValidator(importListExclusionExistsValidator);
|
||||||
|
SharedValidator.RuleFor(c => c.ArtistName).NotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImportListExclusionResource GetImportListExclusion(int id)
|
||||||
|
{
|
||||||
|
return _importListExclusionService.Get(id).ToResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ImportListExclusionResource> GetImportListExclusions()
|
||||||
|
{
|
||||||
|
return _importListExclusionService.All().ToResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AddImportListExclusion(ImportListExclusionResource resource)
|
||||||
|
{
|
||||||
|
var customFilter = _importListExclusionService.Add(resource.ToModel());
|
||||||
|
|
||||||
|
return customFilter.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateImportListExclusion(ImportListExclusionResource resource)
|
||||||
|
{
|
||||||
|
_importListExclusionService.Update(resource.ToModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteImportListExclusionResource(int id)
|
||||||
|
{
|
||||||
|
_importListExclusionService.Delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
using Lidarr.Http.REST;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListExclusionResource : RestResource
|
||||||
|
{
|
||||||
|
public string ForeignId { get; set; }
|
||||||
|
public string ArtistName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ImportListExclusionResourceMapper
|
||||||
|
{
|
||||||
|
public static ImportListExclusionResource ToResource(this ImportListExclusion model)
|
||||||
|
{
|
||||||
|
if (model == null) return null;
|
||||||
|
|
||||||
|
return new ImportListExclusionResource
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
ForeignId = model.ForeignId,
|
||||||
|
ArtistName = model.Name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImportListExclusion ToModel(this ImportListExclusionResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null) return null;
|
||||||
|
|
||||||
|
return new ImportListExclusion
|
||||||
|
{
|
||||||
|
Id = resource.Id,
|
||||||
|
ForeignId = resource.ForeignId,
|
||||||
|
Name = resource.ArtistName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ImportListExclusionResource> ToResource(this IEnumerable<ImportListExclusion> filters)
|
||||||
|
{
|
||||||
|
return filters.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.ValidationTests
|
||||||
|
{
|
||||||
|
public class GuidValidationFixture : CoreTest<GuidValidator>
|
||||||
|
{
|
||||||
|
private TestValidator<ImportListExclusion> _validator;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_validator = new TestValidator<ImportListExclusion>
|
||||||
|
{
|
||||||
|
v => v.RuleFor(s => s.ForeignId).SetValidator(Subject)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_be_valid_if_invalid_guid()
|
||||||
|
{
|
||||||
|
var listExclusion = Builder<ImportListExclusion>.CreateNew()
|
||||||
|
.With(s => s.ForeignId = "e1f1e33e-2e4c-4d43-b91b-7064068d328")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_validator.Validate(listExclusion).IsValid.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_be_valid_if_valid_guid()
|
||||||
|
{
|
||||||
|
var listExclusion = Builder<ImportListExclusion>.CreateNew()
|
||||||
|
.With(s => s.ForeignId = "e1f1e33e-2e4c-4d43-b91b-7064068d3283")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_validator.Validate(listExclusion).IsValid.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(027)]
|
||||||
|
public class add_import_exclusions : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Create.TableForModel("ImportListExclusions")
|
||||||
|
.WithColumn("ForeignId").AsString().NotNullable().Unique()
|
||||||
|
.WithColumn("Name").AsString().NotNullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public class ImportListExclusion : ModelBase
|
||||||
|
{
|
||||||
|
public string ForeignId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using FluentValidation.Validators;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public class ImportListExclusionExistsValidator : PropertyValidator
|
||||||
|
{
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
|
|
||||||
|
public ImportListExclusionExistsValidator(IImportListExclusionService importListExclusionService)
|
||||||
|
: base("This exclusion has already been added.")
|
||||||
|
{
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
if (context.PropertyValue == null) return true;
|
||||||
|
|
||||||
|
return (!_importListExclusionService.All().Exists(s => s.ForeignId == context.PropertyValue.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public interface IImportListExclusionRepository : IBasicRepository<ImportListExclusion>
|
||||||
|
{
|
||||||
|
ImportListExclusion FindByForeignId(string foreignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListExclusionRepository : BasicRepository<ImportListExclusion>, IImportListExclusionRepository
|
||||||
|
{
|
||||||
|
public ImportListExclusionRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||||
|
: base(database, eventAggregator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion FindByForeignId(string foreignId)
|
||||||
|
{
|
||||||
|
return Query.Where<ImportListExclusion>(m => m.ForeignId == foreignId).SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Music.Events;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public interface IImportListExclusionService
|
||||||
|
{
|
||||||
|
ImportListExclusion Add(ImportListExclusion importListExclusion);
|
||||||
|
List<ImportListExclusion> All();
|
||||||
|
void Delete(int id);
|
||||||
|
ImportListExclusion Get(int id);
|
||||||
|
ImportListExclusion FindByForeignId(string foreignId);
|
||||||
|
ImportListExclusion Update(ImportListExclusion importListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListExclusionService : IImportListExclusionService, IHandleAsync<ArtistDeletedEvent>
|
||||||
|
{
|
||||||
|
private readonly IImportListExclusionRepository _repo;
|
||||||
|
|
||||||
|
public ImportListExclusionService(IImportListExclusionRepository repo)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion Add(ImportListExclusion importListExclusion)
|
||||||
|
{
|
||||||
|
return _repo.Insert(importListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion Update(ImportListExclusion importListExclusion)
|
||||||
|
{
|
||||||
|
return _repo.Update(importListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
_repo.Delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion Get(int id)
|
||||||
|
{
|
||||||
|
return _repo.Get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion FindByForeignId(string foreignId)
|
||||||
|
{
|
||||||
|
return _repo.FindByForeignId(foreignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportListExclusion> All()
|
||||||
|
{
|
||||||
|
return _repo.All().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleAsync(ArtistDeletedEvent message)
|
||||||
|
{
|
||||||
|
if (!message.AddImportListExclusion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingExclusion = _repo.FindByForeignId(message.Artist.ForeignArtistId);
|
||||||
|
|
||||||
|
if (existingExclusion != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importExclusion = new ImportListExclusion
|
||||||
|
{
|
||||||
|
ForeignId = message.Artist.ForeignArtistId,
|
||||||
|
Name = message.Artist.Name
|
||||||
|
};
|
||||||
|
|
||||||
|
_repo.Insert(importExclusion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Validation
|
||||||
|
{
|
||||||
|
public class GuidValidator : PropertyValidator
|
||||||
|
{
|
||||||
|
public GuidValidator()
|
||||||
|
: base("String is not a valid Guid")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
if (context.PropertyValue == null) return false;
|
||||||
|
|
||||||
|
return Guid.TryParse(context.PropertyValue.ToString(), out Guid guidOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue