New: Better interface for creating custom formats

This commit is contained in:
ta264
2020-02-18 21:03:05 +00:00
committed by Qstick
parent a5bac30ef3
commit 241bf85f15
74 changed files with 2259 additions and 1090 deletions

View File

@@ -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 {
<div>
{
split(formatTags).map((item) => {
specifications.map((item, index) => {
if (!item) {
return null;
}
return (
<Label
key={item}
kind={kinds.DEFAULT}
key={index}
kind={item.required ? kinds.DANGER : kinds.DEFAULT}
>
{item}
{item.name}
</Label>
);
})
@@ -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

View File

@@ -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 {
>
<div className={styles.customFormats}>
{
items.sort(sortByName).map((item) => {
items.map((item) => {
return (
<CustomFormat
key={item.id}
@@ -87,6 +93,7 @@ class CustomFormats extends Component {
<EditCustomFormatModalConnector
isOpen={this.state.isCustomFormatModalOpen}
tagsFromId={this.state.tagsFromId}
onModalClose={this.onModalClose}
/>

View File

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

View File

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

View File

@@ -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 &&
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>
Name
</FormLabel>
!isFetching && !error && specificationsPopulated &&
<div>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>
Name
</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
</Form>
<FormGroup>
<FormLabel>
Format Tags
</FormLabel>
<FieldSet legend="Conditions">
<div className={styles.customFormats}>
{
specifications.map((tag) => {
return (
<Specification
key={tag.id}
{...tag}
onCloneSpecificationPress={onCloneSpecificationPress}
onConfirmDeleteSpecification={onConfirmDeleteSpecification}
/>
);
})
}
<FormInputGroup
type={inputTypes.TEXT_TAG}
name="formatTags"
{...formatTags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
<Card
className={styles.addSpecification}
onPress={this.onAddSpecificationPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
</FieldSet>
<AddSpecificationModal
isOpen={isAddSpecificationModalOpen}
onModalClose={this.onAddSpecificationModalClose}
/>
<EditSpecificationModalConnector
isOpen={isEditSpecificationModalOpen}
onModalClose={this.onEditSpecificationModalClose}
/>
</div>
}
</div>
</ModalBody>
@@ -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;

View File

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

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,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 (
<div
className={styles.specification}
>
<Link
className={styles.underlay}
onPress={this.onSpecificationSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onSpecificationSelect}
>
Custom
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
Presets
</Button>
<MenuContent>
{
presets.map((preset, index) => {
return (
<AddSpecificationPresetMenuItem
key={index}
name={preset.name}
implementation={implementation}
onPress={onSpecificationSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
More info
</Button>
</div>
</div>
</div>
);
}
}
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;

View File

@@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddSpecificationModalContentConnector
{...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,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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Add Condition
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<div>Unable to add a new condition, please try again.</div>
}
{
isSchemaPopulated && !schemaError &&
<div>
<Alert kind={kinds.INFO}>
<div>Radarr supports custom conditions against the following release properties</div>
<div>Visit github for more details</div>
</Alert>
<div className={styles.specifications}>
{
schema.map((specification) => {
return (
<AddSpecificationItem
key={specification.implementation}
{...specification}
onSpecificationSelect={onSpecificationSelect}
/>
);
})
}
</div>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
Close
</Button>
</ModalFooter>
</ModalContent>
);
}
}
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;

View File

@@ -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 (
<AddSpecificationModalContent
{...this.props}
onSpecificationSelect={this.onSpecificationSelect}
/>
);
}
}
AddSpecificationModalContentConnector.propTypes = {
fetchCustomFormatSpecificationSchema: PropTypes.func.isRequired,
selectCustomFormatSpecificationSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddSpecificationModalContentConnector);

View File

@@ -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 (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddSpecificationPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddSpecificationPresetMenuItem;

View File

@@ -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 (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditSpecificationModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditSpecificationModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditSpecificationModal;

View File

@@ -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 (
<EditSpecificationModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditSpecificationModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditSpecificationModalConnector);

View File

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

View File

@@ -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 (
<ModalContent onModalClose={onCancelPress}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Condition - ${implementationName}`}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
{
fields && fields.some((x) => x.label === 'Regular Expression') &&
<Alert kind={kinds.INFO}>
<div>This condition matches using Regular Expressions. See <Link to="https://www.regular-expressions.info/tutorial.html">here</Link> for details. Note that the characters <code>{'\\^$.|?*+()[{'}</code> have special meanings and need escaping with a <code>\</code></div>
<div>Regular expressions can be tested <Link to="http://regexstorm.net/tester">here</Link>.</div>
</Alert>
}
<FormGroup>
<FormLabel>
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>
Negate
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="negate"
{...negate}
helpText={`If checked, the custom format will not apply if this ${implementationName} condition matches.`}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
Required
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="required"
{...required}
helpText={`This ${implementationName} condition must match for the custom format to apply. Otherwise a single ${implementationName} match is sufficient.`}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteSpecificationPress}
>
Delete
</Button>
}
<Button
onPress={onCancelPress}
>
Cancel
</Button>
<SpinnerErrorButton
isSpinning={false}
onPress={onSavePress}
>
Save
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
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;

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 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 (
<EditSpecificationModalContent
{...this.props}
onCancelPress={this.onCancelPress}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
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);

View File

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

View File

@@ -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 (
<Card
className={styles.customFormat}
overlayContent={true}
onPress={this.onEditSpecificationPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title="Clone Format Tag"
name={icons.CLONE}
onPress={this.onCloneSpecificationPress}
/>
</div>
<div className={styles.labels}>
<Label kind={kinds.DEFAULT}>
{implementationName}
</Label>
{
negate &&
<Label kind={kinds.INVERSE}>
{'Negated'}
</Label>
}
{
required &&
<Label kind={kinds.DANGER}>
{'Required'}
</Label>
}
</div>
<EditSpecificationModalConnector
id={id}
isOpen={this.state.isEditSpecificationModalOpen}
onModalClose={this.onEditSpecificationModalClose}
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER}
title="Delete Custom Format"
message={
<div>
<div>
Are you sure you want to delete format tag '{name}'?
</div>
</div>
}
confirmLabel="Delete"
onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose}
/>
</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
};
export default Specification;

View File

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

View File

@@ -62,12 +62,12 @@ class QualityProfileFormatItems extends Component {
<div className={styles.formats}>
{
profileFormatItems.map(({ allowed, format }, index) => {
profileFormatItems.map(({ allowed, format, name }, index) => {
return (
<QualityProfileFormatItemDragSource
key={format.id}
formatId={format.id}
name={format.name}
key={format}
formatId={format}
name={name}
allowed={allowed}
sortIndex={index}
isDragging={isDragging}

View File

@@ -0,0 +1,184 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import getNextId from 'Utilities/State/getNextId';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import getProviderState from 'Utilities/State/getProviderState';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createClearReducer from 'Store/Actions/Creators/Reducers/createClearReducer';
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
import { set, update, updateItem, removeItem } from '../baseActions';
//
// Variables
const section = 'settings.customFormatSpecifications';
//
// Actions Types
export const FETCH_CUSTOM_FORMAT_SPECIFICATIONS = 'settings/customFormatSpecifications/fetchCustomFormatSpecifications';
export const FETCH_CUSTOM_FORMAT_SPECIFICATION_SCHEMA = 'settings/customFormatSpecifications/fetchCustomFormatSpecificationSchema';
export const SELECT_CUSTOM_FORMAT_SPECIFICATION_SCHEMA = 'settings/customFormatSpecifications/selectCustomFormatSpecificationSchema';
export const SET_CUSTOM_FORMAT_SPECIFICATION_VALUE = 'settings/customFormatSpecifications/setCustomFormatSpecificationValue';
export const SET_CUSTOM_FORMAT_SPECIFICATION_FIELD_VALUE = 'settings/customFormatSpecifications/setCustomFormatSpecificationFieldValue';
export const SAVE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/saveCustomFormatSpecification';
export const DELETE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/deleteCustomFormatSpecification';
export const CLONE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/cloneCustomFormatSpecification';
export const CLEAR_CUSTOM_FORMAT_SPECIFICATIONS = 'settings/customFormatSpecifications/clearCustomFormatSpecifications';
export const CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING = 'settings/customFormatSpecifications/clearCustomFormatSpecificationPending';
//
// Action Creators
export const fetchCustomFormatSpecifications = createThunk(FETCH_CUSTOM_FORMAT_SPECIFICATIONS);
export const fetchCustomFormatSpecificationSchema = createThunk(FETCH_CUSTOM_FORMAT_SPECIFICATION_SCHEMA);
export const selectCustomFormatSpecificationSchema = createAction(SELECT_CUSTOM_FORMAT_SPECIFICATION_SCHEMA);
export const saveCustomFormatSpecification = createThunk(SAVE_CUSTOM_FORMAT_SPECIFICATION);
export const deleteCustomFormatSpecification = createThunk(DELETE_CUSTOM_FORMAT_SPECIFICATION);
export const setCustomFormatSpecificationValue = createAction(SET_CUSTOM_FORMAT_SPECIFICATION_VALUE, (payload) => {
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: []
})
}
};

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
function getNextId(items) {
return items.reduce((id, x) => Math.max(id, x.id), 1) + 1;
}
export default getNextId;

View File

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