diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js
index 974d4b5b5..49f4463de 100644
--- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import split from 'Utilities/String/split';
import { icons, kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
@@ -65,7 +64,7 @@ class CustomFormat extends Component {
const {
id,
name,
- formatTags,
+ specifications,
isDeleting
} = this.props;
@@ -90,17 +89,17 @@ class CustomFormat extends Component {
{
- split(formatTags).map((item) => {
+ specifications.map((item, index) => {
if (!item) {
return null;
}
return (
);
})
@@ -138,7 +137,7 @@ class CustomFormat extends Component {
CustomFormat.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
- formatTags: PropTypes.string.isRequired,
+ specifications: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
onCloneCustomFormatPress: PropTypes.func.isRequired
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js
index 9bc6f90d5..2cc2a41ca 100644
--- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import sortByName from 'Utilities/Array/sortByName';
import { icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet';
import Card from 'Components/Card';
@@ -19,7 +18,8 @@ class CustomFormats extends Component {
super(props, context);
this.state = {
- isCustomFormatModalOpen: false
+ isCustomFormatModalOpen: false,
+ tagsFromId: undefined
};
}
@@ -28,7 +28,10 @@ class CustomFormats extends Component {
onCloneCustomFormatPress = (id) => {
this.props.onCloneCustomFormatPress(id);
- this.setState({ isCustomFormatModalOpen: true });
+ this.setState({
+ isCustomFormatModalOpen: true,
+ tagsFromId: id
+ });
}
onEditCustomFormatPress = () => {
@@ -36,7 +39,10 @@ class CustomFormats extends Component {
}
onModalClose = () => {
- this.setState({ isCustomFormatModalOpen: false });
+ this.setState({
+ isCustomFormatModalOpen: false,
+ tagsFromId: undefined
+ });
}
//
@@ -59,7 +65,7 @@ class CustomFormats extends Component {
>
{
- items.sort(sortByName).map((item) => {
+ items.map((item) => {
return (
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js
index ee5f820c4..bbbb57661 100644
--- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js
@@ -2,17 +2,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
+import sortByName from 'Utilities/Array/sortByName';
+import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import { fetchCustomFormats, deleteCustomFormat, cloneCustomFormat } from 'Store/Actions/settingsActions';
import CustomFormats from './CustomFormats';
function createMapStateToProps() {
return createSelector(
- (state) => state.settings.customFormats,
- (customFormats) => {
- return {
- ...customFormats
- };
- }
+ createSortedSectionSelector('settings.customFormats', sortByName),
+ (customFormats) => customFormats
);
}
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css
index a2b6014df..cdf21ca8a 100644
--- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css
@@ -3,3 +3,20 @@
margin-right: auto;
}
+
+.addSpecification {
+ composes: customFormat from '~./CustomFormat.css';
+
+ background-color: $cardAlternateBackgroundColor;
+ color: $gray;
+ text-align: center;
+ font-size: 45px;
+}
+
+.center {
+ display: inline-block;
+ padding: 5px 20px 0;
+ border: 1px solid $borderColor;
+ border-radius: 4px;
+ background-color: $white;
+}
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js
index 879c143aa..94518d4db 100644
--- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js
@@ -1,6 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { inputTypes, kinds } from 'Helpers/Props';
+import { icons, inputTypes, kinds } from 'Helpers/Props';
+import FieldSet from 'Components/FieldSet';
+import Card from 'Components/Card';
+import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -12,10 +15,43 @@ 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 Specification from './Specifications/Specification';
+import AddSpecificationModal from './Specifications/AddSpecificationModal';
+import EditSpecificationModalConnector from './Specifications/EditSpecificationModalConnector';
import styles from './EditCustomFormatModalContent.css';
class EditCustomFormatModalContent extends Component {
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isAddSpecificationModalOpen: false,
+ isEditSpecificationModalOpen: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onAddSpecificationPress = () => {
+ this.setState({ isAddSpecificationModalOpen: true });
+ }
+
+ onAddSpecificationModalClose = ({ specificationSelected = false } = {}) => {
+ this.setState({
+ isAddSpecificationModalOpen: false,
+ isEditSpecificationModalOpen: specificationSelected
+ });
+ }
+
+ onEditSpecificationModalClose = () => {
+ this.setState({ isEditSpecificationModalOpen: false });
+ }
+
//
// Render
@@ -26,17 +62,25 @@ class EditCustomFormatModalContent extends Component {
isSaving,
saveError,
item,
+ specificationsPopulated,
+ specifications,
onInputChange,
onSavePress,
onModalClose,
onDeleteCustomFormatPress,
+ onCloneSpecificationPress,
+ onConfirmDeleteSpecification,
...otherProps
} = this.props;
+ const {
+ isAddSpecificationModalOpen,
+ isEditSpecificationModalOpen
+ } = this.state;
+
const {
id,
- name,
- formatTags
+ name
} = item;
return (
@@ -59,37 +103,64 @@ class EditCustomFormatModalContent extends Component {
}
{
- !isFetching && !error &&
-
-
+ !isFetching && !error && specificationsPopulated &&
+
+
+
+
+
+
+
+
+
}
@@ -130,11 +201,15 @@ EditCustomFormatModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
+ specificationsPopulated: PropTypes.bool.isRequired,
+ specifications: PropTypes.arrayOf(PropTypes.object),
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onContentHeightChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
- onDeleteCustomFormatPress: PropTypes.func
+ onDeleteCustomFormatPress: PropTypes.func,
+ onCloneSpecificationPress: PropTypes.func.isRequired,
+ onConfirmDeleteSpecification: PropTypes.func.isRequired
};
export default EditCustomFormatModalContent;
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js
index c73fcf174..b3de4d440 100644
--- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js
@@ -3,17 +3,20 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
-import { setCustomFormatValue, saveCustomFormat } from 'Store/Actions/settingsActions';
+import { setCustomFormatValue, saveCustomFormat, fetchCustomFormatSpecifications, cloneCustomFormatSpecification, deleteCustomFormatSpecification } from 'Store/Actions/settingsActions';
import EditCustomFormatModalContent from './EditCustomFormatModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('customFormats'),
- (advancedSettings, customFormat) => {
+ (state) => state.settings.customFormatSpecifications,
+ (advancedSettings, customFormat, specifications) => {
return {
advancedSettings,
- ...customFormat
+ ...customFormat,
+ specificationsPopulated: specifications.isPopulated,
+ specifications: specifications.items
};
}
);
@@ -21,7 +24,10 @@ function createMapStateToProps() {
const mapDispatchToProps = {
setCustomFormatValue,
- saveCustomFormat
+ saveCustomFormat,
+ fetchCustomFormatSpecifications,
+ cloneCustomFormatSpecification,
+ deleteCustomFormatSpecification
};
class EditCustomFormatModalContentConnector extends Component {
@@ -29,6 +35,14 @@ class EditCustomFormatModalContentConnector extends Component {
//
// Lifecycle
+ componentDidMount() {
+ const {
+ id,
+ tagsFromId
+ } = this.props;
+ this.props.fetchCustomFormatSpecifications({ id: tagsFromId || id });
+ }
+
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
@@ -46,6 +60,14 @@ class EditCustomFormatModalContentConnector extends Component {
this.props.saveCustomFormat({ id: this.props.id });
}
+ onCloneSpecificationPress = (id) => {
+ this.props.cloneCustomFormatSpecification({ id });
+ }
+
+ onConfirmDeleteSpecification = (id) => {
+ this.props.deleteCustomFormatSpecification({ id });
+ }
+
//
// Render
@@ -55,6 +77,8 @@ class EditCustomFormatModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
+ onCloneSpecificationPress={this.onCloneSpecificationPress}
+ onConfirmDeleteSpecification={this.onConfirmDeleteSpecification}
/>
);
}
@@ -62,12 +86,16 @@ class EditCustomFormatModalContentConnector extends Component {
EditCustomFormatModalContentConnector.propTypes = {
id: PropTypes.number,
+ tagsFromId: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setCustomFormatValue: PropTypes.func.isRequired,
saveCustomFormat: PropTypes.func.isRequired,
+ fetchCustomFormatSpecifications: PropTypes.func.isRequired,
+ cloneCustomFormatSpecification: PropTypes.func.isRequired,
+ deleteCustomFormatSpecification: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.css b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.css
new file mode 100644
index 000000000..eabcae750
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.css
@@ -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';
+ }
+}
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.js
new file mode 100644
index 000000000..0e5c351bc
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.js
@@ -0,0 +1,110 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { sizes } from 'Helpers/Props';
+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 AddSpecificationPresetMenuItem from './AddSpecificationPresetMenuItem';
+import styles from './AddSpecificationItem.css';
+
+class AddSpecificationItem extends Component {
+
+ //
+ // Listeners
+
+ onSpecificationSelect = () => {
+ const {
+ implementation
+ } = this.props;
+
+ this.props.onSpecificationSelect({ implementation });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ implementation,
+ implementationName,
+ infoLink,
+ presets,
+ onSpecificationSelect
+ } = this.props;
+
+ const hasPresets = !!presets && !!presets.length;
+
+ return (
+
+
+
+
+
+ {implementationName}
+
+
+
+ {
+ hasPresets &&
+
+
+
+
+
+ }
+
+
+
+
+
+ );
+ }
+}
+
+AddSpecificationItem.propTypes = {
+ implementation: PropTypes.string.isRequired,
+ implementationName: PropTypes.string.isRequired,
+ infoLink: PropTypes.string.isRequired,
+ presets: PropTypes.arrayOf(PropTypes.object),
+ onSpecificationSelect: PropTypes.func.isRequired
+};
+
+export default AddSpecificationItem;
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.js
new file mode 100644
index 000000000..19d8a4335
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.js
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Modal from 'Components/Modal/Modal';
+import AddSpecificationModalContentConnector from './AddSpecificationModalContentConnector';
+
+function AddSpecificationModal({ isOpen, onModalClose, ...otherProps }) {
+ return (
+
+
+
+ );
+}
+
+AddSpecificationModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default AddSpecificationModal;
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.css
new file mode 100644
index 000000000..d51349ea9
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.css
@@ -0,0 +1,5 @@
+.specifications {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+}
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js
new file mode 100644
index 000000000..fa526a451
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js
@@ -0,0 +1,93 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { kinds } from 'Helpers/Props';
+import Alert from 'Components/Alert';
+import Button from 'Components/Link/Button';
+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 AddSpecificationItem from './AddSpecificationItem';
+import styles from './AddSpecificationModalContent.css';
+
+class AddSpecificationModalContent extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ isSchemaFetching,
+ isSchemaPopulated,
+ schemaError,
+ schema,
+ onSpecificationSelect,
+ onModalClose
+ } = this.props;
+
+ return (
+
+
+ Add Condition
+
+
+
+ {
+ isSchemaFetching &&
+
+ }
+
+ {
+ !isSchemaFetching && !!schemaError &&
+ Unable to add a new condition, please try again.
+ }
+
+ {
+ isSchemaPopulated && !schemaError &&
+
+
+
+ Radarr supports custom conditions against the following release properties
+ Visit github for more details
+
+
+
+ {
+ schema.map((specification) => {
+ return (
+
+ );
+ })
+ }
+
+
+
+ }
+
+
+
+
+
+ );
+ }
+}
+
+AddSpecificationModalContent.propTypes = {
+ isSchemaFetching: PropTypes.bool.isRequired,
+ isSchemaPopulated: PropTypes.bool.isRequired,
+ schemaError: PropTypes.object,
+ schema: PropTypes.arrayOf(PropTypes.object).isRequired,
+ onSpecificationSelect: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default AddSpecificationModalContent;
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContentConnector.js
new file mode 100644
index 000000000..79c992c9a
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContentConnector.js
@@ -0,0 +1,70 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { fetchCustomFormatSpecificationSchema, selectCustomFormatSpecificationSchema } from 'Store/Actions/settingsActions';
+import AddSpecificationModalContent from './AddSpecificationModalContent';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.settings.customFormatSpecifications,
+ (specifications) => {
+ const {
+ isSchemaFetching,
+ isSchemaPopulated,
+ schemaError,
+ schema
+ } = specifications;
+
+ return {
+ isSchemaFetching,
+ isSchemaPopulated,
+ schemaError,
+ schema
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ fetchCustomFormatSpecificationSchema,
+ selectCustomFormatSpecificationSchema
+};
+
+class AddSpecificationModalContentConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount() {
+ this.props.fetchCustomFormatSpecificationSchema();
+ }
+
+ //
+ // Listeners
+
+ onSpecificationSelect = ({ implementation, name }) => {
+ this.props.selectCustomFormatSpecificationSchema({ implementation, presetName: name });
+ this.props.onModalClose({ specificationSelected: true });
+ }
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+AddSpecificationModalContentConnector.propTypes = {
+ fetchCustomFormatSpecificationSchema: PropTypes.func.isRequired,
+ selectCustomFormatSpecificationSchema: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(AddSpecificationModalContentConnector);
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.js
new file mode 100644
index 000000000..e007d9e21
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.js
@@ -0,0 +1,49 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import MenuItem from 'Components/Menu/MenuItem';
+
+class AddSpecificationPresetMenuItem extends Component {
+
+ //
+ // Listeners
+
+ onPress = () => {
+ const {
+ name,
+ implementation
+ } = this.props;
+
+ this.props.onPress({
+ name,
+ implementation
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ name,
+ implementation,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+AddSpecificationPresetMenuItem.propTypes = {
+ name: PropTypes.string.isRequired,
+ implementation: PropTypes.string.isRequired,
+ onPress: PropTypes.func.isRequired
+};
+
+export default AddSpecificationPresetMenuItem;
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.js
new file mode 100644
index 000000000..5b312ecfc
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.js
@@ -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 EditSpecificationModalContentConnector from './EditSpecificationModalContentConnector';
+
+function EditSpecificationModal({ isOpen, onModalClose, ...otherProps }) {
+ return (
+
+
+
+ );
+}
+
+EditSpecificationModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default EditSpecificationModal;
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalConnector.js
new file mode 100644
index 000000000..996c49c97
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalConnector.js
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { clearPendingChanges } from 'Store/Actions/baseActions';
+import EditSpecificationModal from './EditSpecificationModal';
+
+function createMapDispatchToProps(dispatch, props) {
+ const section = 'settings.customFormatSpecifications';
+
+ return {
+ dispatchClearPendingChanges() {
+ dispatch(clearPendingChanges({ section }));
+ }
+ };
+}
+
+class EditSpecificationModalConnector extends Component {
+
+ //
+ // Listeners
+
+ onModalClose = () => {
+ this.props.dispatchClearPendingChanges();
+ this.props.onModalClose();
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ dispatchClearPendingChanges,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+EditSpecificationModalConnector.propTypes = {
+ onModalClose: PropTypes.func.isRequired,
+ dispatchClearPendingChanges: PropTypes.func.isRequired
+};
+
+export default connect(null, createMapDispatchToProps)(EditSpecificationModalConnector);
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.css b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.css
new file mode 100644
index 000000000..a2b6014df
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.css
@@ -0,0 +1,5 @@
+.deleteButton {
+ composes: button from '~Components/Link/Button.css';
+
+ margin-right: auto;
+}
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js
new file mode 100644
index 000000000..95c6ec54f
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js
@@ -0,0 +1,154 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { inputTypes, kinds } from 'Helpers/Props';
+import Alert from 'Components/Alert';
+import Link from 'Components/Link/Link';
+import Button from 'Components/Link/Button';
+import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
+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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
+import styles from './EditSpecificationModalContent.css';
+
+function EditSpecificationModalContent(props) {
+ const {
+ advancedSettings,
+ item,
+ onInputChange,
+ onFieldChange,
+ onCancelPress,
+ onSavePress,
+ onDeleteSpecificationPress,
+ ...otherProps
+ } = props;
+
+ const {
+ id,
+ implementationName,
+ name,
+ negate,
+ required,
+ fields
+ } = item;
+
+ return (
+
+
+ {`${id ? 'Edit' : 'Add'} Condition - ${implementationName}`}
+
+
+
+
+
+
+ {
+ id &&
+
+ }
+
+
+
+
+ Save
+
+
+
+ );
+}
+
+EditSpecificationModalContent.propTypes = {
+ advancedSettings: PropTypes.bool.isRequired,
+ item: PropTypes.object.isRequired,
+ onInputChange: PropTypes.func.isRequired,
+ onFieldChange: PropTypes.func.isRequired,
+ onCancelPress: PropTypes.func.isRequired,
+ onSavePress: PropTypes.func.isRequired,
+ onDeleteSpecificationPress: PropTypes.func
+};
+
+export default EditSpecificationModalContent;
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContentConnector.js
new file mode 100644
index 000000000..3aea8fc01
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContentConnector.js
@@ -0,0 +1,78 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
+import { setCustomFormatSpecificationValue, setCustomFormatSpecificationFieldValue, saveCustomFormatSpecification, clearCustomFormatSpecificationPending } from 'Store/Actions/settingsActions';
+import EditSpecificationModalContent from './EditSpecificationModalContent';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.settings.advancedSettings,
+ createProviderSettingsSelector('customFormatSpecifications'),
+ (advancedSettings, specification) => {
+ return {
+ advancedSettings,
+ ...specification
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ setCustomFormatSpecificationValue,
+ setCustomFormatSpecificationFieldValue,
+ saveCustomFormatSpecification,
+ clearCustomFormatSpecificationPending
+};
+
+class EditSpecificationModalContentConnector extends Component {
+
+ //
+ // Listeners
+
+ onInputChange = ({ name, value }) => {
+ this.props.setCustomFormatSpecificationValue({ name, value });
+ }
+
+ onFieldChange = ({ name, value }) => {
+ this.props.setCustomFormatSpecificationFieldValue({ name, value });
+ }
+
+ onCancelPress = () => {
+ this.props.clearCustomFormatSpecificationPending();
+ this.props.onModalClose();
+ }
+
+ onSavePress = () => {
+ this.props.saveCustomFormatSpecification({ id: this.props.id });
+ this.props.onModalClose();
+ }
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+EditSpecificationModalContentConnector.propTypes = {
+ id: PropTypes.number,
+ item: PropTypes.object.isRequired,
+ setCustomFormatSpecificationValue: PropTypes.func.isRequired,
+ setCustomFormatSpecificationFieldValue: PropTypes.func.isRequired,
+ clearCustomFormatSpecificationPending: PropTypes.func.isRequired,
+ saveCustomFormatSpecification: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(EditSpecificationModalContentConnector);
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.css b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.css
new file mode 100644
index 000000000..f05c942b0
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.css
@@ -0,0 +1,38 @@
+.customFormat {
+ 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;
+}
diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js
new file mode 100644
index 000000000..55dbec520
--- /dev/null
+++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js
@@ -0,0 +1,145 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { icons, kinds } from 'Helpers/Props';
+import Card from 'Components/Card';
+import Label from 'Components/Label';
+import IconButton from 'Components/Link/IconButton';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
+import EditSpecificationModalConnector from './EditSpecificationModal';
+import styles from './Specification.css';
+
+class Specification extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isEditSpecificationModalOpen: false,
+ isDeleteSpecificationModalOpen: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onEditSpecificationPress = () => {
+ this.setState({ isEditSpecificationModalOpen: true });
+ }
+
+ onEditSpecificationModalClose = () => {
+ this.setState({ isEditSpecificationModalOpen: false });
+ }
+
+ onDeleteSpecificationPress = () => {
+ this.setState({
+ isEditSpecificationModalOpen: false,
+ isDeleteSpecificationModalOpen: true
+ });
+ }
+
+ onDeleteSpecificationModalClose = () => {
+ this.setState({ isDeleteSpecificationModalOpen: false });
+ }
+
+ onCloneSpecificationPress = () => {
+ this.props.onCloneSpecificationPress(this.props.id);
+ }
+
+ onConfirmDeleteSpecification = () => {
+ this.props.onConfirmDeleteSpecification(this.props.id);
+ }
+
+ //
+ // Lifecycle
+
+ render() {
+ const {
+ id,
+ implementationName,
+ name,
+ required,
+ negate
+ } = this.props;
+
+ return (
+
+
+
+
+
+
+ {
+ negate &&
+
+ }
+
+ {
+ required &&
+
+ }
+
+
+
+
+
+
+ Are you sure you want to delete format tag '{name}'?
+
+
+ }
+ confirmLabel="Delete"
+ onConfirm={this.onConfirmDeleteSpecification}
+ onCancel={this.onDeleteSpecificationModalClose}
+ />
+
+ );
+ }
+}
+
+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
+};
+
+export default Specification;
diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js
index e9cf9f83d..411df21d9 100644
--- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js
+++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js
@@ -79,8 +79,8 @@ function createFormatsSelector() {
});
} else {
result.push({
- key: format.id,
- value: format.name
+ key: format,
+ value: name
});
}
}
@@ -201,7 +201,7 @@ class EditQualityProfileModalContentConnector extends Component {
return false;
}
- return i.id === cutoff || (i.format && i.format.id === cutoff);
+ return i.id === cutoff || (i.format === cutoff);
});
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
@@ -210,7 +210,7 @@ class EditQualityProfileModalContentConnector extends Component {
let cutoffId = null;
if (firstAllowed) {
- cutoffId = firstAllowed.format ? firstAllowed.format.id : firstAllowed.id;
+ cutoffId = firstAllowed.format;
}
this.props.setQualityProfileValue({ name: 'formatCutoff', value: cutoffId });
@@ -241,11 +241,7 @@ class EditQualityProfileModalContentConnector extends Component {
onFormatCutoffChange = ({ name, value }) => {
const id = parseInt(value);
- const item = _.find(this.props.item.formatItems.value, (i) => {
- return i.format.id === id;
- });
-
- const cutoffId = item.format.id;
+ const cutoffId = _.find(this.props.item.formatItems.value, (i) => i.format === id).format;
this.props.setQualityProfileValue({ name, value: cutoffId });
}
@@ -281,7 +277,7 @@ class EditQualityProfileModalContentConnector extends Component {
onQualityProfileFormatItemAllowedChange = (id, allowed) => {
const qualityProfile = _.cloneDeep(this.props.item);
const formatItems = qualityProfile.formatItems.value;
- const item = _.find(qualityProfile.formatItems.value, (i) => i.format && i.format.id === id);
+ const item = _.find(qualityProfile.formatItems.value, (i) => i.format === id);
item.allowed = allowed;
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js
index e6e94abbe..a75073fce 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js
@@ -62,12 +62,12 @@ class QualityProfileFormatItems extends Component {
{
- profileFormatItems.map(({ allowed, format }, index) => {
+ profileFormatItems.map(({ allowed, format, name }, index) => {
return (
{
+ return {
+ section,
+ ...payload
+ };
+});
+
+export const setCustomFormatSpecificationFieldValue = createAction(SET_CUSTOM_FORMAT_SPECIFICATION_FIELD_VALUE, (payload) => {
+ return {
+ section,
+ ...payload
+ };
+});
+
+export const cloneCustomFormatSpecification = createAction(CLONE_CUSTOM_FORMAT_SPECIFICATION);
+
+export const clearCustomFormatSpecification = createAction(CLEAR_CUSTOM_FORMAT_SPECIFICATIONS);
+
+export const clearCustomFormatSpecificationPending = createThunk(CLEAR_CUSTOM_FORMAT_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_CUSTOM_FORMAT_SPECIFICATION_SCHEMA]: createFetchSchemaHandler(section, '/customformat/schema'),
+
+ [FETCH_CUSTOM_FORMAT_SPECIFICATIONS]: (getState, payload, dispatch) => {
+ let tags = [];
+ if (payload.id) {
+ const cfState = getSectionState(getState(), 'settings.customFormats', 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_CUSTOM_FORMAT_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.customFormatSpecifications.items);
+ }
+
+ dispatch(batchActions([
+ updateItem({ section, ...saveData }),
+ set({
+ section,
+ pendingChanges: {}
+ })
+ ]));
+ },
+
+ [DELETE_CUSTOM_FORMAT_SPECIFICATION]: (getState, payload, dispatch) => {
+ const id = payload.id;
+ return dispatch(removeItem({ section, id }));
+ },
+
+ [CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING]: (getState, payload, dispatch) => {
+ return dispatch(set({
+ section,
+ pendingChanges: {}
+ }));
+ }
+ },
+
+ //
+ // Reducers
+
+ reducers: {
+ [SET_CUSTOM_FORMAT_SPECIFICATION_VALUE]: createSetSettingValueReducer(section),
+ [SET_CUSTOM_FORMAT_SPECIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
+
+ [SELECT_CUSTOM_FORMAT_SPECIFICATION_SCHEMA]: (state, { payload }) => {
+ return selectProviderSchema(state, section, payload, (selectedSchema) => {
+ return selectedSchema;
+ });
+ },
+
+ [CLONE_CUSTOM_FORMAT_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_CUSTOM_FORMAT_SPECIFICATIONS]: createClearReducer(section, {
+ isPopulated: false,
+ error: null,
+ items: []
+ })
+ }
+};
diff --git a/frontend/src/Store/Actions/Settings/customFormats.js b/frontend/src/Store/Actions/Settings/customFormats.js
index b50fb38b2..54c2544bd 100644
--- a/frontend/src/Store/Actions/Settings/customFormats.js
+++ b/frontend/src/Store/Actions/Settings/customFormats.js
@@ -4,9 +4,9 @@ import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
-import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
+import { set } from '../baseActions';
//
// Variables
@@ -17,7 +17,6 @@ const section = 'settings.customFormats';
// Actions Types
export const FETCH_CUSTOM_FORMATS = 'settings/customFormats/fetchCustomFormats';
-export const FETCH_CUSTOM_FORMAT_SCHEMA = 'settings/customFormats/fetchCustomFormatSchema';
export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
@@ -27,7 +26,6 @@ export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
// Action Creators
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
-export const fetchCustomFormatSchema = createThunk(FETCH_CUSTOM_FORMAT_SCHEMA);
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
@@ -49,15 +47,13 @@ export default {
// State
defaultState: {
+ isSchemaFetching: false,
+ isSchemaPopulated: false,
isFetching: false,
isPopulated: false,
error: null,
isDeleting: false,
deleteError: null,
- isSchemaFetching: false,
- isSchemaPopulated: false,
- schemaError: null,
- schema: {},
isSaving: false,
saveError: null,
items: [],
@@ -69,9 +65,21 @@ export default {
actionHandlers: {
[FETCH_CUSTOM_FORMATS]: createFetchHandler(section, '/customformat'),
- [FETCH_CUSTOM_FORMAT_SCHEMA]: createFetchSchemaHandler(section, '/customformat/schema'),
- [SAVE_CUSTOM_FORMAT]: createSaveProviderHandler(section, '/customformat'),
- [DELETE_CUSTOM_FORMAT]: createRemoveItemHandler(section, '/customformat')
+
+ [DELETE_CUSTOM_FORMAT]: createRemoveItemHandler(section, '/customformat'),
+
+ [SAVE_CUSTOM_FORMAT]: (getState, payload, dispatch) => {
+ // move the format tags in as a pending change
+ const state = getState();
+ const pendingChanges = state.settings.customFormats.pendingChanges;
+ pendingChanges.specifications = state.settings.customFormatSpecifications.items;
+ dispatch(set({
+ section,
+ pendingChanges
+ }));
+
+ createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
+ }
},
//
diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js
index 7cb2a3edd..5fa96d16f 100644
--- a/frontend/src/Store/Actions/settingsActions.js
+++ b/frontend/src/Store/Actions/settingsActions.js
@@ -1,6 +1,7 @@
import { createAction } from 'redux-actions';
import { handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
+import customFormatSpecifications from './Settings/customFormatSpecifications';
import customFormats from './Settings/customFormats';
import delayProfiles from './Settings/delayProfiles';
import downloadClients from './Settings/downloadClients';
@@ -23,6 +24,7 @@ import remotePathMappings from './Settings/remotePathMappings';
import restrictions from './Settings/restrictions';
import ui from './Settings/ui';
+export * from './Settings/customFormatSpecifications.js';
export * from './Settings/customFormats';
export * from './Settings/delayProfiles';
export * from './Settings/downloadClients';
@@ -56,6 +58,7 @@ export const section = 'settings';
export const defaultState = {
advancedSettings: false,
+ customFormatSpecifications: customFormatSpecifications.defaultState,
customFormats: customFormats.defaultState,
delayProfiles: delayProfiles.defaultState,
downloadClients: downloadClients.defaultState,
@@ -97,6 +100,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
// Action Handlers
export const actionHandlers = handleThunks({
+ ...customFormatSpecifications.actionHandlers,
...customFormats.actionHandlers,
...delayProfiles.actionHandlers,
...downloadClients.actionHandlers,
@@ -129,6 +133,7 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
},
+ ...customFormatSpecifications.reducers,
...customFormats.reducers,
...delayProfiles.reducers,
...downloadClients.reducers,
diff --git a/frontend/src/Utilities/State/getNextId.js b/frontend/src/Utilities/State/getNextId.js
new file mode 100644
index 000000000..204aac95a
--- /dev/null
+++ b/frontend/src/Utilities/State/getNextId.js
@@ -0,0 +1,5 @@
+function getNextId(items) {
+ return items.reduce((id, x) => Math.max(id, x.id), 1) + 1;
+}
+
+export default getNextId;
diff --git a/frontend/src/Utilities/State/getProviderState.js b/frontend/src/Utilities/State/getProviderState.js
index 60923a646..4159905b8 100644
--- a/frontend/src/Utilities/State/getProviderState.js
+++ b/frontend/src/Utilities/State/getProviderState.js
@@ -1,7 +1,7 @@
import _ from 'lodash';
import getSectionState from 'Utilities/State/getSectionState';
-function getProviderState(payload, getState, section) {
+function getProviderState(payload, getState, section, keyValueOnly=true) {
const {
id,
...otherPayload
@@ -23,10 +23,17 @@ function getProviderState(payload, getState, section) {
field.value;
// Only send the name and value to the server
- result.push({
- name,
- value
- });
+ if (keyValueOnly) {
+ result.push({
+ name,
+ value
+ });
+ } else {
+ result.push({
+ ...field,
+ value
+ });
+ }
return result;
}, []);
diff --git a/src/NzbDrone.Api/CustomFormats/CustomFormatModule.cs b/src/NzbDrone.Api/CustomFormats/CustomFormatModule.cs
new file mode 100644
index 000000000..a1e82dd9c
--- /dev/null
+++ b/src/NzbDrone.Api/CustomFormats/CustomFormatModule.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+using System.Linq;
+using FluentValidation;
+using NzbDrone.Core.CustomFormats;
+using Radarr.Http;
+
+namespace NzbDrone.Api.CustomFormats
+{
+ public class CustomFormatModule : RadarrRestModule
+ {
+ private readonly ICustomFormatService _formatService;
+
+ public CustomFormatModule(ICustomFormatService formatService)
+ {
+ _formatService = formatService;
+
+ SharedValidator.RuleFor(c => c.Name).NotEmpty();
+ SharedValidator.RuleFor(c => c.Name)
+ .Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
+ SharedValidator.RuleFor(c => c.Specifications).NotEmpty();
+
+ GetResourceAll = GetAll;
+
+ GetResourceById = GetById;
+
+ UpdateResource = Update;
+
+ CreateResource = Create;
+
+ DeleteResource = DeleteFormat;
+
+ Get("schema", x => GetTemplates());
+ }
+
+ private int Create(CustomFormatResource customFormatResource)
+ {
+ var model = customFormatResource.ToModel();
+ return _formatService.Insert(model).Id;
+ }
+
+ private void Update(CustomFormatResource resource)
+ {
+ var model = resource.ToModel();
+ _formatService.Update(model);
+ }
+
+ private CustomFormatResource GetById(int id)
+ {
+ return _formatService.GetById(id).ToResource();
+ }
+
+ private List GetAll()
+ {
+ return _formatService.All().ToResource();
+ }
+
+ private void DeleteFormat(int id)
+ {
+ _formatService.Delete(id);
+ }
+
+ private object GetTemplates()
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/NzbDrone.Api/Qualities/CustomFormatResource.cs b/src/NzbDrone.Api/CustomFormats/CustomFormatResource.cs
similarity index 79%
rename from src/NzbDrone.Api/Qualities/CustomFormatResource.cs
rename to src/NzbDrone.Api/CustomFormats/CustomFormatResource.cs
index b4c658723..f81bbb318 100644
--- a/src/NzbDrone.Api/Qualities/CustomFormatResource.cs
+++ b/src/NzbDrone.Api/CustomFormats/CustomFormatResource.cs
@@ -3,12 +3,12 @@ using System.Linq;
using NzbDrone.Core.CustomFormats;
using Radarr.Http.REST;
-namespace NzbDrone.Api.Qualities
+namespace NzbDrone.Api.CustomFormats
{
public class CustomFormatResource : RestResource
{
public string Name { get; set; }
- public List FormatTags { get; set; }
+ public List Specifications { get; set; }
public string Simplicity { get; set; }
}
@@ -20,23 +20,23 @@ namespace NzbDrone.Api.Qualities
{
Id = model.Id,
Name = model.Name,
- FormatTags = model.FormatTags.Select(t => t.Raw.ToUpper()).ToList(),
+ Specifications = model.Specifications.ToList(),
};
}
+ public static List ToResource(this IEnumerable models)
+ {
+ return models.Select(m => m.ToResource()).ToList();
+ }
+
public static CustomFormat ToModel(this CustomFormatResource resource)
{
return new CustomFormat
{
Id = resource.Id,
Name = resource.Name,
- FormatTags = resource.FormatTags.Select(s => new FormatTag(s)).ToList(),
+ Specifications = resource.Specifications.ToList(),
};
}
-
- public static List ToResource(this IEnumerable models)
- {
- return models.Select(m => m.ToResource()).ToList();
- }
}
}
diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs
index c950083b5..cf0084420 100644
--- a/src/NzbDrone.Api/Profiles/ProfileResource.cs
+++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
-using NzbDrone.Api.Qualities;
+using NzbDrone.Api.CustomFormats;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Profiles;
diff --git a/src/NzbDrone.Api/Qualities/CustomFormatModule.cs b/src/NzbDrone.Api/Qualities/CustomFormatModule.cs
deleted file mode 100644
index 94d9b6edc..000000000
--- a/src/NzbDrone.Api/Qualities/CustomFormatModule.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using FluentValidation;
-using Nancy;
-using NzbDrone.Core.CustomFormats;
-using NzbDrone.Core.Parser;
-using Radarr.Http;
-
-namespace NzbDrone.Api.Qualities
-{
- public class CustomFormatModule : RadarrRestModule
- {
- private readonly ICustomFormatService _formatService;
- private readonly ICustomFormatCalculationService _formatCalculator;
- private readonly IParsingService _parsingService;
-
- public CustomFormatModule(ICustomFormatService formatService,
- ICustomFormatCalculationService formatCalculator,
- IParsingService parsingService)
- {
- _formatService = formatService;
- _formatCalculator = formatCalculator;
- _parsingService = parsingService;
-
- SharedValidator.RuleFor(c => c.Name).NotEmpty();
- SharedValidator.RuleFor(c => c.Name)
- .Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
- SharedValidator.RuleFor(c => c.FormatTags).SetValidator(new FormatTagValidator());
- SharedValidator.RuleFor(c => c.FormatTags).Must((v, c) =>
- {
- var allFormats = _formatService.All();
- return !allFormats.Any(f =>
- {
- var allTags = f.FormatTags.Select(t => t.Raw.ToLower());
- var allNewTags = c.Select(t => t.ToLower());
- var enumerable = allTags.ToList();
- var newTags = allNewTags.ToList();
- return enumerable.All(newTags.Contains) && f.Id != v.Id && enumerable.Count() == newTags.Count();
- });
- })
- .WithMessage("Should be unique.");
-
- GetResourceAll = GetAll;
-
- GetResourceById = GetById;
-
- UpdateResource = Update;
-
- CreateResource = Create;
-
- DeleteResource = DeleteFormat;
-
- Get("/test", x => Test());
-
- Post("/test", x => TestWithNewModel());
-
- Get("schema", x => GetTemplates());
- }
-
- private int Create(CustomFormatResource customFormatResource)
- {
- var model = customFormatResource.ToModel();
- return _formatService.Insert(model).Id;
- }
-
- private void Update(CustomFormatResource resource)
- {
- var model = resource.ToModel();
- _formatService.Update(model);
- }
-
- private CustomFormatResource GetById(int id)
- {
- return _formatService.GetById(id).ToResource();
- }
-
- private List GetAll()
- {
- return _formatService.All().ToResource();
- }
-
- private void DeleteFormat(int id)
- {
- _formatService.Delete(id);
- }
-
- private object GetTemplates()
- {
- return CustomFormatService.Templates.SelectMany(t =>
- {
- return t.Value.Select(m =>
- {
- var r = m.ToResource();
- r.Simplicity = t.Key;
- return r;
- });
- });
- }
-
- private CustomFormatTestResource Test()
- {
- var parsed = _parsingService.ParseMovieInfo((string)Request.Query.title, new List