+
+
+
-
{t('Rules')}
+
{t('Rules')}
-
+
-
-
this.onRulesChanged(rulesTree)}
- isVirtualized={false}
- canDrop={ data => !data.nextParent || (data.nextParent.rule.type in this.compositeRuleTypes) }
- generateNodeProps={data => ({
- buttons: [
- await this.showRuleOptions(data)} className={styles.ruleActionLink}>,
- await this.onRuleDelete(data)} className={styles.ruleActionLink}>
- ]
- })}
- />
-
-
-
-
-
-
-
+
+
this.onRulesChanged(rulesTree)}
+ isVirtualized={false}
+ canDrop={ data => !data.nextParent || (ruleHelpers.isCompositeRuleType(data.nextParent.rule.type)) }
+ generateNodeProps={data => ({
+ buttons: [
+ await this.showRuleOptions(data)} className={styles.ruleActionLink}>,
+ await this.deleteRule(data)} className={styles.ruleActionLink}>
+ ]
+ })}
+ />
-
-
-
-
{t('Rule Options')}
+
- {ruleOptions}
-
-
-
-
-
-
+
-
+
+
+
+ {selectedRule &&
+ }
+
+
+
);
}
diff --git a/client/src/lists/segments/RuleSettingsPane.js b/client/src/lists/segments/RuleSettingsPane.js
new file mode 100644
index 00000000..f301fbfa
--- /dev/null
+++ b/client/src/lists/segments/RuleSettingsPane.js
@@ -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 =
+
+ } 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 =
;
+ 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 =
+
+ const ruleType = this.getFormValue('type');
+ if (ruleType) {
+ ruleSettings = ruleHelpers.primitiveRuleTypes[colType][ruleType].form;
+ }
+ }
+ }
+ }
+
+ ruleOptions =
+
+ {ruleColumnSelect}
+ {ruleTypeSelect}
+ {ruleSettings}
+
;
+ }
+
+
+ return (
+
+
{t('Rule Options')}
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/client/src/lists/segments/rule-helpers.js b/client/src/lists/segments/rule-helpers.js
new file mode 100644
index 00000000..0ce05069
--- /dev/null
+++ b/client/src/lists/segments/rule-helpers.js
@@ -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:
,
+ 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:
,
+ 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:
// FIXME
+ };
+
+ const birthdayRelativeValueSettings = {
+ form:
// FIXME
+ };
+
+ const dateValueSettings = {
+ form:
// FIXME
+ };
+
+ const dateRelativeValueSettings = {
+ form:
// 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;
+}
+
diff --git a/client/src/reports/templates/CUD.js b/client/src/reports/templates/CUD.js
index bb84696c..fa0be414 100644
--- a/client/src/reports/templates/CUD.js
+++ b/client/src/reports/templates/CUD.js
@@ -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);
diff --git a/models/segments.js b/models/segments.js
index 64e315e2..019988fd 100644
--- a/models/segments.js
+++ b/models/segments.js
@@ -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,
diff --git a/routes/rest/segments.js b/routes/rest/segments.js
index 6b1f63cd..8106d357 100644
--- a/routes/rest/segments.js
+++ b/routes/rest/segments.js
@@ -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();
});