diff --git a/app.js b/app.js
index 345110e5..14912aa6 100644
--- a/app.js
+++ b/app.js
@@ -40,11 +40,13 @@ const editorapi = require('./routes/editorapi');
const grapejs = require('./routes/grapejs');
const mosaico = require('./routes/mosaico');
-const namespaces = require('./routes/rest/namespaces');
-const users = require('./routes/rest/users');
-const account = require('./routes/rest/account');
-const reportTemplates = require('./routes/rest/report-templates');
-const reports = require('./routes/rest/reports');
+const namespacesRest = require('./routes/rest/namespaces');
+const usersRest = require('./routes/rest/users');
+const accountRest = require('./routes/rest/account');
+const reportTemplatesRest = require('./routes/rest/report-templates');
+const reportsRest = require('./routes/rest/reports');
+const campaignsRest = require('./routes/rest/campaigns');
+const listsRest = require('./routes/rest/lists');
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
const usersLegacyIntegration = require('./routes/users-legacy-integration');
@@ -257,13 +259,15 @@ app.all('/rest/*', (req, res, next) => {
next();
});
-app.use('/rest', namespaces);
-app.use('/rest', users);
-app.use('/rest', account);
+app.use('/rest', namespacesRest);
+app.use('/rest', usersRest);
+app.use('/rest', accountRest);
+app.use('/rest', campaignsRest);
+app.use('/rest', listsRest);
if (config.reports && config.reports.enabled === true) {
- app.use('/rest', reportTemplates);
- app.use('/rest', reports);
+ app.use('/rest', reportTemplatesRest);
+ app.use('/rest', reportsRest);
}
// catch 404 and forward to error handler
diff --git a/client/src/lib/form.js b/client/src/lib/form.js
index 43eb47b7..2453ec32 100644
--- a/client/src/lib/form.js
+++ b/client/src/lib/form.js
@@ -402,6 +402,7 @@ class TableSelect extends Component {
columns: PropTypes.array,
selectionKeyIndex: PropTypes.number,
selectionLabelIndex: PropTypes.number,
+ selectionAsArray: PropTypes.bool,
selectMode: PropTypes.number,
withHeader: PropTypes.bool,
dropdown: PropTypes.bool,
@@ -432,9 +433,19 @@ class TableSelect extends Component {
}
async onSelectionDataAsync(sel, data) {
- if (this.props.selectMode === TableSelectMode.SINGLE && this.props.dropdown) {
+ if (this.props.dropdown) {
+ let label;
+
+ if (!data) {
+ label = '';
+ } else if (this.props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
+ label = data[this.props.selectionLabelIndex];
+ } else {
+ label = data.map(entry => entry[this.props.selectionLabelIndex]).join('; ');
+ }
+
this.setState({
- selectedLabel: data ? data[this.props.selectionLabelIndex] : ''
+ selectedLabel: label
});
}
}
@@ -462,7 +473,7 @@ class TableSelect extends Component {
-
+
);
@@ -470,7 +481,7 @@ class TableSelect extends Component {
return wrapInput(id, htmlId, owner, props.label, props.help,
-
+
);
@@ -706,12 +717,24 @@ function withForm(target) {
};
inst.updateFormValue = function(key, value) {
- this.setState(previousState => ({
- formState: previousState.formState.withMutations(mutState => {
- mutState.setIn(['data', key, 'value'], value);
- validateFormState(this, mutState);
- })
- }));
+ this.setState(previousState => {
+ const oldValue = previousState.formState.getIn(['data', key, 'value']);
+
+ let newState = {
+ formState: previousState.formState.withMutations(mutState => {
+ mutState.setIn(['data', key, 'value'], value);
+ validateFormState(this, mutState);
+ })
+ };
+
+ const onChangeCallbacks = this.state.formSettings.onChange || {};
+
+ if (onChangeCallbacks[key]) {
+ onChangeCallbacks[key](newState, key, oldValue, value);
+ }
+
+ return newState;
+ });
};
inst.getFormValue = function(name) {
diff --git a/client/src/lib/page.js b/client/src/lib/page.js
index 284f82ca..45d86b83 100644
--- a/client/src/lib/page.js
+++ b/client/src/lib/page.js
@@ -205,8 +205,10 @@ class SectionContent extends Component {
/* FIXME, once we turn Mailtrain to single-page application, this should become navigateTo */
window.location = '/account/login?next=' + encodeURIComponent(this.props.root);
} else if (error.response && error.response.data && error.response.data.message) {
+ console.error(error);
this.navigateToWithFlashMessage(this.props.root, 'danger', error.response.data.message);
} else {
+ console.error(error);
this.navigateToWithFlashMessage(this.props.root, 'danger', error.message);
}
return true;
diff --git a/client/src/lib/table.js b/client/src/lib/table.js
index eeb4e9a4..670015c4 100644
--- a/client/src/lib/table.js
+++ b/client/src/lib/table.js
@@ -45,6 +45,7 @@ class Table extends Component {
selectMode: PropTypes.number,
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
selectionKeyIndex: PropTypes.number,
+ selectionAsArray: PropTypes.bool,
onSelectionChangedAsync: PropTypes.func,
onSelectionDataAsync: PropTypes.func,
actionLinks: PropTypes.array,
@@ -58,14 +59,14 @@ class Table extends Component {
getSelectionMap(props) {
let selArray = [];
- if (props.selectMode === TableSelectMode.SINGLE) {
+ if (props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
if (props.selection !== null && props.selection !== undefined) {
selArray = [props.selection];
} else {
selArray = [];
}
- } else if (props.selectMode === TableSelectMode.MULTI) {
- selArray = props.selection;
+ } else if ((props.selectMode === TableSelectMode.SINGLE && this.props.selectionAsArray) || props.selectMode === TableSelectMode.MULTI) {
+ selArray = props.selection || [];
}
const selMap = new Map();
@@ -126,8 +127,6 @@ class Table extends Component {
values: keysToFetch
});
- console.log(response.data);
-
for (const row of response.data) {
const key = row[this.props.selectionKeyIndex];
if (this.selectionMap.has(key)) {
@@ -270,7 +269,7 @@ class Table extends Component {
let data = selPairs.map(entry => entry[1]);
let sel = selPairs.map(entry => entry[0]);
- if (this.props.selectMode === TableSelectMode.SINGLE) {
+ if (this.props.selectMode === TableSelectMode.SINGLE && !this.props.selectionAsArray) {
if (sel.length) {
sel = sel[0];
data = data[0];
diff --git a/client/src/reports/CUD.js b/client/src/reports/CUD.js
index 3f584ee4..203c1450 100644
--- a/client/src/reports/CUD.js
+++ b/client/src/reports/CUD.js
@@ -3,7 +3,10 @@
import React, { Component } from 'react';
import { translate, Trans } from 'react-i18next';
import { withPageHelpers, Title } from '../lib/page'
-import { withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, TableSelectMode, ButtonRow, Button } from '../lib/form';
+import {
+ withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, TableSelectMode, ButtonRow, Button,
+ Fieldset
+} from '../lib/form';
import axios from '../lib/axios';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import { ModalDialog } from '../lib/bootstrap-components';
@@ -23,16 +26,40 @@ export default class CUD extends Component {
this.state.entityId = parseInt(props.match.params.id);
}
- this.initForm();
+ this.initForm({
+ onChange: {
+ report_template: ::this.onReportTemplateChange
+ }
+ });
}
isDelete() {
return this.props.match.params.action === 'delete';
}
+ @withAsyncErrorHandler
+ async fetchUserFields(reportTemplateId) {
+ const result = await axios.get(`/rest/report-template-user-fields/${reportTemplateId}`);
+ this.updateFormValue('user_fields', result.data);
+ }
+
+ onReportTemplateChange(state, key, oldVal, newVal) {
+ if (oldVal !== newVal) {
+ state.formState = state.formState.setIn(['data', 'user_fields', 'value'], '');
+
+ if (newVal) {
+ this.fetchUserFields(newVal);
+ }
+ }
+ }
+
@withAsyncErrorHandler
async loadFormValues() {
- await this.getFormValuesFromURL(`/rest/reports/${this.state.entityId}`);
+ await this.getFormValuesFromURL(`/rest/reports/${this.state.entityId}`, data => {
+ for (const key in data.params) {
+ data[`param_${key}`] = data.params[key];
+ }
+ });
}
componentDidMount() {
@@ -40,9 +67,10 @@ export default class CUD extends Component {
this.loadFormValues();
} else {
this.populateFormValues({
- report_template: null,
name: '',
- description: ''
+ description: '',
+ report_template: null,
+ user_fields: null
});
}
}
@@ -62,12 +90,43 @@ export default class CUD extends Component {
} else {
state.setIn(['report_template', 'error'], null);
}
+
+ for (const paramId of state.keys()) {
+ if (paramId.startsWith('param_')) {
+ state.deleteIn([paramId, 'error']);
+ }
+ }
+
+ const userFieldsSpec = state.getIn(['user_fields', 'value']);
+ if (userFieldsSpec) {
+ for (const spec of userFieldsSpec) {
+ const fldId = `param_${spec.id}`;
+ const selection = state.getIn([fldId, 'value']) || [];
+
+ if (spec.maxOccurences === 1) {
+ if (spec.minOccurences === 1 && (selection === null || selection === undefined)) {
+ state.setIn([fldId, 'error'], t('Exactly one item has to be selected'));
+ }
+ } else {
+ if (selection.length < spec.minOccurences) {
+ state.setIn([fldId, 'error'], t('At least {{ count }} item(s) have to be selected', { count: spec.minOccurences }));
+ } else if (selection.length > spec.maxOccurences) {
+ state.setIn([fldId, 'error'], t('At most {{ count }} item(s) can to be selected', { count: spec.maxOccurences }));
+ }
+ }
+ }
+ }
}
async submitHandler() {
const t = this.props.t;
const edit = this.props.edit;
+ if (!this.getFormValue('user_fields')) {
+ this.setFormStatusMessage('warning', t('Report parameters are not selected. Wait for them to get displayed and then fill them in.'));
+ return;
+ }
+
let sendMethod, url;
if (edit) {
sendMethod = FormSendMethod.PUT;
@@ -81,7 +140,16 @@ export default class CUD extends Component {
this.setFormStatusMessage('info', t('Saving report template ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
- delete data.password2;
+ const params = {};
+
+ for (const spec of data.user_fields) {
+ const fldId = `param_${spec.id}`;
+ params[spec.id] = data[fldId];
+ delete data[fldId];
+ }
+
+ delete data.user_fields;
+ data.params = params;
});
if (submitSuccessful) {
@@ -124,6 +192,49 @@ export default class CUD extends Component {
{ data: 3, title: t('Created'), render: data => moment(data).fromNow() }
];
+ const userFieldsSpec = this.getFormValue('user_fields');
+ const userFields = [];
+
+ function addUserFieldTableSelect(spec, dataUrl, selIndex, columns) {
+ let dropdown, selectMode;
+
+ if (spec.maxOccurences === 1) {
+ dropdown = true;
+ selectMode = TableSelectMode.SINGLE;
+ } else {
+ dropdown = true;
+ selectMode = TableSelectMode.MULTI;
+ }
+
+ const fld = ;
+
+ userFields.push(fld);
+ }
+
+ if (userFieldsSpec) {
+ for (const spec of userFieldsSpec) {
+ if (spec.type === 'campaign') {
+ addUserFieldTableSelect(spec, '/rest/campaigns-table', 1,[
+ {data: 0, title: "#"},
+ {data: 1, title: t('Name')},
+ {data: 2, title: t('Description')},
+ {data: 3, title: t('Status')},
+ {data: 4, title: t('Created'), render: data => moment(data).fromNow()}
+ ]);
+ } else if (spec.type === 'list') {
+ addUserFieldTableSelect(spec, '/rest/lists-table', 1,[
+ {data: 0, title: "#"},
+ {data: 1, title: t('Name')},
+ {data: 2, title: t('ID')},
+ {data: 3, title: t('Subscribers')},
+ {data: 4, title: t('Description')}
+ ]);
+ } else {
+ userFields.push({t('Unknown field type "{{type}}"', { type: spec.type })}
)
+ }
+ }
+ }
+
return (
{edit &&
@@ -141,7 +252,17 @@ export default class CUD extends Component {
-
+
+
+ {userFieldsSpec ?
+ userFields.length > 0 &&
+
+ :
+ this.getFormValue('report_template') &&
+ {t('Loading report template...')}
+ }