diff --git a/app.js b/app.js
index 244854a7..e1191b34 100644
--- a/app.js
+++ b/app.js
@@ -54,6 +54,8 @@ const listsRest = require('./routes/rest/lists');
const formsRest = require('./routes/rest/forms');
const fieldsRest = require('./routes/rest/fields');
const sharesRest = require('./routes/rest/shares');
+const segmentsRest = require('./routes/rest/segments');
+const subscriptionsRest = require('./routes/rest/subscriptions');
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
const usersLegacyIntegration = require('./routes/users-legacy-integration');
@@ -277,6 +279,8 @@ app.use('/rest', listsRest);
app.use('/rest', formsRest);
app.use('/rest', fieldsRest);
app.use('/rest', sharesRest);
+app.use('/rest', segmentsRest);
+app.use('/rest', subscriptionsRest);
if (config.reports && config.reports.enabled === true) {
app.use('/rest', reportTemplatesRest);
diff --git a/client/src/lib/tree.js b/client/src/lib/tree.js
index a0fd819c..a26a18c4 100644
--- a/client/src/lib/tree.js
+++ b/client/src/lib/tree.js
@@ -91,9 +91,11 @@ class TreeTable extends Component {
const data = [];
for (const unsafeEntry of unsafeData) {
const entry = Object.assign({}, unsafeEntry);
- entry.title = ReactDOMServer.renderToStaticMarkup(
{entry.title}
)
- entry.description = ReactDOMServer.renderToStaticMarkup({entry.description}
)
- entry.children = this.sanitizeTreeData(entry.children);
+ entry.title = ReactDOMServer.renderToStaticMarkup({entry.title}
);
+ entry.description = ReactDOMServer.renderToStaticMarkup({entry.description}
);
+ if (entry.children) {
+ entry.children = this.sanitizeTreeData(entry.children);
+ }
data.push(entry);
}
return data;
diff --git a/client/src/lists/CUD.js b/client/src/lists/CUD.js
index 1abd00ae..f2f32a26 100644
--- a/client/src/lists/CUD.js
+++ b/client/src/lists/CUD.js
@@ -81,7 +81,7 @@ export default class CUD extends Component {
}
this.disableForm();
- this.setFormStatusMessage('info', t('Saving list ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.form === 'default') {
diff --git a/client/src/lists/fields/CUD.js b/client/src/lists/fields/CUD.js
index 834b80b1..fe4599d7 100644
--- a/client/src/lists/fields/CUD.js
+++ b/client/src/lists/fields/CUD.js
@@ -11,7 +11,6 @@ import {
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete";
import { getFieldTypes } from './field-types';
-import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators';
import slugify from 'slugify';
@@ -45,6 +44,7 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
list: PropTypes.object,
+ fields: PropTypes.array,
entity: PropTypes.object
}
@@ -58,27 +58,6 @@ export default class CUD extends Component {
}
}
- @withAsyncErrorHandler
- async loadOrderOptions() {
- const t = this.props.t;
-
- const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
-
- const getOrderOptions = fld => {
- return [
- {key: 'none', label: t('Not visible')},
- ...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
- {key: 'end', label: t('End of list')}
- ];
- };
-
- this.setState({
- orderListOptions: getOrderOptions('order_list'),
- orderSubscribeOptions: getOrderOptions('order_subscribe'),
- orderManageOptions: getOrderOptions('order_manage')
- });
- }
-
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
@@ -139,8 +118,6 @@ export default class CUD extends Component {
orderManageOptions: []
});
}
-
- this.loadOrderOptions();
}
localValidateFormValues(state) {
@@ -250,7 +227,7 @@ export default class CUD extends Component {
try {
this.disableForm();
- this.setFormStatusMessage('info', t('Saving field ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') {
@@ -320,6 +297,16 @@ export default class CUD extends Component {
const t = this.props.t;
const isEdit = !!this.props.entity;
+
+ const getOrderOptions = fld => {
+ return [
+ {key: 'none', label: t('Not visible')},
+ ...this.props.fields.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
+ {key: 'end', label: t('End of list')}
+ ];
+ };
+
+
const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
const type = this.getFormValue('type');
@@ -469,9 +456,9 @@ export default class CUD extends Component {
{type !== 'option' &&
}
diff --git a/client/src/lists/forms/CUD.js b/client/src/lists/forms/CUD.js
index c4999d63..f094364c 100644
--- a/client/src/lists/forms/CUD.js
+++ b/client/src/lists/forms/CUD.js
@@ -323,7 +323,7 @@ export default class CUD extends Component {
}
this.disableForm();
- this.setFormStatusMessage('info', t('Saving forms ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.selectedTemplate;
diff --git a/client/src/lists/root.js b/client/src/lists/root.js
index af9d526e..dc1aff77 100644
--- a/client/src/lists/root.js
+++ b/client/src/lists/root.js
@@ -14,6 +14,7 @@ import FieldsList from './fields/List';
import FieldsCUD from './fields/CUD';
import SubscriptionsList from './subscriptions/List';
import SegmentsList from './segments/List';
+import SegmentsCUD from './segments/CUD';
import Share from '../shares/Share';
@@ -58,20 +59,24 @@ const getStructure = t => {
':fieldId([0-9]+)': {
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
resolve: {
- field: params => `/rest/fields/${params.listId}/${params.fieldId}`
+ field: params => `/rest/fields/${params.listId}/${params.fieldId}`,
+ fields: params => `/rest/fields/${params.listId}`
},
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
- render: props =>
+ render: props =>
}
}
},
create: {
- title: t('Create Field'),
- render: props =>
+ title: t('Create'),
+ resolve: {
+ fields: params => `/rest/fields/${params.listId}`
+ },
+ render: props =>
}
}
},
@@ -79,7 +84,31 @@ const getStructure = t => {
title: t('Segments'),
link: params => `/lists/${params.listId}/segments`,
visible: resolved => resolved.list.permissions.includes('manageSegments'),
- render: props =>
+ render: props => ,
+ children: {
+ ':segmentId([0-9]+)': {
+ title: resolved => t('Segment "{{name}}"', {name: resolved.segment.name}),
+ resolve: {
+ segment: params => `/rest/segments/${params.listId}/${params.segmentId}`,
+ fields: params => `/rest/fields/${params.listId}`
+ },
+ link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
+ navs: {
+ ':action(edit|delete)': {
+ title: t('Edit'),
+ link: params => `/lists/${params.listId}/segments/${params.segmentId}/edit`,
+ render: props =>
+ }
+ }
+ },
+ create: {
+ title: t('Create'),
+ resolve: {
+ fields: params => `/rest/fields/${params.listId}`
+ },
+ render: props =>
+ }
+ }
},
share: {
title: t('Share'),
diff --git a/client/src/lists/segments/CUD.js b/client/src/lists/segments/CUD.js
index 834b80b1..0d0cc035 100644
--- a/client/src/lists/segments/CUD.js
+++ b/client/src/lists/segments/CUD.js
@@ -5,17 +5,12 @@ import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
import {
- withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
- Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField
+ withForm, Form, FormSendMethod, InputField, ButtonRow, Button
} from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete";
-import { getFieldTypes } from './field-types';
-import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors';
-import validators from '../../../../shared/validators';
-import slugify from 'slugify';
-import { parseDate, parseBirthday } from '../../../../shared/fields';
+import {TreeTable} from "../../lib/tree";
@translate()
@withForm
@@ -28,119 +23,28 @@ export default class CUD extends Component {
this.state = {};
- this.fieldTypes = getFieldTypes(props.t);
-
- this.initForm({
- serverValidation: {
- url: `/rest/fields-validate/${this.props.list.id}`,
- changed: ['key'],
- extra: ['id']
- },
- onChange: {
- name: ::this.onChangeName
- }
- });
+ this.initForm();
}
static propTypes = {
action: PropTypes.string.isRequired,
list: PropTypes.object,
+ fields: PropTypes.array,
entity: PropTypes.object
}
- onChangeName(state, attr, oldValue, newValue) {
- const oldComputedKey = ('MERGE_' + slugify(oldValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
- const oldKey = state.formState.getIn(['data', 'key', 'value']);
-
- if (oldKey === '' || oldKey === oldComputedKey) {
- const newKey = ('MERGE_' + slugify(newValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
- state.formState = state.formState.setIn(['data', 'key', 'value'], newKey);
- }
- }
-
- @withAsyncErrorHandler
- async loadOrderOptions() {
- const t = this.props.t;
-
- const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
-
- const getOrderOptions = fld => {
- return [
- {key: 'none', label: t('Not visible')},
- ...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
- {key: 'end', label: t('End of list')}
- ];
- };
-
- this.setState({
- orderListOptions: getOrderOptions('order_list'),
- orderSubscribeOptions: getOrderOptions('order_subscribe'),
- orderManageOptions: getOrderOptions('order_manage')
- });
- }
-
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
- data.settings = data.settings || {};
-
- if (data.default_value === null) {
- data.default_value = '';
- }
-
- if (data.type !== 'option') {
- data.group = null;
- }
-
- data.enumOptions = '';
- data.dateFormat = 'eur';
- data.renderTemplate = '';
-
- switch (data.type) {
- case 'checkbox':
- case 'radio-grouped':
- case 'dropdown-grouped':
- case 'json':
- data.renderTemplate = data.settings.renderTemplate;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- data.enumOptions = this.renderEnumOptions(data.settings.enumOptions);
- data.renderTemplate = data.settings.renderTemplate;
- break;
-
- case 'date':
- case 'birthday':
- data.dateFormat = data.settings.dateFormat;
- break;
- }
-
- data.orderListBefore = data.orderListBefore.toString();
- data.orderSubscribeBefore = data.orderSubscribeBefore.toString();
- data.orderManageBefore = data.orderManageBefore.toString();
+ // FIXME populate all others from settings
});
} else {
this.populateFormValues({
name: '',
- type: 'text',
- key: '',
- default_value: '',
- group: null,
- renderTemplate: '',
- enumOptions: '',
- dateFormat: 'eur',
- orderListBefore: 'end', // possible values are / 'end' / 'none'
- orderSubscribeBefore: 'end',
- orderManageBefore: 'end',
- orderListOptions: [],
- orderSubscribeOptions: [],
- orderManageOptions: []
+ settingsJSON: ''
});
}
-
- this.loadOrderOptions();
}
localValidateFormValues(state) {
@@ -152,151 +56,31 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], null);
}
- const keyServerValidation = state.getIn(['key', 'serverValidation']);
- if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
- state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.'));
- } else if (!keyServerValidation) {
- state.setIn(['key', 'error'], t('Validation is in progress...'));
- } else if (keyServerValidation.exists) {
- state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.'));
- } else {
- state.setIn(['key', 'error'], null);
- }
-
- const type = state.getIn(['type', 'value']);
-
- const group = state.getIn(['group', 'value']);
- if (type === 'option' && !group) {
- state.setIn(['group', 'error'], t('Group has to be selected'));
- } else {
- state.setIn(['group', 'error'], null);
- }
-
- const defaultValue = state.getIn(['default_value', 'value']);
- if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
- state.setIn(['default_value', 'error'], t('Default value is not integer number'));
- } else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) {
- state.setIn(['default_value', 'error'], t('Default value is not a properly formatted date'));
- } else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) {
- state.setIn(['default_value', 'error'], t('Default value is not a properly formatted birthday date'));
- } else {
- state.setIn(['default_value', 'error'], null);
- }
-
- if (type === 'radio-enum' || type === 'dropdown-enum') {
- const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value']));
- if (enumOptions.errors) {
- state.setIn(['enumOptions', 'error'], {enumOptions.errors.map((err, idx) =>
{err}
)}
);
- } else {
- state.setIn(['enumOptions', 'error'], null);
-
- if (defaultValue !== '' && !(defaultValue in enumOptions.options)) {
- state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
- }
- }
- } else {
- state.setIn(['enumOptions', 'error'], null);
- }
}
- parseEnumOptions(text) {
- const t = this.props.t;
- const errors = [];
- const options = {};
-
- const lines = text.split('\n');
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
- const line = lines[lineIdx].trim();
-
- if (line != '') {
- const matches = line.match(/^([^|]*)[|](.*)$/);
- if (matches) {
- const key = matches[1].trim();
- const label = matches[2].trim();
- options[key] = label;
- } else {
- errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1}));
- }
- }
- }
-
- if (errors.length) {
- return {
- errors
- };
- } else {
- return {
- options
- };
- }
- }
-
- renderEnumOptions(options) {
- return Object.keys(options).map(key => `${key}|${options[key]}`).join('\n');
- }
-
-
async submitHandler() {
const t = this.props.t;
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
- url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
+ url = `/rest/segments/${this.props.list.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
- url = `/rest/fields/${this.props.list.id}`
+ url = `/rest/segments/${this.props.list.id}`
}
try {
this.disableForm();
- this.setFormStatusMessage('info', t('Saving field ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
- if (data.default_value.trim() === '') {
- data.default_value = null;
- }
-
- if (data.type !== 'option') {
- data.group = null;
- }
-
- data.settings = {};
- switch (data.type) {
- case 'checkbox':
- case 'radio-grouped':
- case 'dropdown-grouped':
- case 'json':
- data.settings.renderTemplate = data.renderTemplate;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- data.settings.enumOptions = this.parseEnumOptions(data.enumOptions).options;
- data.settings.renderTemplate = data.renderTemplate;
- break;
-
- case 'date':
- case 'birthday':
- data.settings.dateFormat = data.dateFormat;
- break;
- }
-
- delete data.renderTemplate;
- delete data.enumOptions;
- delete data.dateFormat;
-
- if (data.type === 'option') {
- data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none';
- } else {
- data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore;
- data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore;
- data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore;
- }
+ // FIXME - make sure settings is correct and delete all others
});
if (submitSuccessful) {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved'));
+ this.enableForm();
+ this.setFormStatusMessage('success', t('Segment saved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
@@ -320,123 +104,50 @@ export default class CUD extends Component {
const t = this.props.t;
const isEdit = !!this.props.entity;
- const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
+ const sampleTreeData = [
+ {
+ key: 'a',
+ title: 'A',
+ expanded: true,
+ children: [
+ {
+ key: 'aa',
+ title: 'AA',
+ expanded: true,
+ children: [
+ {
+ key: 'aaa',
+ title: 'AAA',
+ expanded: true
+ },
+ {
+ key: 'aab',
+ title: 'AAB',
+ expanded: true
+ }
+ ]
+ },
+ {
+ key: 'ab',
+ title: 'AB',
+ expanded: true,
+ children: [
+ {
+ key: 'aba',
+ title: 'ABA',
+ expanded: true
+ },
+ {
+ key: 'abb',
+ title: 'ABB',
+ expanded: true
+ }
+ ]
+ },
+ ]
+ }
+ ];
- const type = this.getFormValue('type');
-
- let fieldSettings = null;
- switch (type) {
- case 'text':
- case 'website':
- case 'longtext':
- case 'gpg':
- case 'number':
- fieldSettings =
- ;
- break;
-
- case 'checkbox':
- case 'radio-grouped':
- case 'dropdown-grouped':
- fieldSettings =
- ;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- fieldSettings =
- ;
- break;
-
- case 'date':
- fieldSettings =
- ;
- break;
-
- case 'birthday':
- fieldSettings =
- ;
- break;
-
- case 'json':
- fieldSettings = ;
- break;
-
- case 'option':
- const fieldsGroupedColumns = [
- { data: 4, title: "#" },
- { data: 1, title: t('Name') },
- { data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
- { data: 3, title: t('Merge Tag') }
- ];
-
- fieldSettings =
- ;
- break;
- }
return (
@@ -445,40 +156,26 @@ export default class CUD extends Component {
+ 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}`}
+ deletingMsg={t('Deleting segment ...')}
+ deletedMsg={t('Segment deleted')}/>
}
- {isEdit ? t('Edit Field') : t('Create Field')}
+ {isEdit ? t('Edit Segment') : t('Create Segment')}
);
diff --git a/client/src/lists/segments/List.js b/client/src/lists/segments/List.js
index 47832688..7d6fff4e 100644
--- a/client/src/lists/segments/List.js
+++ b/client/src/lists/segments/List.js
@@ -30,7 +30,6 @@ export default class List extends Component {
const columns = [
{ data: 1, title: t('Name') },
- { data: 2, title: t('Match') },
{
actions: data => [{
label: ,
diff --git a/client/src/lists/subscriptions/CUD.js b/client/src/lists/subscriptions/CUD.js
deleted file mode 100644
index 834b80b1..00000000
--- a/client/src/lists/subscriptions/CUD.js
+++ /dev/null
@@ -1,486 +0,0 @@
-'use strict';
-
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { translate, Trans } from 'react-i18next';
-import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
-import {
- withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
- Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField
-} from '../../lib/form';
-import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
-import {DeleteModalDialog} from "../../lib/delete";
-import { getFieldTypes } from './field-types';
-import axios from '../../lib/axios';
-import interoperableErrors from '../../../../shared/interoperable-errors';
-import validators from '../../../../shared/validators';
-import slugify from 'slugify';
-import { parseDate, parseBirthday } from '../../../../shared/fields';
-
-@translate()
-@withForm
-@withPageHelpers
-@withErrorHandling
-@requiresAuthenticatedUser
-export default class CUD extends Component {
- constructor(props) {
- super(props);
-
- this.state = {};
-
- this.fieldTypes = getFieldTypes(props.t);
-
- this.initForm({
- serverValidation: {
- url: `/rest/fields-validate/${this.props.list.id}`,
- changed: ['key'],
- extra: ['id']
- },
- onChange: {
- name: ::this.onChangeName
- }
- });
- }
-
- static propTypes = {
- action: PropTypes.string.isRequired,
- list: PropTypes.object,
- entity: PropTypes.object
- }
-
- onChangeName(state, attr, oldValue, newValue) {
- const oldComputedKey = ('MERGE_' + slugify(oldValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
- const oldKey = state.formState.getIn(['data', 'key', 'value']);
-
- if (oldKey === '' || oldKey === oldComputedKey) {
- const newKey = ('MERGE_' + slugify(newValue, '_')).toUpperCase().replace(/[^A-Z0-9_]/g, '');
- state.formState = state.formState.setIn(['data', 'key', 'value'], newKey);
- }
- }
-
- @withAsyncErrorHandler
- async loadOrderOptions() {
- const t = this.props.t;
-
- const flds = await axios.get(`/rest/fields/${this.props.list.id}`);
-
- const getOrderOptions = fld => {
- return [
- {key: 'none', label: t('Not visible')},
- ...flds.data.filter(x => (!this.props.entity || x.id !== this.props.entity.id) && x[fld] !== null && x.type !== 'option').sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id.toString(), label: `${x.name} (${this.fieldTypes[x.type].label})`})),
- {key: 'end', label: t('End of list')}
- ];
- };
-
- this.setState({
- orderListOptions: getOrderOptions('order_list'),
- orderSubscribeOptions: getOrderOptions('order_subscribe'),
- orderManageOptions: getOrderOptions('order_manage')
- });
- }
-
- componentDidMount() {
- if (this.props.entity) {
- this.getFormValuesFromEntity(this.props.entity, data => {
- data.settings = data.settings || {};
-
- if (data.default_value === null) {
- data.default_value = '';
- }
-
- if (data.type !== 'option') {
- data.group = null;
- }
-
- data.enumOptions = '';
- data.dateFormat = 'eur';
- data.renderTemplate = '';
-
- switch (data.type) {
- case 'checkbox':
- case 'radio-grouped':
- case 'dropdown-grouped':
- case 'json':
- data.renderTemplate = data.settings.renderTemplate;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- data.enumOptions = this.renderEnumOptions(data.settings.enumOptions);
- data.renderTemplate = data.settings.renderTemplate;
- break;
-
- case 'date':
- case 'birthday':
- data.dateFormat = data.settings.dateFormat;
- break;
- }
-
- data.orderListBefore = data.orderListBefore.toString();
- data.orderSubscribeBefore = data.orderSubscribeBefore.toString();
- data.orderManageBefore = data.orderManageBefore.toString();
- });
-
- } else {
- this.populateFormValues({
- name: '',
- type: 'text',
- key: '',
- default_value: '',
- group: null,
- renderTemplate: '',
- enumOptions: '',
- dateFormat: 'eur',
- orderListBefore: 'end', // possible values are / 'end' / 'none'
- orderSubscribeBefore: 'end',
- orderManageBefore: 'end',
- orderListOptions: [],
- orderSubscribeOptions: [],
- orderManageOptions: []
- });
- }
-
- this.loadOrderOptions();
- }
-
- localValidateFormValues(state) {
- const t = this.props.t;
-
- if (!state.getIn(['name', 'value'])) {
- state.setIn(['name', 'error'], t('Name must not be empty'));
- } else {
- state.setIn(['name', 'error'], null);
- }
-
- const keyServerValidation = state.getIn(['key', 'serverValidation']);
- if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
- state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.'));
- } else if (!keyServerValidation) {
- state.setIn(['key', 'error'], t('Validation is in progress...'));
- } else if (keyServerValidation.exists) {
- state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.'));
- } else {
- state.setIn(['key', 'error'], null);
- }
-
- const type = state.getIn(['type', 'value']);
-
- const group = state.getIn(['group', 'value']);
- if (type === 'option' && !group) {
- state.setIn(['group', 'error'], t('Group has to be selected'));
- } else {
- state.setIn(['group', 'error'], null);
- }
-
- const defaultValue = state.getIn(['default_value', 'value']);
- if (type === 'number' && !/^[0-9]*$/.test(defaultValue.trim())) {
- state.setIn(['default_value', 'error'], t('Default value is not integer number'));
- } else if (type === 'date' && !parseDate(state.getIn(['dateFormat', 'value']), defaultValue)) {
- state.setIn(['default_value', 'error'], t('Default value is not a properly formatted date'));
- } else if (type === 'birthday' && !parseBirthday(state.getIn(['dateFormat', 'value']), defaultValue)) {
- state.setIn(['default_value', 'error'], t('Default value is not a properly formatted birthday date'));
- } else {
- state.setIn(['default_value', 'error'], null);
- }
-
- if (type === 'radio-enum' || type === 'dropdown-enum') {
- const enumOptions = this.parseEnumOptions(state.getIn(['enumOptions', 'value']));
- if (enumOptions.errors) {
- state.setIn(['enumOptions', 'error'], {enumOptions.errors.map((err, idx) =>
{err}
)}
);
- } else {
- state.setIn(['enumOptions', 'error'], null);
-
- if (defaultValue !== '' && !(defaultValue in enumOptions.options)) {
- state.setIn(['default_value', 'error'], t('Default value is not one of the allowed options'));
- }
- }
- } else {
- state.setIn(['enumOptions', 'error'], null);
- }
- }
-
- parseEnumOptions(text) {
- const t = this.props.t;
- const errors = [];
- const options = {};
-
- const lines = text.split('\n');
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
- const line = lines[lineIdx].trim();
-
- if (line != '') {
- const matches = line.match(/^([^|]*)[|](.*)$/);
- if (matches) {
- const key = matches[1].trim();
- const label = matches[2].trim();
- options[key] = label;
- } else {
- errors.push(t('Errror on line {{ line }}', { line: lineIdx + 1}));
- }
- }
- }
-
- if (errors.length) {
- return {
- errors
- };
- } else {
- return {
- options
- };
- }
- }
-
- renderEnumOptions(options) {
- return Object.keys(options).map(key => `${key}|${options[key]}`).join('\n');
- }
-
-
- async submitHandler() {
- const t = this.props.t;
-
- let sendMethod, url;
- if (this.props.entity) {
- sendMethod = FormSendMethod.PUT;
- url = `/rest/fields/${this.props.list.id}/${this.props.entity.id}`
- } else {
- sendMethod = FormSendMethod.POST;
- url = `/rest/fields/${this.props.list.id}`
- }
-
- try {
- this.disableForm();
- this.setFormStatusMessage('info', t('Saving field ...'));
-
- const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
- if (data.default_value.trim() === '') {
- data.default_value = null;
- }
-
- if (data.type !== 'option') {
- data.group = null;
- }
-
- data.settings = {};
- switch (data.type) {
- case 'checkbox':
- case 'radio-grouped':
- case 'dropdown-grouped':
- case 'json':
- data.settings.renderTemplate = data.renderTemplate;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- data.settings.enumOptions = this.parseEnumOptions(data.enumOptions).options;
- data.settings.renderTemplate = data.renderTemplate;
- break;
-
- case 'date':
- case 'birthday':
- data.settings.dateFormat = data.dateFormat;
- break;
- }
-
- delete data.renderTemplate;
- delete data.enumOptions;
- delete data.dateFormat;
-
- if (data.type === 'option') {
- data.orderListBefore = data.orderSubscribeBefore = data.orderManageBefore = 'none';
- } else {
- data.orderListBefore = Number.parseInt(data.orderListBefore) || data.orderListBefore;
- data.orderSubscribeBefore = Number.parseInt(data.orderSubscribeBefore) || data.orderSubscribeBefore;
- data.orderManageBefore = Number.parseInt(data.orderManageBefore) || data.orderManageBefore;
- }
- });
-
- if (submitSuccessful) {
- this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved'));
- } else {
- this.enableForm();
- this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
- }
- } catch (error) {
- if (error instanceof interoperableErrors.DependencyNotFoundError) {
- this.setFormStatusMessage('danger',
-
- {t('Your updates cannot be saved.')}{' '}
- {t('It seems that another field upon which sort field order was established has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
-
- );
- return;
- }
-
- throw error;
- }
- }
-
- render() {
- const t = this.props.t;
- const isEdit = !!this.props.entity;
-
- const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
-
- const type = this.getFormValue('type');
-
- let fieldSettings = null;
- switch (type) {
- case 'text':
- case 'website':
- case 'longtext':
- case 'gpg':
- case 'number':
- fieldSettings =
- ;
- break;
-
- case 'checkbox':
- case 'radio-grouped':
- case 'dropdown-grouped':
- fieldSettings =
- ;
- break;
-
- case 'radio-enum':
- case 'dropdown-enum':
- fieldSettings =
- ;
- break;
-
- case 'date':
- fieldSettings =
- ;
- break;
-
- case 'birthday':
- fieldSettings =
- ;
- break;
-
- case 'json':
- fieldSettings = ;
- break;
-
- case 'option':
- const fieldsGroupedColumns = [
- { data: 4, title: "#" },
- { data: 1, title: t('Name') },
- { data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
- { data: 3, title: t('Merge Tag') }
- ];
-
- fieldSettings =
- ;
- break;
- }
-
-
- return (
-
- {isEdit &&
-
- }
-
-
{isEdit ? t('Edit Field') : t('Create Field')}
-
-
-
- );
- }
-}
\ No newline at end of file
diff --git a/client/src/namespaces/CUD.js b/client/src/namespaces/CUD.js
index 26819b35..467ed397 100644
--- a/client/src/namespaces/CUD.js
+++ b/client/src/namespaces/CUD.js
@@ -125,7 +125,7 @@ export default class CUD extends Component {
try {
this.disableForm();
- this.setFormStatusMessage('info', t('Saving namespace ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);
diff --git a/client/src/reports/CUD.js b/client/src/reports/CUD.js
index 7f0a237a..7f2d2029 100644
--- a/client/src/reports/CUD.js
+++ b/client/src/reports/CUD.js
@@ -134,7 +134,7 @@ export default class CUD extends Component {
}
this.disableForm();
- this.setFormStatusMessage('info', t('Saving report ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
const params = {};
diff --git a/client/src/reports/templates/CUD.js b/client/src/reports/templates/CUD.js
index 556997d6..bb84696c 100644
--- a/client/src/reports/templates/CUD.js
+++ b/client/src/reports/templates/CUD.js
@@ -244,7 +244,7 @@ export default class CUD extends Component {
}
this.disableForm();
- this.setFormStatusMessage('info', t('Saving report template ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);
diff --git a/client/src/users/CUD.js b/client/src/users/CUD.js
index d067061f..47045a5e 100644
--- a/client/src/users/CUD.js
+++ b/client/src/users/CUD.js
@@ -141,7 +141,7 @@ export default class CUD extends Component {
try {
this.disableForm();
- this.setFormStatusMessage('info', t('Saving user ...'));
+ this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.password2;
diff --git a/models/fields.js b/models/fields.js
index 7eb6c187..7a059fd5 100644
--- a/models/fields.js
+++ b/models/fields.js
@@ -10,6 +10,7 @@ const shares = require('./shares');
const bluebird = require('bluebird');
const validators = require('../shared/validators');
const shortid = require('shortid');
+const segments = require('./segments');
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
const allowedKeysUpdate = new Set(['name', 'key', 'default_value', 'group', 'settings']);
@@ -353,32 +354,44 @@ async function updateWithConsistencyCheck(context, listId, entity) {
});
}
+async function removeTx(tx, context, listId, id) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
+
+ const existing = await tx('custom_fields').where({list: listId, id: id}).first();
+ if (!existing) {
+ throw new interoperableErrors.NotFoundError();
+ }
+
+ const fieldType = fieldTypes[existing.type];
+
+ await tx('custom_fields').where({list: listId, id}).del();
+
+ if (fieldType.grouped) {
+ await tx('custom_fields').where({list: listId, group: id}).del();
+
+ } else {
+ await knex.schema.table('subscription__' + listId, table => {
+ table.dropColumn(existing.column);
+ });
+
+ await segments.removeRulesByFieldIdTx(tx, context, listId, id);
+ }
+}
+
async function remove(context, listId, id) {
await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
-
- const existing = await tx('custom_fields').where({list: listId, id: id}).first();
- if (!existing) {
- throw new interoperableErrors.NotFoundError();
- }
-
- const fieldType = fieldTypes[existing.type];
-
- await tx('custom_fields').where({list: listId, id}).del();
-
- if (fieldType.grouped) {
- await tx('custom_fields').where({list: listId, group: id}).del();
-
- } else {
- await knex.schema.table('subscription__' + listId, table => {
- table.dropColumn(existing.column);
- });
-
- await tx('segemnt_rules').where({column: existing.column}).del();
- }
+ await removeTx(tx, context, listId, id);
});
}
+async function removeAllByListIdTx(tx, context, listId) {
+ const entities = await tx('custom_fields').where('list', listId).select(['id']);
+ for (const entity of entities) {
+ await removeTx(tx, context, listId, entity.id);
+ }
+}
+
+
module.exports = {
hash,
getById,
@@ -389,5 +402,6 @@ module.exports = {
create,
updateWithConsistencyCheck,
remove,
+ removeAllByListIdTx,
serverValidate
};
\ No newline at end of file
diff --git a/models/forms.js b/models/forms.js
index e3043df2..a8bb9d72 100644
--- a/models/forms.js
+++ b/models/forms.js
@@ -12,6 +12,7 @@ const fsReadFile = bluebird.promisify(require('fs').readFile);
const path = require('path');
const mjml = require('mjml');
const _ = require('../lib/translate')._;
+const lists = require('./lists');
const formAllowedKeys = new Set([
'name',
@@ -131,7 +132,7 @@ async function create(context, entity) {
})
}
- await shares.rebuildPermissions(tx, { entityTypeId: 'customForm', entityId: id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'customForm', entityId: id });
return id;
});
}
@@ -164,7 +165,7 @@ async function updateWithConsistencyCheck(context, entity) {
});
}
- await shares.rebuildPermissions(tx, { entityTypeId: 'customForm', entityId: entity.id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'customForm', entityId: entity.id });
});
}
@@ -172,11 +173,7 @@ async function remove(context, id) {
await knex.transaction(async tx => {
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete');
- const entity = await tx('custom_forms').where('id', id).first();
-
- if (!entity) {
- throw shares.throwPermissionDenied();
- }
+ lists.removeFormFromAllTx(tx, context, id);
await tx('custom_forms_data').where('form', id).del();
await tx('custom_forms').where('id', id).del();
diff --git a/models/lists.js b/models/lists.js
index dcc97af7..fd1c7381 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -9,6 +9,7 @@ const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers');
const fields = require('./fields');
+const segments = require('./segments');
const UnsubscriptionMode = require('../shared/lists').UnsubscriptionMode;
@@ -56,7 +57,7 @@ async function create(context, entity) {
await knex.schema.raw('CREATE TABLE `subscription__' + id + '` LIKE subscription');
- await shares.rebuildPermissions(tx, { entityTypeId: 'list', entityId: id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: id });
return id;
});
@@ -82,7 +83,7 @@ async function updateWithConsistencyCheck(context, entity) {
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));
- await shares.rebuildPermissions(tx, { entityTypeId: 'list', entityId: entity.id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: entity.id });
});
}
@@ -90,11 +91,25 @@ async function remove(context, id) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'delete');
+ await fields.removeAllByListIdTx(tx, context, id);
+ await segments.removeAllByListIdTx(tx, context, id);
+
await tx('lists').where('id', id).del();
await knex.schema.dropTableIfExists('subscription__' + id);
});
}
+async function removeFormFromAllTx(tx, context, formId) {
+ await knex.transaction(async tx => {
+ const entities = tx('lists').where('default_form', formId).select(['id']);
+
+ for (const entity of entities) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'edit');
+ await tx('lists').where('id', entity.id).update({default_form: null});
+ }
+ });
+}
+
module.exports = {
UnsubscriptionMode,
@@ -103,5 +118,6 @@ module.exports = {
getById,
create,
updateWithConsistencyCheck,
- remove
+ remove,
+ removeFormFromAllTx
};
\ No newline at end of file
diff --git a/models/namespaces.js b/models/namespaces.js
index d4318ef3..f68ed2ba 100644
--- a/models/namespaces.js
+++ b/models/namespaces.js
@@ -126,7 +126,7 @@ async function create(context, entity) {
const id = ids[0];
// We don't have to rebuild all entity types, because no entity can be a child of the namespace at this moment.
- await shares.rebuildPermissions(tx, { entityTypeId: 'namespace', entityId: id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'namespace', entityId: id });
return id;
});
@@ -166,7 +166,7 @@ async function updateWithConsistencyCheck(context, entity) {
await tx('namespaces').where('id', entity.id).update(filterObject(entity, allowedKeys));
- await shares.rebuildPermissions(tx);
+ await shares.rebuildPermissionsTx(tx);
});
}
diff --git a/models/report-templates.js b/models/report-templates.js
index 5d53ffd2..982da36a 100644
--- a/models/report-templates.js
+++ b/models/report-templates.js
@@ -7,6 +7,7 @@ const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors');
const namespaceHelpers = require('../lib/namespace-helpers');
const shares = require('./shares');
+const reports = require('./reports');
const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs', 'namespace']);
@@ -42,7 +43,7 @@ async function create(context, entity) {
const ids = await tx('report_templates').insert(filterObject(entity, allowedKeys));
const id = ids[0];
- await shares.rebuildPermissions(tx, { entityTypeId: 'reportTemplate', entityId: id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'reportTemplate', entityId: id });
return id;
});
@@ -68,14 +69,18 @@ async function updateWithConsistencyCheck(context, entity) {
await tx('report_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
- await shares.rebuildPermissions(tx, { entityTypeId: 'reportTemplate', entityId: entity.id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'reportTemplate', entityId: entity.id });
});
}
async function remove(context, id) {
- await shares.enforceEntityPermission(context, 'reportTemplate', id, 'delete');
+ await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', id, 'delete');
- await knex('report_templates').where('id', id).del();
+ await reports.removeAllByReportTemplateIdTx(tx, context, id);
+
+ await tx('report_templates').where('id', id).del();
+ });
}
async function getUserFieldsById(context, id) {
diff --git a/models/reports.js b/models/reports.js
index faf561a1..96ae0ad8 100644
--- a/models/reports.js
+++ b/models/reports.js
@@ -68,7 +68,7 @@ async function create(context, entity) {
const ids = await tx('reports').insert(filterObject(entity, allowedKeys));
id = ids[0];
- await shares.rebuildPermissions(tx, { entityTypeId: 'report', entityId: id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'report', entityId: id });
});
const reportProcessor = require('../lib/report-processor');
@@ -103,7 +103,7 @@ async function updateWithConsistencyCheck(context, entity) {
await tx('reports').where('id', entity.id).update(filteredUpdates);
- await shares.rebuildPermissions(tx, { entityTypeId: 'report', entityId: entity.id });
+ await shares.rebuildPermissionsTx(tx, { entityTypeId: 'report', entityId: entity.id });
});
// This require is here to avoid cyclic dependency
@@ -111,12 +111,28 @@ async function updateWithConsistencyCheck(context, entity) {
await reportProcessor.start(entity.id);
}
-async function remove(context, id) {
- await shares.enforceEntityPermission(context, 'report', id, 'delete');
+async function removeTx(tx, context, id) {
+ await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'delete');
- await knex('reports').where('id', id).del();
+ await tx('reports').where('id', id).del();
+
+ // FIXME: Remove generated files
}
+async function remove(context, id) {
+ await knex.transaction(async tx => {
+ await removeTx(tx, context, id);
+ });
+}
+
+async function removeAllByReportTemplateIdTx(tx, context, templateId) {
+ const entities = await tx('reports').where('report_template', templateId).select(['id']);
+ for (const entity of entities) {
+ await removeTx(tx, context, entity.id);
+ }
+}
+
+
async function updateFields(id, fields) {
return await knex('reports').where('id', id).update(fields);
}
@@ -186,8 +202,9 @@ module.exports = {
create,
updateWithConsistencyCheck,
remove,
+ removeAllByReportTemplateIdTx,
updateFields,
listByState,
bulkChangeState,
- getCampaignResults
+ getCampaignResults,
};
\ No newline at end of file
diff --git a/models/segments.js b/models/segments.js
index 75ebee54..64e315e2 100644
--- a/models/segments.js
+++ b/models/segments.js
@@ -5,17 +5,11 @@ const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares');
-//const allowedKeys = new Set(['cid', 'email']);
+const allowedKeys = new Set(['name', 'settings']);
-/*
function hash(entity) {
- const allowedKeys = allowedKeysBase.slice();
-
- // TODO add keys from custom fields
-
return hasher.hash(filterObject(entity, allowedKeys));
}
-*/
async function listDTAjax(context, listId, params) {
return await knex.transaction(async tx => {
@@ -27,12 +21,11 @@ async function listDTAjax(context, listId, params) {
builder => builder
.from('segments')
.where('list', listId),
- ['id', 'name', 'type']
+ ['id', 'name']
);
});
}
-
async function list(context, listId) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
@@ -41,7 +34,82 @@ async function list(context, listId) {
});
}
+async function getById(context, listId, id) {
+ return await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
+ const entity = await tx('segments').where({id, list: listId}).first();
+ entity.settings = JSON.parse(entity.settings);
+ return entity;
+ });
+}
+
+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);
+
+ const filteredEntity = filterObject(entity, allowedKeys);
+ filteredEntity.list = listId;
+
+ const ids = await tx('segments').insert(filteredEntity);
+ const id = ids[0];
+
+ return id;
+ });
+}
+
+async function updateWithConsistencyCheck(context, listId, entity) {
+ await knex.transaction(async tx => {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
+
+ const existing = await tx('segments').where({list: listId, id: entity.id}).first();
+ if (!existing) {
+ throw new interoperableErrors.NotFoundError();
+ }
+
+ const existingHash = hash(existing);
+ if (existingHash !== entity.originalHash) {
+ throw new interoperableErrors.ChangedError();
+ }
+
+ entity.settings = JSON.stringify(entity.params);
+
+ await tx('segments').where('id', entity.id).update(filterObject(entity, allowedKeys));
+ });
+}
+
+
+async function removeTx(tx, context, listId, id) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
+
+ // The listId "where" is here to prevent deleting segment of a list for which a user does not have permission
+ await tx('segments').where({list: listId, id: id}).del();
+}
+
+async function remove(context, listId, id) {
+ await knex.transaction(async tx => {
+ await removeTx(tx, context, listId, id);
+ });
+}
+
+async function removeAllByListIdTx(tx, context, listId) {
+ const entities = await tx('segments').where('list', listId).select(['id']);
+ for (const entity of entities) {
+ await removeTx(tx, context, entity.id);
+ }
+}
+
+async function removeRulesByFieldIdTx(tx, context, listId, fieldId) {
+ // FIXME
+}
+
module.exports = {
listDTAjax,
- list
+ list,
+ create,
+ updateWithConsistencyCheck,
+ remove,
+ removeAllByListIdTx,
+ removeRulesByFieldIdTx
};
\ No newline at end of file
diff --git a/models/shares.js b/models/shares.js
index 8d8443fb..8cef377f 100644
--- a/models/shares.js
+++ b/models/shares.js
@@ -115,14 +115,16 @@ async function assign(context, entityTypeId, entityId, userId, role) {
await tx(entityType.permissionsTable).where({user: userId, entity: entityId}).del();
if (entityTypeId === 'namespace') {
- await rebuildPermissions(tx, {userId});
+ await rebuildPermissionsTx(tx, {userId});
} else if (role) {
- await rebuildPermissions(tx, { entityTypeId, entityId, userId });
+ await rebuildPermissionsTx(tx, { entityTypeId, entityId, userId });
}
});
}
-async function _rebuildPermissions(tx, restriction) {
+async function rebuildPermissionsTx(tx, restriction) {
+ restriction = restriction || {};
+
const namespaceEntityType = permissions.getEntityType('namespace');
// Collect entity types we care about
@@ -358,16 +360,10 @@ async function _rebuildPermissions(tx, restriction) {
}
}
-async function rebuildPermissions(tx, restriction) {
- restriction = restriction || {};
-
- if (tx) {
- await _rebuildPermissions(tx, restriction);
- } else {
- await knex.transaction(async tx => {
- await _rebuildPermissions(tx, restriction);
- });
- }
+async function rebuildPermissions(restriction) {
+ await knex.transaction(async tx => {
+ await rebuildPermissionsTx(tx, restriction);
+ });
}
async function regenerateRoleNamesTable() {
@@ -556,6 +552,7 @@ module.exports = {
listUnassignedUsersDTAjax,
listRolesDTAjax,
assign,
+ rebuildPermissionsTx,
rebuildPermissions,
removeDefaultShares,
enforceEntityPermission,
diff --git a/models/users.js b/models/users.js
index a72d4452..64c050b8 100644
--- a/models/users.js
+++ b/models/users.js
@@ -183,7 +183,7 @@ async function create(context, user) {
id = ids[0];
}
- await shares.rebuildPermissions(tx, { userId: id });
+ await shares.rebuildPermissionsTx(tx, { userId: id });
});
return id;
@@ -231,7 +231,7 @@ async function updateWithConsistencyCheck(context, user, isOwnAccount) {
await shares.removeDefaultShares(tx, existing);
}
- await shares.rebuildPermissions(tx, { userId: user.id });
+ await shares.rebuildPermissionsTx(tx, { userId: user.id });
});
}
diff --git a/routes/rest/lists.js b/routes/rest/lists.js
index a8208c88..4ad95135 100644
--- a/routes/rest/lists.js
+++ b/routes/rest/lists.js
@@ -2,8 +2,6 @@
const passport = require('../../lib/passport');
const lists = require('../../models/lists');
-const subscriptions = require('../../models/subscriptions');
-const segments = require('../../models/segments');
const router = require('../../lib/router-async').create();
@@ -24,10 +22,10 @@ router.postAsync('/lists', passport.loggedIn, passport.csrfProtection, async (re
});
router.putAsync('/lists/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
- const list = req.body;
- list.id = parseInt(req.params.listId);
+ const entity = req.body;
+ entity.id = parseInt(req.params.listId);
- await lists.updateWithConsistencyCheck(req.context, list);
+ await lists.updateWithConsistencyCheck(req.context, entity);
return res.json();
});
@@ -36,17 +34,5 @@ router.deleteAsync('/lists/:listId', passport.loggedIn, passport.csrfProtection,
return res.json();
});
-router.postAsync('/subscriptions-table/:listId', passport.loggedIn, async (req, res) => {
- return res.json(await subscriptions.listDTAjax(req.context, req.params.listId, req.body));
-});
-
-router.getAsync('/segments/:listId', passport.loggedIn, async (req, res) => {
- return res.json(await segments.list(req.context, req.params.listId));
-});
-
-router.postAsync('/segments-table/:listId', passport.loggedIn, async (req, res) => {
- return res.json(await segments.listDTAjax(req.context, req.params.listId, req.body));
-});
-
module.exports = router;
\ No newline at end of file
diff --git a/routes/rest/segments.js b/routes/rest/segments.js
new file mode 100644
index 00000000..6b1f63cd
--- /dev/null
+++ b/routes/rest/segments.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const passport = require('../../lib/passport');
+const segments = require('../../models/segments');
+
+const router = require('../../lib/router-async').create();
+
+
+router.postAsync('/segments-table/:listId', passport.loggedIn, async (req, res) => {
+ return res.json(await segments.listDTAjax(req.context, req.params.listId, req.body));
+});
+
+router.getAsync('/segments/:listId', passport.loggedIn, async (req, res) => {
+ return res.json(await segments.list(req.context, req.params.listId));
+});
+
+router.postAsync('/segments/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ await segments.create(req.context, req.params.listId, req.body);
+ return res.json();
+});
+
+router.putAsync('/segments/:listId/:segmentId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ const entity = req.body;
+ entity.id = parseInt(req.params.segmentId);
+
+ await segments.updateWithConsistencyCheck(req.context, req.params.listId, entity);
+ 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);
+ return res.json();
+});
+
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/rest/subscriptions.js b/routes/rest/subscriptions.js
new file mode 100644
index 00000000..38275a67
--- /dev/null
+++ b/routes/rest/subscriptions.js
@@ -0,0 +1,14 @@
+'use strict';
+
+const passport = require('../../lib/passport');
+const subscriptions = require('../../models/subscriptions');
+
+const router = require('../../lib/router-async').create();
+
+
+router.postAsync('/subscriptions-table/:listId', passport.loggedIn, async (req, res) => {
+ return res.json(await subscriptions.listDTAjax(req.context, req.params.listId, req.body));
+});
+
+
+module.exports = router;
\ No newline at end of file
diff --git a/setup/knex/migrations/20170731072050_change_field_group_template_to_settings_and_simplify_types.js b/setup/knex/migrations/20170731072050_upgrade_custom_fields.js
similarity index 88%
rename from setup/knex/migrations/20170731072050_change_field_group_template_to_settings_and_simplify_types.js
rename to setup/knex/migrations/20170731072050_upgrade_custom_fields.js
index a599a15b..eae1db26 100644
--- a/setup/knex/migrations/20170731072050_change_field_group_template_to_settings_and_simplify_types.js
+++ b/setup/knex/migrations/20170731072050_upgrade_custom_fields.js
@@ -5,6 +5,11 @@ exports.up = (knex, Promise) => (async() => {
table.json('settings');
});
+ await knex.schema.table('custom_fields', table => {
+ table.dropForeign('list', 'custom_fields_ibfk_1');
+ table.foreign('list').references('lists.id');
+ });
+
const fields = await knex('custom_fields');
for (const field of fields) {
diff --git a/setup/knex/migrations/20170813143004_create_foreign_keys_in_custom_fields.js b/setup/knex/migrations/20170813143004_create_foreign_keys_in_custom_fields.js
deleted file mode 100644
index 5c3924c6..00000000
--- a/setup/knex/migrations/20170813143004_create_foreign_keys_in_custom_fields.js
+++ /dev/null
@@ -1,8 +0,0 @@
-exports.up = (knex, Promise) => (async() => {
- await knex.schema.table('custom_fields', table => {
- table.foreign('group').references('custom_fields.id').onDelete('CASCADE');
- });
-})();
-
-exports.down = (knex, Promise) => (async() => {
-})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170814174051_upgrade_segments.js b/setup/knex/migrations/20170814174051_upgrade_segments.js
new file mode 100644
index 00000000..42c51f76
--- /dev/null
+++ b/setup/knex/migrations/20170814174051_upgrade_segments.js
@@ -0,0 +1,175 @@
+"use strict";
+
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.table('segments', table => {
+ table.json('settings');
+ });
+
+ await knex.schema.table('segments', table => {
+ table.dropForeign('list', 'segments_ibfk_1');
+ table.foreign('list').references('lists.id');
+ });
+
+
+ const segments = await knex('segments');
+
+ for (const segment of segments) {
+ const oldRules = await knex('segment_rules').where('segment', segment.id);
+
+ let type;
+ if (segment.type === 1) {
+ type = 'all';
+ } else {
+ type = 'some';
+ }
+
+ const rules = [];
+ for (const oldRule of oldRules) {
+ const oldSettings = JSON.parse(oldRule.value);
+
+ const predefColumns = {
+ email: 'string',
+ opt_in_country: 'string',
+ created: 'date',
+ latest_open: 'date',
+ latest_click: 'date'
+ };
+ // first_name and last_name are not here because they have been already converted to custom fields by 20170731072050_upgrade_custom_fields.js
+
+ let fieldType;
+ if (oldRule.column in predefColumns) {
+ fieldType = predefColumns[oldRule.column];
+ } else {
+ const field = await knex('custom_fields').where({list: segment.list, type: 'like', column: oldRule.column}).select(['type']).first();
+ if (field) {
+ fieldType = field.type;
+ }
+ }
+
+ switch (fieldType) {
+ case 'string':
+ rules.push({ column: oldRule.column, value: oldSettings.value });
+ break;
+ case 'boolean':
+ rules.push({ type: 'eq', column: oldRule.column, value: oldSettings.value });
+ break;
+ case 'number':
+ if (oldSettings.range) {
+ if (oldSettings.start && oldSettings.end) {
+ if (type === 'all') {
+ rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start});
+ rules.push({ type: 'lt', column: oldRule.column, value: oldSettings.end});
+ } else {
+ rules.push({
+ type: 'all',
+ rules: [
+ {type: 'ge', value: oldSettings.start},
+ {type: 'lt', value: oldSettings.end}
+ ]
+ });
+ }
+ } else if (oldSettings.start) {
+ rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start });
+ }
+ if (oldSettings.end) {
+ rules.push({ type: 'lt', column: oldRule.column, value: oldSettings.end });
+ }
+ } else {
+ rules.push({ type: 'eq', column: oldRule.column, value: oldSettings.value });
+ }
+ break;
+ 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':
+ if (oldSettings.relativeRange) {
+ if (oldSettings.start && oldSettings.end) {
+ if (type === 'all') {
+ rules.push({ type: 'dateGeNowPlusDays', column: oldRule.column, value: oldSettings.start});
+ rules.push({ type: 'dateLeNowPlusDays', column: oldRule.column, value: oldSettings.end});
+ } else {
+ rules.push({
+ type: 'all',
+ rules: [
+ { type: 'dateGeNowPlusDays', column: oldRule.column, value: oldSettings.start},
+ { type: 'dateLeNowPlusDays', column: oldRule.column, value: oldSettings.end}
+ ]
+ });
+ }
+ } else if (oldSettings.start) {
+ rules.push({ type: 'dateGeNowPlusDays', column: oldRule.column, value: oldSettings.startDirection ? oldSettings.start : -oldSettings.start });
+ }
+ if (oldSettings.end) {
+ rules.push({ type: 'dateLeNowPlusDays', column: oldRule.column, value: oldSettings.endDirection ? oldSettings.end : -oldSettings.end });
+ }
+ } else if (oldSettings.range) {
+ if (oldSettings.start && oldSettings.end) {
+ if (type === 'all') {
+ rules.push({ type: 'dateGe', column: oldRule.column, value: oldSettings.start});
+ rules.push({ type: 'dateLe', column: oldRule.column, value: oldSettings.end});
+ } else {
+ rules.push({
+ type: 'all',
+ rules: [
+ { type: 'dateGe', column: oldRule.column, value: oldSettings.start},
+ { type: 'dateLe', column: oldRule.column, value: oldSettings.end}
+ ]
+ });
+ }
+ } else if (oldSettings.start) {
+ rules.push({ type: 'dateGe', column: oldRule.column, value: oldSettings.start });
+ }
+ if (oldSettings.end) {
+ rules.push({ type: 'dateLe', column: oldRule.column, value: oldSettings.end });
+ }
+ } else {
+ rules.push({ type: 'dateEq', column: oldRule.column, value: oldSettings.value });
+ }
+ break;
+ default:
+ throw new Error(`Unknown rule for column ${oldRule.column} with field type ${fieldType}`);
+ }
+ }
+
+ const settings = {
+ rootRule: {
+ type,
+ rules
+ }
+ };
+
+ await knex('segments').where('id', segment.id).update({settings: JSON.stringify(settings)});
+ }
+
+ await knex.schema.table('segments', table => {
+ table.dropColumn('type');
+ });
+
+ await knex.schema.dropTable('segment_rules');
+})();
+
+
+exports.down = (knex, Promise) => (async() => {
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170814180643_remove_cascading_delete_in_reports.js b/setup/knex/migrations/20170814180643_remove_cascading_delete_in_reports.js
new file mode 100644
index 00000000..8fcbff8e
--- /dev/null
+++ b/setup/knex/migrations/20170814180643_remove_cascading_delete_in_reports.js
@@ -0,0 +1,9 @@
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.table('reports', table => {
+ table.dropForeign('report_template', 'report_template_ibfk_1');
+ table.foreign('report_template').references('report_templates.id');
+ });
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
\ No newline at end of file