work in progress on segments

some cleanup of models - handling dependencies in delete
This commit is contained in:
Tomas Bures 2017-08-14 22:53:29 +02:00
parent b23529a75b
commit 0bfb30817b
29 changed files with 553 additions and 990 deletions

4
app.js
View file

@ -54,6 +54,8 @@ const listsRest = require('./routes/rest/lists');
const formsRest = require('./routes/rest/forms'); const formsRest = require('./routes/rest/forms');
const fieldsRest = require('./routes/rest/fields'); const fieldsRest = require('./routes/rest/fields');
const sharesRest = require('./routes/rest/shares'); 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 namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
const usersLegacyIntegration = require('./routes/users-legacy-integration'); const usersLegacyIntegration = require('./routes/users-legacy-integration');
@ -277,6 +279,8 @@ app.use('/rest', listsRest);
app.use('/rest', formsRest); app.use('/rest', formsRest);
app.use('/rest', fieldsRest); app.use('/rest', fieldsRest);
app.use('/rest', sharesRest); app.use('/rest', sharesRest);
app.use('/rest', segmentsRest);
app.use('/rest', subscriptionsRest);
if (config.reports && config.reports.enabled === true) { if (config.reports && config.reports.enabled === true) {
app.use('/rest', reportTemplatesRest); app.use('/rest', reportTemplatesRest);

View file

@ -91,9 +91,11 @@ class TreeTable extends Component {
const data = []; const data = [];
for (const unsafeEntry of unsafeData) { for (const unsafeEntry of unsafeData) {
const entry = Object.assign({}, unsafeEntry); const entry = Object.assign({}, unsafeEntry);
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>) entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>);
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>) entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>);
entry.children = this.sanitizeTreeData(entry.children); if (entry.children) {
entry.children = this.sanitizeTreeData(entry.children);
}
data.push(entry); data.push(entry);
} }
return data; return data;

View file

@ -81,7 +81,7 @@ export default class CUD extends Component {
} }
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving list ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.form === 'default') { if (data.form === 'default') {

View file

@ -11,7 +11,6 @@ import {
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete"; import {DeleteModalDialog} from "../../lib/delete";
import { getFieldTypes } from './field-types'; import { getFieldTypes } from './field-types';
import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors'; import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators'; import validators from '../../../../shared/validators';
import slugify from 'slugify'; import slugify from 'slugify';
@ -45,6 +44,7 @@ export default class CUD extends Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
list: PropTypes.object, list: PropTypes.object,
fields: PropTypes.array,
entity: PropTypes.object 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() { componentDidMount() {
if (this.props.entity) { if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => { this.getFormValuesFromEntity(this.props.entity, data => {
@ -139,8 +118,6 @@ export default class CUD extends Component {
orderManageOptions: [] orderManageOptions: []
}); });
} }
this.loadOrderOptions();
} }
localValidateFormValues(state) { localValidateFormValues(state) {
@ -250,7 +227,7 @@ export default class CUD extends Component {
try { try {
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving field ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') { if (data.default_value.trim() === '') {
@ -320,6 +297,16 @@ export default class CUD extends Component {
const t = this.props.t; const t = this.props.t;
const isEdit = !!this.props.entity; 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 typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label: this.fieldTypes[key].label}));
const type = this.getFormValue('type'); const type = this.getFormValue('type');
@ -469,9 +456,9 @@ export default class CUD extends Component {
{type !== 'option' && {type !== 'option' &&
<Fieldset label={t('Field order')}> <Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/> <Dropdown id="orderListBefore" label={t('Listings (before)')} options={getOrderOptions('order_list')} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/> <Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={getOrderOptions('order_subscribe')} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/> <Dropdown id="orderManageBefore" label={t('Management form (before)')} options={getOrderOptions('order_manage')} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset> </Fieldset>
} }

View file

@ -323,7 +323,7 @@ export default class CUD extends Component {
} }
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving forms ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.selectedTemplate; delete data.selectedTemplate;

View file

@ -14,6 +14,7 @@ import FieldsList from './fields/List';
import FieldsCUD from './fields/CUD'; import FieldsCUD from './fields/CUD';
import SubscriptionsList from './subscriptions/List'; import SubscriptionsList from './subscriptions/List';
import SegmentsList from './segments/List'; import SegmentsList from './segments/List';
import SegmentsCUD from './segments/CUD';
import Share from '../shares/Share'; import Share from '../shares/Share';
@ -58,20 +59,24 @@ const getStructure = t => {
':fieldId([0-9]+)': { ':fieldId([0-9]+)': {
title: resolved => t('Field "{{name}}"', {name: resolved.field.name}), title: resolved => t('Field "{{name}}"', {name: resolved.field.name}),
resolve: { 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`, link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
navs: { navs: {
':action(edit|delete)': { ':action(edit|delete)': {
title: t('Edit'), title: t('Edit'),
link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`, link: params => `/lists/${params.listId}/fields/${params.fieldId}/edit`,
render: props => <FieldsCUD action={props.match.params.action} entity={props.resolved.field} list={props.resolved.list} /> render: props => <FieldsCUD action={props.match.params.action} entity={props.resolved.field} list={props.resolved.list} fields={props.resolved.fields} />
} }
} }
}, },
create: { create: {
title: t('Create Field'), title: t('Create'),
render: props => <FieldsCUD action="create" list={props.resolved.list} /> resolve: {
fields: params => `/rest/fields/${params.listId}`
},
render: props => <FieldsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
} }
} }
}, },
@ -79,7 +84,31 @@ const getStructure = t => {
title: t('Segments'), title: t('Segments'),
link: params => `/lists/${params.listId}/segments`, link: params => `/lists/${params.listId}/segments`,
visible: resolved => resolved.list.permissions.includes('manageSegments'), visible: resolved => resolved.list.permissions.includes('manageSegments'),
render: props => <SegmentsList list={props.resolved.list} /> render: props => <SegmentsList list={props.resolved.list} />,
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 => <SegmentsCUD action={props.match.params.action} entity={props.resolved.segment} list={props.resolved.list} fields={props.resolved.fields} />
}
}
},
create: {
title: t('Create'),
resolve: {
fields: params => `/rest/fields/${params.listId}`
},
render: props => <SegmentsCUD action="create" list={props.resolved.list} fields={props.resolved.fields} />
}
}
}, },
share: { share: {
title: t('Share'), title: t('Share'),

View file

@ -5,17 +5,12 @@ import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next'; import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page'; import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
import { import {
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button, withForm, Form, FormSendMethod, InputField, ButtonRow, Button
Fieldset, Dropdown, AlignedRow, ACEEditor, StaticField
} from '../../lib/form'; } from '../../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/delete"; import {DeleteModalDialog} from "../../lib/delete";
import { getFieldTypes } from './field-types';
import axios from '../../lib/axios';
import interoperableErrors from '../../../../shared/interoperable-errors'; import interoperableErrors from '../../../../shared/interoperable-errors';
import validators from '../../../../shared/validators'; import {TreeTable} from "../../lib/tree";
import slugify from 'slugify';
import { parseDate, parseBirthday } from '../../../../shared/fields';
@translate() @translate()
@withForm @withForm
@ -28,119 +23,28 @@ export default class CUD extends Component {
this.state = {}; this.state = {};
this.fieldTypes = getFieldTypes(props.t); this.initForm();
this.initForm({
serverValidation: {
url: `/rest/fields-validate/${this.props.list.id}`,
changed: ['key'],
extra: ['id']
},
onChange: {
name: ::this.onChangeName
}
});
} }
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
list: PropTypes.object, list: PropTypes.object,
fields: PropTypes.array,
entity: 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() { componentDidMount() {
if (this.props.entity) { if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => { this.getFormValuesFromEntity(this.props.entity, data => {
data.settings = data.settings || {}; // FIXME populate all others from 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 { } else {
this.populateFormValues({ this.populateFormValues({
name: '', name: '',
type: 'text', settingsJSON: ''
key: '',
default_value: '',
group: null,
renderTemplate: '',
enumOptions: '',
dateFormat: 'eur',
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
orderSubscribeBefore: 'end',
orderManageBefore: 'end',
orderListOptions: [],
orderSubscribeOptions: [],
orderManageOptions: []
}); });
} }
this.loadOrderOptions();
} }
localValidateFormValues(state) { localValidateFormValues(state) {
@ -152,151 +56,31 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], null); 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'], <div>{enumOptions.errors.map((err, idx) => <div key={idx}>{err}</div>)}</div>);
} 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() { async submitHandler() {
const t = this.props.t; const t = this.props.t;
let sendMethod, url; let sendMethod, url;
if (this.props.entity) { if (this.props.entity) {
sendMethod = FormSendMethod.PUT; 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 { } else {
sendMethod = FormSendMethod.POST; sendMethod = FormSendMethod.POST;
url = `/rest/fields/${this.props.list.id}` url = `/rest/segments/${this.props.list.id}`
} }
try { try {
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving field ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (data.default_value.trim() === '') { // FIXME - make sure settings is correct and delete all others
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) { if (submitSuccessful) {
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/fields`, 'success', t('Field saved')); this.enableForm();
this.setFormStatusMessage('success', t('Segment saved'));
} else { } else {
this.enableForm(); this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
@ -320,123 +104,50 @@ export default class CUD extends Component {
const t = this.props.t; const t = this.props.t;
const isEdit = !!this.props.entity; 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 =
<Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array, for
example <code>{'{{#each values}} {{this}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'radio-enum':
case 'dropdown-enum':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="enumOptions"
label={t('Options')}
height="250px"
mode="text"
help={<Trans><div>Specify the options to select from in the following format:<code>key|label</code>. For example:</div>
<div><code>au|Australia</code></div><div><code>at|Austria</code></div></Trans>}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array.
Each entry in the array is an object with attributes <code>key</code> and <code>label</code>.
For example <code>{'{{#each values}} {{this.value}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'date':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD/YYYY')},
{key: 'eur', label: t('DD/MM/YYYY')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'birthday':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD')},
{key: 'eur', label: t('DD/MM')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'json':
fieldSettings = <Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="json"
help={<Trans>You can use this template to render JSON values (if the JSON is an array then the array is
exposed as <code>values</code>, otherwise you can access the JSON keys directly).</Trans>}
/>
</Fieldset>;
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 =
<Fieldset label={t('Field settings')}>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`/rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
}
return ( return (
@ -445,40 +156,26 @@ export default class CUD extends Component {
<DeleteModalDialog <DeleteModalDialog
stateOwner={this} stateOwner={this}
visible={this.props.action === 'delete'} visible={this.props.action === 'delete'}
deleteUrl={`/rest/fields/${this.props.list.id}/${this.props.entity.id}`} deleteUrl={`/rest/segments/${this.props.list.id}/${this.props.entity.id}`}
cudUrl={`/lists/fields/${this.props.list.id}/${this.props.entity.id}/edit`} cudUrl={`/lists/segments/${this.props.list.id}/${this.props.entity.id}/edit`}
listUrl={`/lists/fields/${this.props.list.id}`} listUrl={`/lists/segments/${this.props.list.id}`}
deletingMsg={t('Deleting field ...')} deletingMsg={t('Deleting segment ...')}
deletedMsg={t('Field deleted')}/> deletedMsg={t('Segment deleted')}/>
} }
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title> <Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/> <InputField id="name" label={t('Name')}/>
{isEdit ?
<StaticField id="type" className="mt-form-disabled" label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
:
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
}
<InputField id="key" label={t('Merge tag')}/>
{fieldSettings}
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
}
<ButtonRow> <ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/> <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/fields/${this.props.list.id}/${this.props.entity.id}/delete`}/>}
</ButtonRow> </ButtonRow>
<hr />
<TreeTable data={sampleTreeData} />
</Form> </Form>
</div> </div>
); );

View file

@ -30,7 +30,6 @@ export default class List extends Component {
const columns = [ const columns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Match') },
{ {
actions: data => [{ actions: data => [{
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>, label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,

View file

@ -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 <numeric id> / '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'], <div>{enumOptions.errors.map((err, idx) => <div key={idx}>{err}</div>)}</div>);
} 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',
<span>
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
{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.')}
</span>
);
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 =
<Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
case 'checkbox':
case 'radio-grouped':
case 'dropdown-grouped':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array, for
example <code>{'{{#each values}} {{this}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'radio-enum':
case 'dropdown-enum':
fieldSettings =
<Fieldset label={t('Field settings')}>
<ACEEditor
id="enumOptions"
label={t('Options')}
height="250px"
mode="text"
help={<Trans><div>Specify the options to select from in the following format:<code>key|label</code>. For example:</div>
<div><code>au|Australia</code></div><div><code>at|Austria</code></div></Trans>}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="handlebars"
help={<Trans>You can control the appearance of the merge tag with this template. The template
uses handlebars syntax and you can find all values from <code>{'{{values}}'}</code> array.
Each entry in the array is an object with attributes <code>key</code> and <code>label</code>.
For example <code>{'{{#each values}} {{this.value}} {{/each}}'}</code>. If template is not defined then
multiple values are joined with commas.</Trans>}
/>
</Fieldset>;
break;
case 'date':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD/YYYY')},
{key: 'eur', label: t('DD/MM/YYYY')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'birthday':
fieldSettings =
<Fieldset label={t('Field settings')}>
<Dropdown id="dateFormat" label={t('Date format')}
options={[
{key: 'us', label: t('MM/DD')},
{key: 'eur', label: t('DD/MM')}
]}
/>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default value used when the field is empty.</Trans>}/>
</Fieldset>;
break;
case 'json':
fieldSettings = <Fieldset label={t('Field settings')}>
<InputField id="default_value" label={t('Default value')} help={<Trans>Default key (e.g. <code>au</code> used when the field is empty.')</Trans>}/>
<ACEEditor
id="renderTemplate"
label={t('Template')}
height="250px"
mode="json"
help={<Trans>You can use this template to render JSON values (if the JSON is an array then the array is
exposed as <code>values</code>, otherwise you can access the JSON keys directly).</Trans>}
/>
</Fieldset>;
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 =
<Fieldset label={t('Field settings')}>
<TableSelect id="group" label={t('Group')} withHeader dropdown dataUrl={`/rest/fields-grouped-table/${this.props.list.id}`} columns={fieldsGroupedColumns} selectionLabelIndex={1} help={t('Select group to which the options should belong.')}/>
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
</Fieldset>;
break;
}
return (
<div>
{isEdit &&
<DeleteModalDialog
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}`}
deletingMsg={t('Deleting field ...')}
deletedMsg={t('Field deleted')}/>
}
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
{isEdit ?
<StaticField id="type" className="mt-form-disabled" label={t('Type')}>{(this.fieldTypes[this.getFormValue('type')] || {}).label}</StaticField>
:
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
}
<InputField id="key" label={t('Merge tag')}/>
{fieldSettings}
{type !== 'option' &&
<Fieldset label={t('Field order')}>
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
</Fieldset>
}
<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`}/>}
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -125,7 +125,7 @@ export default class CUD extends Component {
try { try {
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving namespace ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url); const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);

View file

@ -134,7 +134,7 @@ export default class CUD extends Component {
} }
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving report ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
const params = {}; const params = {};

View file

@ -244,7 +244,7 @@ export default class CUD extends Component {
} }
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving report template ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url); const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url);

View file

@ -141,7 +141,7 @@ export default class CUD extends Component {
try { try {
this.disableForm(); this.disableForm();
this.setFormStatusMessage('info', t('Saving user ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
delete data.password2; delete data.password2;

View file

@ -10,6 +10,7 @@ const shares = require('./shares');
const bluebird = require('bluebird'); const bluebird = require('bluebird');
const validators = require('../shared/validators'); const validators = require('../shared/validators');
const shortid = require('shortid'); const shortid = require('shortid');
const segments = require('./segments');
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']); const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
const allowedKeysUpdate = new Set(['name', 'key', 'default_value', '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) { async function remove(context, listId, id) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields'); await removeTx(tx, context, listId, id);
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();
}
}); });
} }
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 = { module.exports = {
hash, hash,
getById, getById,
@ -389,5 +402,6 @@ module.exports = {
create, create,
updateWithConsistencyCheck, updateWithConsistencyCheck,
remove, remove,
removeAllByListIdTx,
serverValidate serverValidate
}; };

View file

@ -12,6 +12,7 @@ const fsReadFile = bluebird.promisify(require('fs').readFile);
const path = require('path'); const path = require('path');
const mjml = require('mjml'); const mjml = require('mjml');
const _ = require('../lib/translate')._; const _ = require('../lib/translate')._;
const lists = require('./lists');
const formAllowedKeys = new Set([ const formAllowedKeys = new Set([
'name', '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; 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 => { await knex.transaction(async tx => {
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete'); shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete');
const entity = await tx('custom_forms').where('id', id).first(); lists.removeFormFromAllTx(tx, context, id);
if (!entity) {
throw shares.throwPermissionDenied();
}
await tx('custom_forms_data').where('form', id).del(); await tx('custom_forms_data').where('form', id).del();
await tx('custom_forms').where('id', id).del(); await tx('custom_forms').where('id', id).del();

View file

@ -9,6 +9,7 @@ const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares'); const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers'); const namespaceHelpers = require('../lib/namespace-helpers');
const fields = require('./fields'); const fields = require('./fields');
const segments = require('./segments');
const UnsubscriptionMode = require('../shared/lists').UnsubscriptionMode; 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 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; return id;
}); });
@ -82,7 +83,7 @@ async function updateWithConsistencyCheck(context, entity) {
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys)); 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 knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'delete'); 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 tx('lists').where('id', id).del();
await knex.schema.dropTableIfExists('subscription__' + id); 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 = { module.exports = {
UnsubscriptionMode, UnsubscriptionMode,
@ -103,5 +118,6 @@ module.exports = {
getById, getById,
create, create,
updateWithConsistencyCheck, updateWithConsistencyCheck,
remove remove,
removeFormFromAllTx
}; };

View file

@ -126,7 +126,7 @@ async function create(context, entity) {
const id = ids[0]; 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. // 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; return id;
}); });
@ -166,7 +166,7 @@ async function updateWithConsistencyCheck(context, entity) {
await tx('namespaces').where('id', entity.id).update(filterObject(entity, allowedKeys)); await tx('namespaces').where('id', entity.id).update(filterObject(entity, allowedKeys));
await shares.rebuildPermissions(tx); await shares.rebuildPermissionsTx(tx);
}); });
} }

View file

@ -7,6 +7,7 @@ const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const namespaceHelpers = require('../lib/namespace-helpers'); const namespaceHelpers = require('../lib/namespace-helpers');
const shares = require('./shares'); const shares = require('./shares');
const reports = require('./reports');
const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs', 'namespace']); 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 ids = await tx('report_templates').insert(filterObject(entity, allowedKeys));
const id = ids[0]; const id = ids[0];
await shares.rebuildPermissions(tx, { entityTypeId: 'reportTemplate', entityId: id }); await shares.rebuildPermissionsTx(tx, { entityTypeId: 'reportTemplate', entityId: id });
return 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 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) { 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) { async function getUserFieldsById(context, id) {

View file

@ -68,7 +68,7 @@ async function create(context, entity) {
const ids = await tx('reports').insert(filterObject(entity, allowedKeys)); const ids = await tx('reports').insert(filterObject(entity, allowedKeys));
id = ids[0]; 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'); 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 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 // This require is here to avoid cyclic dependency
@ -111,12 +111,28 @@ async function updateWithConsistencyCheck(context, entity) {
await reportProcessor.start(entity.id); await reportProcessor.start(entity.id);
} }
async function remove(context, id) { async function removeTx(tx, context, id) {
await shares.enforceEntityPermission(context, 'report', id, 'delete'); 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) { async function updateFields(id, fields) {
return await knex('reports').where('id', id).update(fields); return await knex('reports').where('id', id).update(fields);
} }
@ -186,8 +202,9 @@ module.exports = {
create, create,
updateWithConsistencyCheck, updateWithConsistencyCheck,
remove, remove,
removeAllByReportTemplateIdTx,
updateFields, updateFields,
listByState, listByState,
bulkChangeState, bulkChangeState,
getCampaignResults getCampaignResults,
}; };

View file

@ -5,17 +5,11 @@ const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors'); const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares'); const shares = require('./shares');
//const allowedKeys = new Set(['cid', 'email']); const allowedKeys = new Set(['name', 'settings']);
/*
function hash(entity) { function hash(entity) {
const allowedKeys = allowedKeysBase.slice();
// TODO add keys from custom fields
return hasher.hash(filterObject(entity, allowedKeys)); return hasher.hash(filterObject(entity, allowedKeys));
} }
*/
async function listDTAjax(context, listId, params) { async function listDTAjax(context, listId, params) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
@ -27,12 +21,11 @@ async function listDTAjax(context, listId, params) {
builder => builder builder => builder
.from('segments') .from('segments')
.where('list', listId), .where('list', listId),
['id', 'name', 'type'] ['id', 'name']
); );
}); });
} }
async function list(context, listId) { async function list(context, listId) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions'); 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 = { module.exports = {
listDTAjax, listDTAjax,
list list,
create,
updateWithConsistencyCheck,
remove,
removeAllByListIdTx,
removeRulesByFieldIdTx
}; };

View file

@ -115,14 +115,16 @@ async function assign(context, entityTypeId, entityId, userId, role) {
await tx(entityType.permissionsTable).where({user: userId, entity: entityId}).del(); await tx(entityType.permissionsTable).where({user: userId, entity: entityId}).del();
if (entityTypeId === 'namespace') { if (entityTypeId === 'namespace') {
await rebuildPermissions(tx, {userId}); await rebuildPermissionsTx(tx, {userId});
} else if (role) { } 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'); const namespaceEntityType = permissions.getEntityType('namespace');
// Collect entity types we care about // Collect entity types we care about
@ -358,16 +360,10 @@ async function _rebuildPermissions(tx, restriction) {
} }
} }
async function rebuildPermissions(tx, restriction) { async function rebuildPermissions(restriction) {
restriction = restriction || {}; await knex.transaction(async tx => {
await rebuildPermissionsTx(tx, restriction);
if (tx) { });
await _rebuildPermissions(tx, restriction);
} else {
await knex.transaction(async tx => {
await _rebuildPermissions(tx, restriction);
});
}
} }
async function regenerateRoleNamesTable() { async function regenerateRoleNamesTable() {
@ -556,6 +552,7 @@ module.exports = {
listUnassignedUsersDTAjax, listUnassignedUsersDTAjax,
listRolesDTAjax, listRolesDTAjax,
assign, assign,
rebuildPermissionsTx,
rebuildPermissions, rebuildPermissions,
removeDefaultShares, removeDefaultShares,
enforceEntityPermission, enforceEntityPermission,

View file

@ -183,7 +183,7 @@ async function create(context, user) {
id = ids[0]; id = ids[0];
} }
await shares.rebuildPermissions(tx, { userId: id }); await shares.rebuildPermissionsTx(tx, { userId: id });
}); });
return id; return id;
@ -231,7 +231,7 @@ async function updateWithConsistencyCheck(context, user, isOwnAccount) {
await shares.removeDefaultShares(tx, existing); await shares.removeDefaultShares(tx, existing);
} }
await shares.rebuildPermissions(tx, { userId: user.id }); await shares.rebuildPermissionsTx(tx, { userId: user.id });
}); });
} }

View file

@ -2,8 +2,6 @@
const passport = require('../../lib/passport'); const passport = require('../../lib/passport');
const lists = require('../../models/lists'); const lists = require('../../models/lists');
const subscriptions = require('../../models/subscriptions');
const segments = require('../../models/segments');
const router = require('../../lib/router-async').create(); 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) => { router.putAsync('/lists/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const list = req.body; const entity = req.body;
list.id = parseInt(req.params.listId); entity.id = parseInt(req.params.listId);
await lists.updateWithConsistencyCheck(req.context, list); await lists.updateWithConsistencyCheck(req.context, entity);
return res.json(); return res.json();
}); });
@ -36,17 +34,5 @@ router.deleteAsync('/lists/:listId', passport.loggedIn, passport.csrfProtection,
return res.json(); 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; module.exports = router;

36
routes/rest/segments.js Normal file
View file

@ -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;

View file

@ -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;

View file

@ -5,6 +5,11 @@ exports.up = (knex, Promise) => (async() => {
table.json('settings'); 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'); const fields = await knex('custom_fields');
for (const field of fields) { for (const field of fields) {

View file

@ -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() => {
})();

View file

@ -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() => {
})();

View file

@ -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() => {
})();