work in progress on segments
some cleanup of models - handling dependencies in delete
This commit is contained in:
parent
b23529a75b
commit
0bfb30817b
29 changed files with 553 additions and 990 deletions
4
app.js
4
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);
|
||||
|
|
|
@ -91,9 +91,11 @@ class TreeTable extends Component {
|
|||
const data = [];
|
||||
for (const unsafeEntry of unsafeData) {
|
||||
const entry = Object.assign({}, unsafeEntry);
|
||||
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>)
|
||||
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>)
|
||||
entry.children = this.sanitizeTreeData(entry.children);
|
||||
entry.title = ReactDOMServer.renderToStaticMarkup(<div>{entry.title}</div>);
|
||||
entry.description = ReactDOMServer.renderToStaticMarkup(<div>{entry.description}</div>);
|
||||
if (entry.children) {
|
||||
entry.children = this.sanitizeTreeData(entry.children);
|
||||
}
|
||||
data.push(entry);
|
||||
}
|
||||
return data;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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' &&
|
||||
<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".')}/>
|
||||
<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={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={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>
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 => <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: {
|
||||
title: t('Create Field'),
|
||||
render: props => <FieldsCUD action="create" list={props.resolved.list} />
|
||||
title: t('Create'),
|
||||
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'),
|
||||
link: params => `/lists/${params.listId}/segments`,
|
||||
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: {
|
||||
title: t('Share'),
|
||||
|
|
|
@ -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 <numeric id> / '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'], <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}`
|
||||
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 =
|
||||
<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 (
|
||||
|
@ -445,40 +156,26 @@ export default class CUD extends Component {
|
|||
<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')}/>
|
||||
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')}/>
|
||||
}
|
||||
|
||||
<Title>{isEdit ? t('Edit Field') : t('Create Field')}</Title>
|
||||
<Title>{isEdit ? t('Edit Segment') : t('Create Segment')}</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>
|
||||
|
||||
<hr />
|
||||
|
||||
<TreeTable data={sampleTreeData} />
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 = {};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
36
routes/rest/segments.js
Normal file
36
routes/rest/segments.js
Normal 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;
|
14
routes/rest/subscriptions.js
Normal file
14
routes/rest/subscriptions.js
Normal 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;
|
|
@ -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) {
|
|
@ -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() => {
|
||||
})();
|
175
setup/knex/migrations/20170814174051_upgrade_segments.js
Normal file
175
setup/knex/migrations/20170814174051_upgrade_segments.js
Normal 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() => {
|
||||
})();
|
|
@ -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() => {
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue