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

@ -238,7 +238,7 @@ class InputField extends Component {
}
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
<input type={type} value={owner.getFormValue(id)} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
<input type={type} value={owner.getFormValue(id) || ''} placeholder={props.placeholder} id={htmlId} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
);
}
}
@ -290,7 +290,7 @@ class TextArea extends Component {
const htmlId = 'form_' + id;
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
<textarea id={htmlId} value={owner.getFormValue(id)} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}></textarea>
<textarea id={htmlId} value={owner.getFormValue(id) || ''} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}></textarea>
);
}
}
@ -841,10 +841,14 @@ function withForm(target) {
})
};
const onChangeCallbacks = this.state.formSettings.onChange || {};
const onChangeCallback = this.state.formSettings.onChange || {};
if (onChangeCallbacks[key]) {
onChangeCallbacks[key](newState, key, oldValue, value);
if (typeof onChangeCallback === 'object') {
if (onChangeCallback[key]) {
onChangeCallback[key](newState, key, oldValue, value);
}
} else {
onChangeCallback(newState, key, oldValue, value);
}
return newState;

View file

@ -433,8 +433,8 @@ export default class CUD extends Component {
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/edit`}
listUrl={`/lists/fields/${this.props.list.id}`}
cudUrl={`/lists/${this.props.list.id}/fields/${this.props.entity.id}/edit`}
listUrl={`/lists/${this.props.list.id}/fields`}
deletingMsg={t('Deleting field ...')}
deletedMsg={t('Field deleted')}/>
}
@ -464,7 +464,7 @@ export default class CUD extends Component {
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{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/${this.props.list.id}/fields/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>

View file

@ -163,7 +163,7 @@ const getStructure = t => {
export default function() {
ReactDOM.render(
<I18nextProvider i18n={ i18n }><Section root='/lists' structure={getStructure}/></I18nextProvider>,
<I18nextProvider i18n={ i18n }><Section root='/lists/1/segments/create' /* FIXME */ structure={getStructure}/></I18nextProvider>,
document.getElementById('root')
);
};

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;
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
if (state.getIn(['selectedRule', 'value']) === null) {
state.setIn(['selectedRule', 'error'], null);
}
localValidateSelectedRule() {
}
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,17 +329,24 @@ export default class CUD extends Component {
<Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
{isEdit ?
<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')}/>
<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`}/>}
<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 />
@ -664,7 +355,7 @@ export default class CUD extends Component {
<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}/>
<Button className="btn-primary" label={t('Add Rule')} onClickAsync={::this.addPrimitiveRule}/>
</Toolbar>
<h3>{t('Rules')}</h3>
@ -676,11 +367,11 @@ export default class CUD extends Component {
treeData={this.state.rulesTree}
onChange={rulesTree => this.onRulesChanged(rulesTree)}
isVirtualized={false}
canDrop={ data => !data.nextParent || (data.nextParent.rule.type in this.compositeRuleTypes) }
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.onRuleDelete(data)} className={styles.ruleActionLink}><Icon name="remove"/></ActionLink>
<ActionLink onClickAsync={async () => await this.deleteRule(data)} className={styles.ruleActionLink}><Icon name="remove"/></ActionLink>
]
})}
/>
@ -696,20 +387,12 @@ export default class CUD extends Component {
<div className={styles.rightPane}>
<div className={styles.rightPaneInner}>
<div className={styles.ruleOptions}>
<h3>{t('Rule Options')}</h3>
{ruleOptions}
<ButtonRow>
<Button className="btn-primary" icon="chevron-left" label={t('Back')} onClickAsync={::this.hideRuleOptions}/>
</ButtonRow>
{selectedRule &&
<RuleSettingsPane rule={selectedRule} fields={this.props.fields} onChange={::this.onRuleSettingsPaneUpdated} onClose={::this.onRuleSettingsPaneClose} forceShowValidation={this.isFormValidationShown()}/>}
</div>
</div>
</div>
</div>
</Form>
</div>
);
}
}

View file

@ -0,0 +1,204 @@
'use strict';
import React, {Component} from "react";
import PropTypes from "prop-types";
import {translate} from "react-i18next";
import {requiresAuthenticatedUser, withPageHelpers} from "../../lib/page";
import {Button, ButtonRow, Dropdown, Form, TableSelect, withForm} from "../../lib/form";
import {withErrorHandling} from "../../lib/error-handling";
import {getRuleHelpers} from "./rule-helpers";
import {getFieldTypes} from "../fields/field-types";
import styles from "./CUD.scss";
@translate()
@withForm
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component {
constructor(props) {
super(props);
const t = props.t;
this.ruleHelpers = getRuleHelpers(t, props.fields);
this.fieldTypes = getFieldTypes(t);
this.state = {};
this.initForm({
onChange: ::this.onFormChange
});
}
static propTypes = {
rule: PropTypes.object.isRequired,
fields: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
forceShowValidation: PropTypes.bool.isRequired
}
updateStateFromProps(props, populateForm) {
if (populateForm) {
const rule = props.rule;
const ruleHelpers = this.ruleHelpers;
let data;
if (!ruleHelpers.isCompositeRuleType(rule.type)) {
const settings = ruleHelpers.getRuleTypeSettings(rule);
if (settings) {
data = settings.getFormData(rule);
} else {
data = {}; // This handles the case of a new rule, which does not have a type and column yet
}
data.type = rule.type || ''; // On '', we display label "--SELECT--" in the type dropdown. Null would not be accepted by React.
data.column = rule.column;
} else {
data = {
type: rule.type
};
}
this.populateFormValues(data);
}
if (props.forceShowValidation) {
this.showFormValidation();
}
}
componentDidMount() {
this.updateStateFromProps(this.props, true);
}
componentWillReceiveProps(nextProps) {
this.updateStateFromProps(nextProps, this.props.rule !== nextProps.rule);
}
localValidateFormValues(state) {
const t = this.props.t;
const ruleHelpers = this.ruleHelpers;
for (const key of state.keys()) {
state.setIn([key, 'error'], null);
}
const ruleType = state.getIn(['type', 'value']);
if (!ruleHelpers.isCompositeRuleType(ruleType)) {
const columnType = state.getIn(['column', 'value']);
if (columnType) {
const colType = ruleHelpers.getColumnType(columnType);
if (ruleType) {
const settings = ruleHelpers.primitiveRuleTypes[colType][ruleType];
settings.validate(state);
} else {
state.setIn(['type', 'error'], t('Type must be selected'));
}
} else {
state.setIn(['column', 'error'], t('Field must be selected'));
}
}
}
onFormChange(newState) {
const noErrors = !newState.formState.get('data').find(attr => attr.get('error'));
if (noErrors) {
const rule = this.props.rule;
const ruleHelpers = this.ruleHelpers;
rule.type = newState.formState.getIn(['data','type','value']);
if (!ruleHelpers.isCompositeRuleType(rule.type)) {
rule.column = newState.formState.getIn(['data','column','value']);
const settings = this.ruleHelpers.getRuleTypeSettings(rule);
settings.assignRuleSettings(rule, key => newState.formState.getIn(['data', key, 'value']));
}
this.props.onChange(false);
} else {
this.props.onChange(true);
}
}
async closeForm() {
if (this.isFormWithoutErrors()) {
this.props.onClose();
} else {
this.showFormValidation();
}
}
render() {
const t = this.props.t;
const rule = this.props.rule;
const ruleHelpers = this.ruleHelpers;
let ruleOptions = null;
if (ruleHelpers.isCompositeRuleType(rule.type)) {
ruleOptions = <Dropdown id="type" label={t('Type')} options={ruleHelpers.getCompositeRuleTypeOptions()} />
} else {
const ruleColumnOptionsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Type') },
{ data: 3, title: t('Merge Tag') }
];
const ruleColumnOptions = ruleHelpers.fields.map(fld => [ fld.column, fld.name, this.fieldTypes[fld.type].label, fld.tag || '' ]);
const ruleColumnSelect = <TableSelect id="column" label={t('Field')} data={ruleColumnOptions} columns={ruleColumnOptionsColumns} dropdown withHeader selectionLabelIndex={1} />;
let ruleTypeSelect = null;
let ruleSettings = null;
const ruleColumn = this.getFormValue('column');
if (ruleColumn) {
const colType = ruleHelpers.getColumnType(ruleColumn);
if (colType) {
const ruleTypeOptions = ruleHelpers.getPrimitiveRuleTypeOptions(colType);
ruleTypeOptions.unshift({ key: '', label: t('-- Select --')});
if (ruleTypeOptions) {
ruleTypeSelect = <Dropdown id="type" label={t('Type')} options={ruleTypeOptions} />
const ruleType = this.getFormValue('type');
if (ruleType) {
ruleSettings = ruleHelpers.primitiveRuleTypes[colType][ruleType].form;
}
}
}
}
ruleOptions =
<div>
{ruleColumnSelect}
{ruleTypeSelect}
{ruleSettings}
</div>;
}
return (
<div className={styles.ruleOptions}>
<h3>{t('Rule Options')}</h3>
<Form stateOwner={this} onSubmitAsync={::this.closeForm}>
{ruleOptions}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="chevron-left" label={t('OK')}/>
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -0,0 +1,369 @@
'use strict';
import React from 'react';
import {InputField} from "../../lib/form";
export function getRuleHelpers(t, fields) {
function formatDate(date) {
return date; // FIXME
}
const ruleHelpers = {};
ruleHelpers.compositeRuleTypes = {
all: {
dropdownLabel: t('All rules must match'),
treeLabel: rule => t('All rules must match')
},
some: {
dropdownLabel: t('At least one rule must match'),
treeLabel: rule => t('At least one rule must match')
},
none: {
dropdownLabel: t('No rule may match'),
treeLabel: rule => t('No rule may match')
}
};
ruleHelpers.primitiveRuleTypes = {};
ruleHelpers.primitiveRuleTypes.text = {
eq: {
dropdownLabel: t('Equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('Match (with SQL LIKE)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with SQL LIKE) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('Match (with regular expressions)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with regular expressions) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('Alphabetically before'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically before "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('Alphabetically before or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically before or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('Alphabetically after'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically after "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('Alphabetically after or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is alphabetically after or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.website = {
eq: {
dropdownLabel: t('Equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('Match (with SQL LIKE)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with SQL LIKE) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('Match (with regular expressions)'),
treeLabel: rule => t('Value in column "{{colName}}" matches (with regular expressions) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.number = {
eq: {
dropdownLabel: t('Equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is equal to {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('Less than'),
treeLabel: rule => t('Value in column "{{colName}}" is less than {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('Less than or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is less than or equal to {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('Greater than'),
treeLabel: rule => t('Value in column "{{colName}}" is greater than {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('Greater than or equal to'),
treeLabel: rule => t('Value in column "{{colName}}" is greater than or equal to {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
// TODO: This generates strings that cannot be statically detected. It will require dynamic discovery of translatable strings.
function getRelativeDateTreeLabel(rule, textFragment) {
if (rule.value === 0) {
return t('Date in column "{{colName}}" ' + textFragment + ' the current date', {colName: ruleHelpers.getColumnName(rule.column)})
} else if (rule.value > 0) {
return t('Date in column "{{colName}}" ' + textFragment + ' {{value}}-th day after the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
} else {
return t('Date in column "{{colName}}" ' + textFragment + ' {{value}}-th day before the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
}
}
ruleHelpers.primitiveRuleTypes.date = ruleHelpers.primitiveRuleTypes.birthday = {
eq: {
dropdownLabel: t('On'),
treeLabel: rule => t('Date in column "{{colName}}" is {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
},
lt: {
dropdownLabel: t('Before'),
treeLabel: rule => t('Date in column "{{colName}}" is before {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
},
le: {
dropdownLabel: t('Before or on'),
treeLabel: rule => t('Date in column "{{colName}}" is before or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
},
gt: {
dropdownLabel: t('After'),
treeLabel: rule => t('Date in column "{{colName}}" is after {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
},
ge: {
dropdownLabel: t('After or on'),
treeLabel: rule => t('Date in column "{{colName}}" is after or on {{value}}', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(rule.value)}),
},
eqNowPlusDays: {
dropdownLabel: t('On x-th day before/after now'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is'),
},
ltNowPlusDays: {
dropdownLabel: t('Before x-th day before/after now'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before'),
},
leNowPlusDays: {
dropdownLabel: t('Before or on x-th day before/after now'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before or on'),
},
gtNowPlusDays: {
dropdownLabel: t('After x-th day before/after now'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after'),
},
geNowPlusDays: {
dropdownLabel: t('After or on x-th day before/after now'),
treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after or on'),
}
};
ruleHelpers.primitiveRuleTypes.option = {
isTrue: {
dropdownLabel: t('Is selected'),
treeLabel: rule => t('Value in column "{{colName}}" is selected', {colName: ruleHelpers.getColumnName(rule.column)}),
},
isFalse: {
dropdownLabel: t('Is not selected'),
treeLabel: rule => t('Value in column "{{colName}}" is not selected', {colName: ruleHelpers.getColumnName(rule.column)}),
}
};
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
eq: {
dropdownLabel: t('Key equal to'),
treeLabel: rule => t('The selected key in column "{{colName}}" is equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('Key match (with SQL LIKE)'),
treeLabel: rule => t('The selected key in column "{{colName}}" matches (with SQL LIKE) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('Key match (with regular expressions)'),
treeLabel: rule => t('The selected key in column "{{colName}}" matches (with regular expressions) "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('Key alphabetically before'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically before "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('Key alphabetically before or equal to'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically before or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('Key alphabetically after'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically after "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('Key alphabetically after or equal to'),
treeLabel: rule => t('The selected key in column "{{colName}}" is alphabetically after or equal to "{{value}}"', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
const stringValueSettings = {
form: <InputField id="value" label={t('Value')} />,
getFormData: rule => ({
value: rule.value
}),
assignRuleSettings: (rule, getter) => {
rule.value = getter('value');
},
validate: state => {
if (!state.getIn(['value', 'value'])) {
state.setIn(['value', 'error'], t('Value must not be empty'));
} else {
state.setIn(['value', 'error'], null);
}
}
};
const numberValueSettings = {
form: <InputField id="value" label={t('Value')} />,
getFormData: rule => ({
value: rule.value.toString()
}),
assignRuleSettings: (rule, getter) => {
rule.value = parseInt(getter('value'));
},
validate: state => {
const value = state.getIn(['value', 'value']);
if (!value) {
state.setIn(['value', 'error'], t('Value must not be empty'));
} else if (isNaN(value)) {
state.setIn(['value', 'error'], t('Value must be a number'));
} else {
state.setIn(['value', 'error'], null);
}
}
};
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,
getFormData: rule => ({}),
assignRuleSettings: (rule, getter) => {},
validate: state => {}
};
function assignSettingsToRuleTypes(ruleTypes, keys, settings) {
for (const key of keys) {
Object.assign(ruleTypes[key], settings);
}
}
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, Object.keys(ruleHelpers.primitiveRuleTypes.text), stringValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, Object.keys(ruleHelpers.primitiveRuleTypes.website), stringValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, Object.keys(ruleHelpers.primitiveRuleTypes.number), numberValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'], birthdayRelativeValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'], dateRelativeValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, Object.keys(ruleHelpers.primitiveRuleTypes.option), optionValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], Object.keys(ruleHelpers.primitiveRuleTypes['dropdown-enum']), stringValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], Object.keys(ruleHelpers.primitiveRuleTypes['radio-enum']), stringValueSettings);
ruleHelpers.getCompositeRuleTypeOptions = () => {
const order = ['all', 'some', 'none'];
return order.map(key => ({ key, label: ruleHelpers.compositeRuleTypes[key].dropdownLabel }));
};
ruleHelpers.getPrimitiveRuleTypeOptions = columnType => {
const order = {
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
website: ['eq', 'like', 're'],
number: ['eq', 'lt', 'le', 'gt', 'ge'],
birthday: ['eq', 'lt', 'le', 'gt', 'ge', 'eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'],
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqNowPlusDays', 'ltNowPlusDays', 'leNowPlusDays', 'gtNowPlusDays', 'geNowPlusDays'],
option: ['isTrue', 'isFalse'],
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
};
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
};
const 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'
}
];
ruleHelpers.fields = [
...predefColumns,
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
];
ruleHelpers.fieldsByColumn = [];
for (const fld of ruleHelpers.fields) {
ruleHelpers.fieldsByColumn[fld.column] = fld;
}
ruleHelpers.getColumnType = column => {
const field = ruleHelpers.fieldsByColumn[column];
if (field) {
return field.type;
}
};
ruleHelpers.getColumnName = column => {
const field = ruleHelpers.fieldsByColumn[column];
if (field) {
return field.name;
}
};
ruleHelpers.getRuleTypeSettings = rule => {
if (ruleHelpers.isCompositeRuleType(rule.type)) {
return ruleHelpers.compositeRuleTypes[rule.type];
} else {
const colType = ruleHelpers.getColumnType(rule.column);
if (colType) {
if (rule.type in ruleHelpers.primitiveRuleTypes[colType]) {
return ruleHelpers.primitiveRuleTypes[colType][rule.type];
}
}
}
};
ruleHelpers.isCompositeRuleType = ruleType => ruleType in ruleHelpers.compositeRuleTypes;
return ruleHelpers;
}

View file

@ -29,6 +29,11 @@ export default class CUD extends Component {
entity: PropTypes.object
}
@withAsyncErrorHandler
async loadFormValues() {
await this.getFormValuesFromURL(`/rest/report-templates/${this.props.entity.id}`);
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity);

View file

@ -4,6 +4,8 @@ const knex = require('../lib/knex');
const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares');
const { enforce, filterObject } = require('../lib/helpers');
const hasher = require('node-object-hash')();
const allowedKeys = new Set(['name', 'settings']);
@ -47,7 +49,7 @@ async function create(context, listId, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
entity.settings = JSON.stringify(entity.params);
entity.settings = JSON.stringify(entity.settings);
const filteredEntity = filterObject(entity, allowedKeys);
filteredEntity.list = listId;
@ -68,12 +70,14 @@ async function updateWithConsistencyCheck(context, listId, entity) {
throw new interoperableErrors.NotFoundError();
}
existing.settings = JSON.parse(existing.settings);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError();
}
entity.settings = JSON.stringify(entity.params);
entity.settings = JSON.stringify(entity.settings);
await tx('segments').where('id', entity.id).update(filterObject(entity, allowedKeys));
});
@ -105,8 +109,10 @@ async function removeRulesByFieldIdTx(tx, context, listId, fieldId) {
}
module.exports = {
hash,
listDTAjax,
list,
getById,
create,
updateWithConsistencyCheck,
remove,

View file

@ -14,6 +14,12 @@ router.getAsync('/segments/:listId', passport.loggedIn, async (req, res) => {
return res.json(await segments.list(req.context, req.params.listId));
});
router.getAsync('/segments/:listId/:segmentId', passport.loggedIn, async (req, res) => {
const segment = await segments.getById(req.context, req.params.listId, req.params.segmentId);
segment.hash = segments.hash(segment);
return res.json(segment);
});
router.postAsync('/segments/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await segments.create(req.context, req.params.listId, req.body);
return res.json();
@ -27,8 +33,8 @@ router.putAsync('/segments/:listId/:segmentId', passport.loggedIn, passport.csrf
return res.json();
});
router.deleteAsync('/segments/:segmentId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await segments.remove(req.context, req.params.listId, req.params.segmentid);
router.deleteAsync('/segments/:listId/:segmentId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await segments.remove(req.context, req.params.listId, req.params.segmentId);
return res.json();
});