Snapshot before refactoring the rule settings to a separate component
This commit is contained in:
parent
6fbbe9a497
commit
d0a714b3d4
6 changed files with 474 additions and 149 deletions
|
@ -76,7 +76,7 @@ export default class CUD extends Component {
|
||||||
data.renderTemplate = '';
|
data.renderTemplate = '';
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'checkbox':
|
case 'checkbox-grouped':
|
||||||
case 'radio-grouped':
|
case 'radio-grouped':
|
||||||
case 'dropdown-grouped':
|
case 'dropdown-grouped':
|
||||||
case 'json':
|
case 'json':
|
||||||
|
@ -240,7 +240,7 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
data.settings = {};
|
data.settings = {};
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'checkbox':
|
case 'checkbox-grouped':
|
||||||
case 'radio-grouped':
|
case 'radio-grouped':
|
||||||
case 'dropdown-grouped':
|
case 'dropdown-grouped':
|
||||||
case 'json':
|
case 'json':
|
||||||
|
@ -324,7 +324,7 @@ export default class CUD extends Component {
|
||||||
</Fieldset>;
|
</Fieldset>;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'checkbox':
|
case 'checkbox-grouped':
|
||||||
case 'radio-grouped':
|
case 'radio-grouped':
|
||||||
case 'dropdown-grouped':
|
case 'dropdown-grouped':
|
||||||
fieldSettings =
|
fieldSettings =
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function getFieldTypes(t) {
|
||||||
number: {
|
number: {
|
||||||
label: t('Number'),
|
label: t('Number'),
|
||||||
},
|
},
|
||||||
checkbox: {
|
'checkbox-grouped': {
|
||||||
label: t('Checkboxes (from option fields)'),
|
label: t('Checkboxes (from option fields)'),
|
||||||
},
|
},
|
||||||
'radio-grouped': {
|
'radio-grouped': {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||||
import { translate, Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton, Toolbar} from '../../lib/page';
|
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton, Toolbar} from '../../lib/page';
|
||||||
import {
|
import {
|
||||||
withForm, Form, FormSendMethod, InputField, ButtonRow, Button, Fieldset
|
withForm, Form, FormSendMethod, InputField, ButtonRow, Button, Fieldset, Dropdown, TreeTableSelect
|
||||||
} from '../../lib/form';
|
} from '../../lib/form';
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||||
import {DeleteModalDialog} from "../../lib/delete";
|
import {DeleteModalDialog} from "../../lib/delete";
|
||||||
|
@ -17,6 +17,7 @@ import HTML5Backend from 'react-dnd-html5-backend';
|
||||||
import TouchBackend from 'react-dnd-touch-backend';
|
import TouchBackend from 'react-dnd-touch-backend';
|
||||||
import SortableTree from 'react-sortable-tree';
|
import SortableTree from 'react-sortable-tree';
|
||||||
import {ActionLink, Icon} from "../../lib/bootstrap-components";
|
import {ActionLink, Icon} from "../../lib/bootstrap-components";
|
||||||
|
import { getFieldTypes } from '../fields/field-types';
|
||||||
|
|
||||||
// https://stackoverflow.com/a/4819886/1601953
|
// https://stackoverflow.com/a/4819886/1601953
|
||||||
const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
|
const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
|
||||||
|
@ -31,55 +32,238 @@ export default class CUD extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.compoundRuleTypes = [ 'all', 'some', 'one', 'none' ];
|
const t = props.t;
|
||||||
|
|
||||||
/*
|
this.fieldTypes = getFieldTypes(t);
|
||||||
const allRule = {
|
|
||||||
type: 'all'
|
|
||||||
};
|
|
||||||
|
|
||||||
const otherRule = {
|
this.predefColumns = [
|
||||||
type: 'eq'
|
|
||||||
};
|
|
||||||
|
|
||||||
const sampleRules = [
|
|
||||||
{
|
{
|
||||||
type: 'all',
|
column: 'email',
|
||||||
rules: [
|
name: t('Email address'),
|
||||||
{
|
type: 'text',
|
||||||
type: 'some',
|
tag: 'EMAIL'
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
type: 'eq',
|
|
||||||
value: 11
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'eq',
|
column: 'opt_in_country',
|
||||||
value: 9
|
name: t('Signup country'),
|
||||||
}
|
type: 'text'
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'some',
|
column: 'created',
|
||||||
rules: [
|
name: t('Sign up date'),
|
||||||
{
|
type: 'date'
|
||||||
type: 'eq',
|
|
||||||
value: 3
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'eq',
|
column: 'latest_open',
|
||||||
value: 7
|
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.state = {
|
this.state = {
|
||||||
rules: [],
|
|
||||||
rulesTree: this.getTreeFromRules([])
|
rulesTree: this.getTreeFromRules([])
|
||||||
|
// There is no ruleOptionsVisible here. We have 3 state logic for the visibility:
|
||||||
|
// Undef - not shown, True - shown with entry animation, False - hidden with exit animation
|
||||||
};
|
};
|
||||||
|
|
||||||
this.initForm();
|
this.initForm();
|
||||||
|
@ -92,6 +276,13 @@ export default class CUD extends Component {
|
||||||
entity: PropTypes.object
|
entity: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getColumnType(column) {
|
||||||
|
const field = this.props.fields.find(fld => x.column === column);
|
||||||
|
if (field) {
|
||||||
|
return field.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getRulesFromTree(tree) {
|
getRulesFromTree(tree) {
|
||||||
const rules = [];
|
const rules = [];
|
||||||
for (const node of tree) {
|
for (const node of tree) {
|
||||||
|
@ -123,16 +314,61 @@ export default class CUD extends Component {
|
||||||
return tree;
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.entity) {
|
if (this.props.entity) {
|
||||||
|
this.setState({
|
||||||
|
rulesTree: this.getTreeFromRules(this.props.entity.settings.rootRule.rules)
|
||||||
|
});
|
||||||
|
|
||||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||||
// FIXME populate all others from settings
|
data.rootRuleType = data.settings.rootRule.type;
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.populateFormValues({
|
this.populateFormValues({
|
||||||
name: '',
|
name: '',
|
||||||
settingsJSON: ''
|
settings: {
|
||||||
|
rootRule: {
|
||||||
|
type: 'all',
|
||||||
|
rules: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rootRuleType: 'all'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,11 +382,20 @@ export default class CUD extends Component {
|
||||||
state.setIn(['name', 'error'], null);
|
state.setIn(['name', 'error'], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME - validate rule
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitHandler() {
|
localValidateSelectedRule() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async doSubmit(stay) {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
|
if (!this.localValidateSelectedRule()) {
|
||||||
|
// FIXME
|
||||||
|
}
|
||||||
|
|
||||||
let sendMethod, url;
|
let sendMethod, url;
|
||||||
if (this.props.entity) {
|
if (this.props.entity) {
|
||||||
sendMethod = FormSendMethod.PUT;
|
sendMethod = FormSendMethod.PUT;
|
||||||
|
@ -165,12 +410,29 @@ export default class CUD extends Component {
|
||||||
this.setFormStatusMessage('info', t('Saving ...'));
|
this.setFormStatusMessage('info', t('Saving ...'));
|
||||||
|
|
||||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||||
// FIXME - make sure settings is correct and delete all others
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (submitSuccessful) {
|
if (submitSuccessful) {
|
||||||
|
if (stay) {
|
||||||
|
await this.loadFormValues();
|
||||||
this.enableForm();
|
this.enableForm();
|
||||||
this.setFormStatusMessage('success', t('Segment saved'));
|
this.setFormStatusMessage('success', t('Segment saved'));
|
||||||
|
} else {
|
||||||
|
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/segments`, 'success', t('Segment saved'));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.enableForm();
|
this.enableForm();
|
||||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||||
|
@ -190,11 +452,18 @@ export default class CUD extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async submitAndStay() {
|
||||||
|
await this.formHandleChangedError(async () => await this.doSubmit(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitAndLeave() {
|
||||||
|
await this.formHandleChangedError(async () => await this.doSubmit(false));
|
||||||
|
}
|
||||||
|
|
||||||
async onRuleDelete(data) {
|
async onRuleDelete(data) {
|
||||||
let finishedSearching = false;
|
let finishedSearching = false;
|
||||||
|
|
||||||
function childrenWithoutRule(rules) {
|
function childrenWithoutRule(rules) {
|
||||||
console.log(rules);
|
|
||||||
const newRules = [];
|
const newRules = [];
|
||||||
|
|
||||||
for (const rule of rules) {
|
for (const rule of rules) {
|
||||||
|
@ -218,59 +487,89 @@ export default class CUD extends Component {
|
||||||
return newRules;
|
return newRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.ruleOptionsVisible) {
|
||||||
const rules = childrenWithoutRule(this.state.rules);
|
const rules = childrenWithoutRule(this.state.rules);
|
||||||
console.log(rules);
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
rules,
|
rules,
|
||||||
rulesTree: this.getTreeFromRules(rules)
|
rulesTree: this.getTreeFromRules(rules)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async showRuleOptions(data) {
|
async showRuleOptions(data) {
|
||||||
|
const rule = data.node.rule;
|
||||||
|
|
||||||
|
this.populateFormValues(this.getFormData(rule));
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
ruleOptionsVisible: true
|
ruleOptionsVisible: true,
|
||||||
|
selectedRule: rule
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async hideRuleOptions() {
|
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({
|
this.setState({
|
||||||
ruleOptionsVisible: false
|
ruleOptionsVisible: false,
|
||||||
|
selectedRule: null,
|
||||||
|
rulesTree: this.getTreeFromRules(this.state.rules)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onRulesChanged(rulesTree) {
|
async onRulesChanged(rulesTree) {
|
||||||
|
// This assumes that !this.state.ruleOptionsVisible
|
||||||
|
this.getFormValue('settings').rootRule.rules = this.getRulesFromTree(rulesTree);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
rulesTree,
|
rulesTree
|
||||||
rules: this.getRulesFromTree(rulesTree)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_addRule(type) {
|
async addCompositeRule() {
|
||||||
const rules = this.state.rules;
|
if (!this.state.ruleOptionsVisible) {
|
||||||
|
const rule = {
|
||||||
rules.push({
|
type: 'all',
|
||||||
type,
|
|
||||||
rules: []
|
rules: []
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const rules = this.getFormValue('settings').rootRule.rules;
|
||||||
|
rules.push(rule);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
rules,
|
|
||||||
rulesTree: this.getTreeFromRules(rules)
|
rulesTree: this.getTreeFromRules(rules)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCompositeRule() {
|
|
||||||
this._addRule('all');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addRule() {
|
async addRule() {
|
||||||
this._addRule('eq');
|
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() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const isEdit = !!this.props.entity;
|
const isEdit = !!this.props.entity;
|
||||||
|
const selectedRule = this.state.selectedRule;
|
||||||
|
|
||||||
let ruleOptionsVisibilityClass = '';
|
let ruleOptionsVisibilityClass = '';
|
||||||
if ('ruleOptionsVisible' in this.state) {
|
if ('ruleOptionsVisible' in this.state) {
|
||||||
|
@ -281,6 +580,54 @@ 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 (
|
return (
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -297,16 +644,18 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
<Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</Title>
|
<Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</Title>
|
||||||
|
|
||||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
|
||||||
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
|
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
|
||||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
<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 && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
|
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
|
|
||||||
<h3>{t('Segment Options')}</h3>
|
<h3>{t('Segment Options')}</h3>
|
||||||
|
|
||||||
<InputField id="name" label={t('Name')} />
|
<InputField id="name" label={t('Name')} />
|
||||||
|
<Dropdown id="rootRuleType" label={t('Toplevel match type')} options={this.compositeRuleTypeOptions} />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
@ -327,7 +676,7 @@ export default class CUD extends Component {
|
||||||
treeData={this.state.rulesTree}
|
treeData={this.state.rulesTree}
|
||||||
onChange={rulesTree => this.onRulesChanged(rulesTree)}
|
onChange={rulesTree => this.onRulesChanged(rulesTree)}
|
||||||
isVirtualized={false}
|
isVirtualized={false}
|
||||||
canDrop={ data => !data.nextParent || this.compoundRuleTypes.includes(data.nextParent.rule.type) }
|
canDrop={ data => !data.nextParent || (data.nextParent.rule.type in this.compositeRuleTypes) }
|
||||||
generateNodeProps={data => ({
|
generateNodeProps={data => ({
|
||||||
buttons: [
|
buttons: [
|
||||||
<ActionLink onClickAsync={async () => await this.showRuleOptions(data)} className={styles.ruleActionLink}><Icon name="edit"/></ActionLink>,
|
<ActionLink onClickAsync={async () => await this.showRuleOptions(data)} className={styles.ruleActionLink}><Icon name="edit"/></ActionLink>,
|
||||||
|
@ -349,7 +698,8 @@ export default class CUD extends Component {
|
||||||
<div className={styles.rightPaneInner}>
|
<div className={styles.rightPaneInner}>
|
||||||
<div className={styles.ruleOptions}>
|
<div className={styles.ruleOptions}>
|
||||||
<h3>{t('Rule Options')}</h3>
|
<h3>{t('Rule Options')}</h3>
|
||||||
<InputField id="name" label={t('Name')}/>
|
|
||||||
|
{ruleOptions}
|
||||||
|
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button className="btn-primary" icon="chevron-left" label={t('Back')} onClickAsync={::this.hideRuleOptions}/>
|
<Button className="btn-primary" icon="chevron-left" label={t('Back')} onClickAsync={::this.hideRuleOptions}/>
|
||||||
|
|
|
@ -6,6 +6,20 @@ $mobileAnimationStartPosition: 100px;
|
||||||
$desktopLeftPaneResidualWidth: 200px;
|
$desktopLeftPaneResidualWidth: 200px;
|
||||||
$desktopAnimationStartPosition: 300px;
|
$desktopAnimationStartPosition: 300px;
|
||||||
|
|
||||||
|
@mixin optionsHidden {
|
||||||
|
transform: translateX($mobileAnimationStartPosition);
|
||||||
|
@media (min-width: $desktopMinWidth) {
|
||||||
|
transform: translateX($desktopAnimationStartPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin optionsVisible {
|
||||||
|
transform: translateX($mobileLeftPaneResidualWidth);
|
||||||
|
@media (min-width: $desktopMinWidth) {
|
||||||
|
transform: translateX($desktopLeftPaneResidualWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +42,7 @@ $desktopAnimationStartPosition: 300px;
|
||||||
.ruleTree {
|
.ruleTree {
|
||||||
background: #fbfbfb;
|
background: #fbfbfb;
|
||||||
border: #cfcfcf 1px solid;
|
border: #cfcfcf 1px solid;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
padding: 10px 0px;
|
padding: 10px 0px;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
@ -62,10 +76,7 @@ $desktopAnimationStartPosition: 300px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: url('./divider.png') repeat-y;
|
background: url('./divider.png') repeat-y;
|
||||||
|
|
||||||
transform: translateX($mobileAnimationStartPosition);
|
@include optionsHidden;
|
||||||
@media (min-width: $desktopMinWidth) {
|
|
||||||
transform: translateX($desktopAnimationStartPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
padding-left: 50px;
|
padding-left: 50px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -89,10 +100,7 @@ $desktopAnimationStartPosition: 300px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
transform: translateX($mobileAnimationStartPosition);
|
@include optionsHidden;
|
||||||
@media (min-width: $desktopMinWidth) {
|
|
||||||
transform: translateX($desktopAnimationStartPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -119,10 +127,7 @@ $desktopAnimationStartPosition: 300px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
||||||
transform: translateX($mobileLeftPaneResidualWidth);
|
@include optionsVisible;
|
||||||
@media (min-width: $desktopMinWidth) {
|
|
||||||
transform: translateX($desktopLeftPaneResidualWidth);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightPane {
|
.rightPane {
|
||||||
|
@ -130,10 +135,7 @@ $desktopAnimationStartPosition: 300px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
||||||
transform: translateX($mobileLeftPaneResidualWidth);
|
@include optionsVisible;
|
||||||
@media (min-width: $desktopMinWidth) {
|
|
||||||
transform: translateX($desktopLeftPaneResidualWidth);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,7 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
settings.groupTemplate = field.group_template;
|
settings.groupTemplate = field.group_template;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'checkbox') {
|
if (['checkbox', 'dropdown', 'radio'].includes(type)) {
|
||||||
settings.groupTemplate = field.group_template;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['dropdown', 'radio'].includes(type)) {
|
|
||||||
settings.groupTemplate = field.group_template;
|
settings.groupTemplate = field.group_template;
|
||||||
type = type + '-grouped';
|
type = type + '-grouped';
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,19 +40,17 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
if (oldRule.column in predefColumns) {
|
if (oldRule.column in predefColumns) {
|
||||||
fieldType = predefColumns[oldRule.column];
|
fieldType = predefColumns[oldRule.column];
|
||||||
} else {
|
} else {
|
||||||
const field = await knex('custom_fields').where({list: segment.list, type: 'like', column: oldRule.column}).select(['type']).first();
|
const field = await knex('custom_fields').where({list: segment.list, column: oldRule.column}).select(['type']).first();
|
||||||
if (field) {
|
if (field) {
|
||||||
fieldType = field.type;
|
fieldType = field.type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case 'string':
|
case 'text':
|
||||||
|
case 'website':
|
||||||
rules.push({ column: oldRule.column, value: oldSettings.value });
|
rules.push({ column: oldRule.column, value: oldSettings.value });
|
||||||
break;
|
break;
|
||||||
case 'boolean':
|
|
||||||
rules.push({ type: 'eq', column: oldRule.column, value: oldSettings.value });
|
|
||||||
break;
|
|
||||||
case 'number':
|
case 'number':
|
||||||
if (oldSettings.range) {
|
if (oldSettings.range) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
|
@ -79,75 +77,54 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'birthday':
|
case 'birthday':
|
||||||
if (oldSettings.range) {
|
|
||||||
if (oldSettings.start && oldSettings.end) {
|
|
||||||
if (type === 'all') {
|
|
||||||
rules.push({ type: 'birthdayGe', column: oldRule.column, value: oldSettings.start});
|
|
||||||
rules.push({ type: 'birthdayLe', column: oldRule.column, value: oldSettings.end});
|
|
||||||
} else {
|
|
||||||
rules.push({
|
|
||||||
type: 'all',
|
|
||||||
rules: [
|
|
||||||
{ type: 'birthdayGe', column: oldRule.column, value: oldSettings.start},
|
|
||||||
{ type: 'birthdayLe', column: oldRule.column, value: oldSettings.end}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (oldSettings.start) {
|
|
||||||
rules.push({ type: 'birthdayGe', column: oldRule.column, value: oldSettings.start });
|
|
||||||
}
|
|
||||||
if (oldSettings.end) {
|
|
||||||
rules.push({ type: 'birthdayLe', column: oldRule.column, value: oldSettings.end });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rules.push({ type: 'birthdayEq', column: oldRule.column, value: oldSettings.value });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'date':
|
case 'date':
|
||||||
if (oldSettings.relativeRange) {
|
if (oldSettings.relativeRange) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
rules.push({ type: 'dateGeNowPlusDays', column: oldRule.column, value: oldSettings.start});
|
rules.push({ type: 'geNowPlusDays', column: oldRule.column, value: oldSettings.start});
|
||||||
rules.push({ type: 'dateLeNowPlusDays', column: oldRule.column, value: oldSettings.end});
|
rules.push({ type: 'leNowPlusDays', column: oldRule.column, value: oldSettings.end});
|
||||||
} else {
|
} else {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
rules: [
|
rules: [
|
||||||
{ type: 'dateGeNowPlusDays', column: oldRule.column, value: oldSettings.start},
|
{ type: 'geNowPlusDays', column: oldRule.column, value: oldSettings.start},
|
||||||
{ type: 'dateLeNowPlusDays', column: oldRule.column, value: oldSettings.end}
|
{ type: 'leNowPlusDays', column: oldRule.column, value: oldSettings.end}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (oldSettings.start) {
|
} else if (oldSettings.start) {
|
||||||
rules.push({ type: 'dateGeNowPlusDays', column: oldRule.column, value: oldSettings.startDirection ? oldSettings.start : -oldSettings.start });
|
rules.push({ type: 'geNowPlusDays', column: oldRule.column, value: oldSettings.startDirection ? oldSettings.start : -oldSettings.start });
|
||||||
}
|
}
|
||||||
if (oldSettings.end) {
|
if (oldSettings.end) {
|
||||||
rules.push({ type: 'dateLeNowPlusDays', column: oldRule.column, value: oldSettings.endDirection ? oldSettings.end : -oldSettings.end });
|
rules.push({ type: 'leNowPlusDays', column: oldRule.column, value: oldSettings.endDirection ? oldSettings.end : -oldSettings.end });
|
||||||
}
|
}
|
||||||
} else if (oldSettings.range) {
|
} else if (oldSettings.range) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
rules.push({ type: 'dateGe', column: oldRule.column, value: oldSettings.start});
|
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start});
|
||||||
rules.push({ type: 'dateLe', column: oldRule.column, value: oldSettings.end});
|
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end});
|
||||||
} else {
|
} else {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
rules: [
|
rules: [
|
||||||
{ type: 'dateGe', column: oldRule.column, value: oldSettings.start},
|
{ type: 'ge', column: oldRule.column, value: oldSettings.start},
|
||||||
{ type: 'dateLe', column: oldRule.column, value: oldSettings.end}
|
{ type: 'le', column: oldRule.column, value: oldSettings.end}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (oldSettings.start) {
|
} else if (oldSettings.start) {
|
||||||
rules.push({ type: 'dateGe', column: oldRule.column, value: oldSettings.start });
|
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start });
|
||||||
}
|
}
|
||||||
if (oldSettings.end) {
|
if (oldSettings.end) {
|
||||||
rules.push({ type: 'dateLe', column: oldRule.column, value: oldSettings.end });
|
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rules.push({ type: 'dateEq', column: oldRule.column, value: oldSettings.value });
|
rules.push({ type: 'eq', column: oldRule.column, value: oldSettings.value });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'option':
|
||||||
|
rules.push({ type: 'eq', column: oldRule.column, value: oldSettings.value });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown rule for column ${oldRule.column} with field type ${fieldType}`);
|
throw new Error(`Unknown rule for column ${oldRule.column} with field type ${fieldType}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue