WiP on segments.

This commit is contained in:
Tomas Bures 2017-08-18 21:04:31 +02:00
parent d0a714b3d4
commit 6cc34136f5
9 changed files with 766 additions and 489 deletions

View file

@ -1,23 +1,22 @@
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton, Toolbar} from '../../lib/page';
import {
withForm, Form, FormSendMethod, InputField, ButtonRow, Button, Fieldset, Dropdown, TreeTableSelect
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import {NavButton, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from "../../lib/page";
import {Button as FormButton, ButtonRow, Dropdown, Form, FormSendMethod, InputField, withForm} from "../../lib/form";
import {withAsyncErrorHandler, withErrorHandling} from "../../lib/error-handling";
import {DeleteModalDialog} from "../../lib/delete";
import interoperableErrors from '../../../../shared/interoperable-errors';
import interoperableErrors from "../../../../shared/interoperable-errors";
import styles from './CUD.scss';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
import SortableTree from 'react-sortable-tree';
import {ActionLink, Icon} from "../../lib/bootstrap-components";
import { getFieldTypes } from '../fields/field-types';
import styles from "./CUD.scss";
import {DragDropContext} from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import TouchBackend from "react-dnd-touch-backend";
import SortableTree from "react-sortable-tree";
import {ActionLink, Button, Icon} from "../../lib/bootstrap-components";
import {getRuleHelpers} from "./rule-helpers";
import RuleSettingsPane from "./RuleSettingsPane";
// https://stackoverflow.com/a/4819886/1601953
const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
@ -29,236 +28,14 @@ const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
@withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component {
// The code below keeps the segment settings in form value. However, it uses it as a mutable datastructure.
// After initilization, segment settings is never set using setState. This is OK we update the state.rulesTree
// from the segment settings on relevant events (changes in the tree and closing the rule settings pane).
constructor(props) {
super(props);
const t = props.t;
this.fieldTypes = getFieldTypes(t);
this.predefColumns = [
{
column: 'email',
name: t('Email address'),
type: 'text',
tag: 'EMAIL'
},
{
column: 'opt_in_country',
name: t('Signup country'),
type: 'text'
},
{
column: 'created',
name: t('Sign up date'),
type: 'date'
},
{
column: 'latest_open',
name: t('Latest open'),
type: 'date'
},
{
column: 'latest_click',
name: t('Latest click'),
type: 'date'
}
];
this.compositeRuleTypeOptions = [
{ key: 'all', label: t('All rules must match')},
{ key: 'some', label: t('At least one rule must match')},
{ key: 'none', label: t('No rule may match')}
];
this.compositeRuleTypes = {
all: true,
some: true,
none: true
};
this.primitiveRuleTypeOptions = {
text: [
{ key: 'eq', label: t('Equal to')},
{ key: 'like', label: t('Match (with SQL LIKE)')},
{ key: 're', label: t('Match (with regular expressions)')},
{ key: 'lt', label: t('Alphabetically before')},
{ key: 'le', label: t('Alphabetically before or equal to')},
{ key: 'gt', label: t('Alphabetically after')},
{ key: 'ge', label: t('Alphabetically after or equal to')}
],
website: [
{ key: 'eq', label: t('Equal to')},
{ key: 'like', label: t('Match (with SQL LIKE)')},
{ key: 're', label: t('Match (with regular expressions)')}
],
number: [
{ key: 'eq', label: t('Equal to')},
{ key: 'lt', label: t('Less than')},
{ key: 'le', label: t('Less than or equal to')},
{ key: 'gt', label: t('Greater than')},
{ key: 'ge', label: t('Greater than or equal to')}
],
birthday: [
{ key: 'eq', label: t('On')},
{ key: 'lt', label: t('Before')},
{ key: 'le', label: t('Before or on')},
{ key: 'gt', label: t('After')},
{ key: 'ge', label: t('After or on')},
{ key: 'eqNowPlusDays', label: t('On x-th day before/after now')},
{ key: 'ltNowPlusDays', label: t('Before x-th day before/after now')},
{ key: 'leNowPlusDays', label: t('Before or on x-th day before/after now')},
{ key: 'gtNowPlusDays', label: t('After x-th day before/after now')},
{ key: 'geNowPlusDays', label: t('After or on x-th day before/after now')},
],
date: [
{ key: 'eq', label: t('On')},
{ key: 'lt', label: t('Before')},
{ key: 'le', label: t('Before or on')},
{ key: 'gt', label: t('After')},
{ key: 'ge', label: t('After or on')},
{ key: 'eqNowPlusDays', label: t('On x-th day before/after now')},
{ key: 'ltNowPlusDays', label: t('Before x-th day before/after now')},
{ key: 'leNowPlusDays', label: t('Before or on x-th day before/after now')},
{ key: 'gtNowPlusDays', label: t('After x-th day before/after now')},
{ key: 'geNowPlusDays', label: t('After or on x-th day before/after now')},
],
option: [
{ key: 'isTrue', label: t('Is selected')},
{ key: 'isFalse', label: t('Is not selected')}
],
'radio-enum': [
{ key: 'eq', label: t('Key equal to')},
{ key: 'like', label: t('Key match (with SQL LIKE)')},
{ key: 're', label: t('Key match (with regular expressions)')},
{ key: 'lt', label: t('Key alphabetically before')},
{ key: 'le', label: t('Key alphabetically before or equal to')},
{ key: 'gt', label: t('Key alphabetically after')},
{ key: 'ge', label: t('Key alphabetically after or equal to')}
],
'dropdown-enum': [
{ key: 'eq', label: t('Key equal to')},
{ key: 'like', label: t('Key match (with SQL LIKE)')},
{ key: 're', label: t('Key match (with regular expressions)')},
{ key: 'lt', label: t('Key alphabetically before')},
{ key: 'le', label: t('Key alphabetically before or equal to')},
{ key: 'gt', label: t('Key alphabetically after')},
{ key: 'ge', label: t('Key alphabetically after or equal to')}
]
};
const stringValueSettings = {
form: <InputField id="ruleValue" label={t('Value')} />,
getFormData: rule => ({
ruleValue: rule.value
}),
assignRuleSettings: rule => {
rule.value = this.getFormValue('ruleValue');
},
localValidateForm: state => {
if (!state.getIn(['ruleValue', 'value'])) {
state.setIn(['ruleValue', 'error'], t('Value must not be empty'));
} else {
state.setIn(['ruleValue', 'error'], null);
}
}
};
const numberValueSettings = {
form: <InputField id="value" label={t('Value')} />
};
const birthdayValueSettings = {
form: <InputField id="value" label={t('Value')} /> // FIXME
};
const birthdayRelativeValueSettings = {
form: <InputField id="value" label={t('Value')} /> // FIXME
};
const dateValueSettings = {
form: <InputField id="value" label={t('Value')} /> // FIXME
};
const dateRelativeValueSettings = {
form: <InputField id="value" label={t('Value')} /> // FIXME
};
const optionValueSettings = {
form: null
};
this.primitiveRuleTypes = {
text: {
eq: stringValueSettings,
like: stringValueSettings,
re: stringValueSettings,
lt: stringValueSettings,
le: stringValueSettings,
gt: stringValueSettings,
ge: stringValueSettings
},
website: {
eq: stringValueSettings,
like: stringValueSettings,
re: stringValueSettings,
},
number: {
eq: numberValueSettings,
lt: numberValueSettings,
le: numberValueSettings,
gt: numberValueSettings,
ge: numberValueSettings
},
birthday: {
eq: birthdayValueSettings,
lt: birthdayValueSettings,
le: birthdayValueSettings,
gt: birthdayValueSettings,
ge: birthdayValueSettings,
eqNowPlusDays: birthdayRelativeValueSettings,
ltNowPlusDays: birthdayRelativeValueSettings,
leNowPlusDays: birthdayRelativeValueSettings,
gtNowPlusDays: birthdayRelativeValueSettings,
geNowPlusDays: birthdayRelativeValueSettings
},
date: {
eq: dateValueSettings,
lt: dateValueSettings,
le: dateValueSettings,
gt: dateValueSettings,
ge: dateValueSettings,
eqNowPlusDays: dateRelativeValueSettings,
ltNowPlusDays: dateRelativeValueSettings,
leNowPlusDays: dateRelativeValueSettings,
gtNowPlusDays: dateRelativeValueSettings,
geNowPlusDays: dateRelativeValueSettings
},
option: {
isTrue: optionValueSettings,
isFale: optionValueSettings
},
'radio-enum': {
eq: stringValueSettings,
like: stringValueSettings,
re: stringValueSettings,
lt: stringValueSettings,
le: stringValueSettings,
gt: stringValueSettings,
ge: stringValueSettings
},
'dropdown-enum': {
eq: stringValueSettings,
like: stringValueSettings,
re: stringValueSettings,
lt: stringValueSettings,
le: stringValueSettings,
gt: stringValueSettings,
ge: stringValueSettings
}
};
this.ruleHelpers = getRuleHelpers(props.t, props.fields);
this.state = {
rulesTree: this.getTreeFromRules([])
@ -276,18 +53,16 @@ export default class CUD extends Component {
entity: PropTypes.object
}
getColumnType(column) {
const field = this.props.fields.find(fld => x.column === column);
if (field) {
return field.type;
}
}
getRulesFromTree(tree) {
const rules = [];
for (const node of tree) {
const rule = node.rule;
rule.rules = this.getRulesFromTree(node.children);
if (this.ruleHelpers.isCompositeRuleType(rule.type)) {
rule.rules = this.getRulesFromTree(node.children);
}
rules.push(rule);
}
@ -295,17 +70,16 @@ export default class CUD extends Component {
}
getTreeFromRules(rules) {
const ruleHelpers = this.ruleHelpers;
const tree = [];
for (const rule of rules) {
let title, subtitle;
title = rule.type; // FIXME
subtitle = null;
const ruleTypeSettings = ruleHelpers.getRuleTypeSettings(rule);
const title = ruleTypeSettings ? ruleTypeSettings.treeLabel(rule) : this.props.t('New rule');
tree.push({
rule,
title,
subtitle,
expanded: true,
children: this.getTreeFromRules(rule.rules || [])
});
@ -314,39 +88,12 @@ export default class CUD extends Component {
return tree;
}
assignRuleSettings(rule) {
// This assumes that the rule form has successfully passed validation
rule.type = this.getFormValue('ruleType');
if (rule.type in !this.compositeRuleTypes) {
rule.column = this.getFormValue('ruleColumn');
const colType = this.getColumnType(rule.column);
if (colType) {
this.primitiveRuleTypes[colType][rule.type].assignRuleSettings(rule);
}
}
}
getFormData(rule) {
if (rule.type in !this.compositeRuleTypes) {
let data;
const colType = this.getColumnType(rule.column);
if (colType) { // getFormData is called also from addRule, which means the rule may not have a column selected
data = this.primitiveRuleTypes[colType][rule.type].getFormData(rule);
} else {
data = {};
}
data.ruleType = rule.type;
data.ruleColumn = rule.column;
} else {
return {
ruleType: rule.type
};
}
@withAsyncErrorHandler
async loadFormValues() {
await this.getFormValuesFromURL(`/rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
data.rootRuleType = data.settings.rootRule.type;
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
});
}
componentDidMount() {
@ -357,6 +104,7 @@ export default class CUD extends Component {
this.getFormValuesFromEntity(this.props.entity, data => {
data.rootRuleType = data.settings.rootRule.type;
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
});
} else {
@ -368,7 +116,8 @@ export default class CUD extends Component {
rules: []
}
},
rootRuleType: 'all'
rootRuleType: 'all',
selectedRule: null
});
}
}
@ -382,20 +131,14 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], null);
}
// FIXME - validate rule
}
localValidateSelectedRule() {
if (state.getIn(['selectedRule', 'value']) === null) {
state.setIn(['selectedRule', 'error'], null);
}
}
async doSubmit(stay) {
const t = this.props.t;
if (!this.localValidateSelectedRule()) {
// FIXME
}
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
@ -412,17 +155,10 @@ export default class CUD extends Component {
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
const keep = ['name', 'settings', 'originalHash'];
if (this.state.selectedRule) {
this.assignRuleSettings(this.state.selectedRule);
}
data.settings.rootRule.type = data.rootRuleType;
for (const key in data) {
if (!keep.includes(key)) {
delete data[key];
}
}
delete data.rootRuleType;
delete data.selectedRule;
});
if (submitSuccessful) {
@ -460,7 +196,68 @@ export default class CUD extends Component {
await this.formHandleChangedError(async () => await this.doSubmit(false));
}
async onRuleDelete(data) {
async onRulesChanged(rulesTree) {
// This assumes that !this.state.ruleOptionsVisible
this.getFormValue('settings').rootRule.rules = this.getRulesFromTree(rulesTree);
this.setState({
rulesTree
})
}
async showRuleOptions(data) {
const rule = data.node.rule;
this.updateFormValue('selectedRule', rule);
this.setState({
ruleOptionsVisible: true
});
}
onRuleSettingsPaneClose() {
this.updateFormValue('selectedRule', null);
this.setState({
ruleOptionsVisible: false,
rulesTree: this.getTreeFromRules(this.getFormValue('settings').rootRule.rules)
});
}
onRuleSettingsPaneUpdated(hasErrors) {
this.setState(previousState => ({
formState: previousState.formState.setIn(['data', 'selectedRule', 'error'], hasErrors)
}));
}
addRule(rule) {
if (!this.state.ruleOptionsVisible) {
const rules = this.getFormValue('settings').rootRule.rules;
rules.push(rule);
this.updateFormValue('selectedRule', rule);
this.setState({
ruleOptionsVisible: true,
rulesTree: this.getTreeFromRules(rules)
});
}
}
async addCompositeRule() {
this.addRule({
type: 'all',
rules: []
});
}
async addPrimitiveRule() {
this.addRule({
type: null // Null type means a primitive rule where the type has to be chosen based on the chosen column
});
}
async deleteRule(data) {
let finishedSearching = false;
function childrenWithoutRule(rules) {
@ -488,58 +285,9 @@ export default class CUD extends Component {
}
if (!this.state.ruleOptionsVisible) {
const rules = childrenWithoutRule(this.state.rules);
const rules = childrenWithoutRule(this.getFormValue('settings').rootRule.rules);
this.setState({
rules,
rulesTree: this.getTreeFromRules(rules)
});
}
}
async showRuleOptions(data) {
const rule = data.node.rule;
this.populateFormValues(this.getFormData(rule));
this.setState({
ruleOptionsVisible: true,
selectedRule: rule
});
}
async hideRuleOptions() {
const rule = this.state.selectedRule;
// FIXME validate rule settings and if it is valid, do the rest
this.assignRuleSettings(rule); // This changes the rule in the state without notifying. Although this is an anti-pattern for React, this behavior is OK here because we don't want to notify anyone.
this.setState({
ruleOptionsVisible: false,
selectedRule: null,
rulesTree: this.getTreeFromRules(this.state.rules)
});
}
async onRulesChanged(rulesTree) {
// This assumes that !this.state.ruleOptionsVisible
this.getFormValue('settings').rootRule.rules = this.getRulesFromTree(rulesTree);
this.setState({
rulesTree
})
}
async addCompositeRule() {
if (!this.state.ruleOptionsVisible) {
const rule = {
type: 'all',
rules: []
};
const rules = this.getFormValue('settings').rootRule.rules;
rules.push(rule);
this.getFormValue('settings').rootRule.rules = rules;
this.setState({
rulesTree: this.getTreeFromRules(rules)
@ -547,29 +295,12 @@ export default class CUD extends Component {
}
}
async addRule() {
if (!this.state.ruleOptionsVisible) {
const rule = {
type: null // Null type means a primitive rule where the type has to be chosen based on the chosen column
};
const rules = this.getFormValue('settings').rootRule.rules;
rules.push(rule);
this.populateFormValues(this.getFormData(rule));
this.setState({
ruleOptionsVisible: true,
selectedRule: rule,
rulesTree: this.getTreeFromRules(rules)
});
}
}
render() {
const t = this.props.t;
const isEdit = !!this.props.entity;
const selectedRule = this.state.selectedRule;
const selectedRule = this.getFormValue('selectedRule');
const ruleHelpers = this.ruleHelpers;
let ruleOptionsVisibilityClass = '';
if ('ruleOptionsVisible' in this.state) {
@ -580,53 +311,6 @@ export default class CUD extends Component {
}
}
let ruleOptions = null;
if (selectedRule) {
if (selectedRule.type in this.compositeRuleTypes) {
ruleOptions = <Dropdown id="ruleType" label={t('Type')} options={this.compositeRuleTypeOptions} />
}
else {
const ruleColumnOptionsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Type') },
{ data: 3, title: t('Merge Tag') }
];
const ruleColumnOptions = [
...this.predefColumns.map(fld => [ fld.column, fld.name, this.fieldTypes[fld.type], fld.tag || '' ]),
...this.props.fields.filter(fld => fld.type in this.primitiveRuleTypes).map(fld => [ fld.column, fld.name, this.fieldTypes[fld.type], fld.tag || '' ])
];
const ruleColumnSelect = <TableSelect id="ruleColumn" label={t('Field')} data={ruleColumnOptions} columns={ruleColumnOptionsColumns} dropdown withHeader />;
let ruleTypeSelect = null;
let ruleSettings = null;
const ruleColumn = this.getFormValue('ruleColumn');
if (ruleColumn) {
const colType = this.getColumnType(ruleColumn);
if (colType) {
const ruleTypeOptions = this.primitiveRuleTypeOptions[colType];
if (ruleTypeOptions) {
ruleTypeSelect = <Dropdown id="ruleType" label={t('Type')} options={ruleTypeOptions} />
const ruleType = this.getFormValue('ruleType');
if (ruleType) {
ruleSettings = this.state.primitiveRuleTypes[colType][ruleType].form;
}
}
}
}
ruleOptions =
<div>
{ruleColumnSelect}
{ruleTypeSelect}
{ruleSettings}
</div>;
}
}
return (
@ -636,8 +320,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/segments/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/segments/${this.props.list.id}/${this.props.entity.id}/edit`}
listUrl={`/lists/segments/${this.props.list.id}`}
cudUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
listUrl={`/lists/${this.props.list.id}/segments`}
deletingMsg={t('Deleting segment ...')}
deletedMsg={t('Segment deleted')}/>
}
@ -645,70 +329,69 @@ export default class CUD extends Component {
<Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
{isEdit ?
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
</ButtonRow>
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/delete`}/>
</ButtonRow>
:
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
<FormButton type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
</ButtonRow>
}
<h3>{t('Segment Options')}</h3>
<InputField id="name" label={t('Name')} />
<Dropdown id="rootRuleType" label={t('Toplevel match type')} options={this.compositeRuleTypeOptions} />
<Dropdown id="rootRuleType" label={t('Toplevel match type')} options={ruleHelpers.getCompositeRuleTypeOptions()} />
</Form>
<hr />
<hr />
<div className={styles.rulePane + ruleOptionsVisibilityClass}>
<div className={styles.leftPane}>
<div className={styles.leftPaneInner}>
<Toolbar>
<Button className="btn-primary" label={t('Add Composite Rule')} onClickAsync={::this.addCompositeRule}/>
<Button className="btn-primary" label={t('Add Rule')} onClickAsync={::this.addRule}/>
</Toolbar>
<div className={styles.rulePane + ruleOptionsVisibilityClass}>
<div className={styles.leftPane}>
<div className={styles.leftPaneInner}>
<Toolbar>
<Button className="btn-primary" label={t('Add Composite Rule')} onClickAsync={::this.addCompositeRule}/>
<Button className="btn-primary" label={t('Add Rule')} onClickAsync={::this.addPrimitiveRule}/>
</Toolbar>
<h3>{t('Rules')}</h3>
<h3>{t('Rules')}</h3>
<div className="clearfix"/>
<div className="clearfix"/>
<div className={styles.ruleTree}>
<SortableTree
treeData={this.state.rulesTree}
onChange={rulesTree => this.onRulesChanged(rulesTree)}
isVirtualized={false}
canDrop={ data => !data.nextParent || (data.nextParent.rule.type in this.compositeRuleTypes) }
generateNodeProps={data => ({
buttons: [
<ActionLink onClickAsync={async () => await this.showRuleOptions(data)} className={styles.ruleActionLink}><Icon name="edit"/></ActionLink>,
<ActionLink onClickAsync={async () => await this.onRuleDelete(data)} className={styles.ruleActionLink}><Icon name="remove"/></ActionLink>
]
})}
/>
</div>
</div>
<div className={styles.leftPaneOverlay} />
<div className={styles.paneDivider}>
<div className={styles.paneDividerSolidBackground}/>
<div className={styles.ruleTree}>
<SortableTree
treeData={this.state.rulesTree}
onChange={rulesTree => this.onRulesChanged(rulesTree)}
isVirtualized={false}
canDrop={ data => !data.nextParent || (ruleHelpers.isCompositeRuleType(data.nextParent.rule.type)) }
generateNodeProps={data => ({
buttons: [
<ActionLink onClickAsync={async () => await this.showRuleOptions(data)} className={styles.ruleActionLink}><Icon name="edit"/></ActionLink>,
<ActionLink onClickAsync={async () => await this.deleteRule(data)} className={styles.ruleActionLink}><Icon name="remove"/></ActionLink>
]
})}
/>
</div>
</div>
<div className={styles.rightPane}>
<div className={styles.rightPaneInner}>
<div className={styles.ruleOptions}>
<h3>{t('Rule Options')}</h3>
<div className={styles.leftPaneOverlay} />
{ruleOptions}
<ButtonRow>
<Button className="btn-primary" icon="chevron-left" label={t('Back')} onClickAsync={::this.hideRuleOptions}/>
</ButtonRow>
</div>
</div>
<div className={styles.paneDivider}>
<div className={styles.paneDividerSolidBackground}/>
</div>
</div>
</Form>
<div className={styles.rightPane}>
<div className={styles.rightPaneInner}>
{selectedRule &&
<RuleSettingsPane rule={selectedRule} fields={this.props.fields} onChange={::this.onRuleSettingsPaneUpdated} onClose={::this.onRuleSettingsPaneClose} forceShowValidation={this.isFormValidationShown()}/>}
</div>
</div>
</div>
</div>
);
}