From 6d95fa515e1c8b329e5b25371aa7bface05df412 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Tue, 11 Jul 2017 11:28:44 +0200 Subject: [PATCH] CUD operations on reports and report templates seem to work Execution of reports is TBD --- app.js | 24 ++--- client/src/lib/form.js | 43 ++++++--- client/src/lib/page.js | 2 + client/src/lib/table.js | 11 ++- client/src/reports/CUD.js | 135 ++++++++++++++++++++++++++-- client/src/reports/List.js | 2 +- client/src/reports/templates/CUD.js | 4 +- lib/dt-helpers.js | 2 + lib/knex.js | 2 +- models/campaigns.js | 13 +++ models/lists.js | 13 +++ models/report-templates.js | 12 ++- models/reports.js | 19 ++-- package.json | 2 +- routes/rest/campaigns.js | 14 +++ routes/rest/lists.js | 14 +++ routes/rest/report-templates.js | 5 ++ routes/rest/reports.js | 2 +- 18 files changed, 273 insertions(+), 46 deletions(-) create mode 100644 models/campaigns.js create mode 100644 models/lists.js create mode 100644 routes/rest/campaigns.js create mode 100644 routes/rest/lists.js 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 {