diff --git a/client/src/report-templates/CUD.js b/client/src/reports/CUD.js similarity index 100% rename from client/src/report-templates/CUD.js rename to client/src/reports/CUD.js diff --git a/client/src/report-templates/List.js b/client/src/reports/List.js similarity index 100% rename from client/src/report-templates/List.js rename to client/src/reports/List.js diff --git a/client/src/report-templates/root.js b/client/src/reports/root.js similarity index 100% rename from client/src/report-templates/root.js rename to client/src/reports/root.js diff --git a/client/src/reports/templates/CUD.js b/client/src/reports/templates/CUD.js new file mode 100644 index 00000000..e19a2a44 --- /dev/null +++ b/client/src/reports/templates/CUD.js @@ -0,0 +1,350 @@ +'use strict'; + +import React, { Component } from 'react'; +import { translate, Trans } from 'react-i18next'; +import { withPageHelpers, Title } from '../lib/page' +import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEditor, ButtonRow, Button } from '../lib/form'; +import axios from '../lib/axios'; +import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; +import { ModalDialog } from '../lib/bootstrap-components'; + +@translate() +@withForm +@withPageHelpers +@withErrorHandling +export default class CUD extends Component { + constructor(props) { + super(props); + + this.state = {}; + + if (props.edit) { + this.state.entityId = parseInt(props.match.params.id); + } + + this.initForm(); + } + + isDelete() { + return this.props.match.params.action === 'delete'; + } + + @withAsyncErrorHandler + async loadFormValues() { + await this.getFormValuesFromURL(`/rest/report-templates/${this.state.entityId}`); + } + + componentDidMount() { + if (this.props.edit) { + this.loadFormValues(); + + } else { + const wizard = this.props.match.params.wizard; + + if (wizard === 'subscribers-all') { + this.populateFormValues({ + name: '', + description: 'Generates a campaign report listing all subscribers along with their statistics.', + mime_type: 'text/html', + user_fields: + '[\n' + + ' {\n' + + ' "id": "campaign",\n' + + ' "name": "Campaign",\n' + + ' "type": "campaign",\n' + + ' "minOccurences": 1,\n' + + ' "maxOccurences": 1\n' + + ' }\n' + + ']', + js: + 'campaigns.results(inputs.campaign, ["*"], "", (err, results) => {\n' + + ' if (err) {\n' + + ' return callback(err);\n' + + ' }\n' + + '\n' + + ' const data = {\n' + + ' results: results\n' + + ' };\n' + + '\n' + + ' return callback(null, data);\n' + + '});', + hbs: + '

{{title}}

\n' + + '\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' {{#if results}}\n' + + ' \n' + + ' {{#each results}}\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' {{/each}}\n' + + ' \n' + + ' {{/if}}\n' + + '
\n' + + ' {{#translate}}Email{{/translate}}\n' + + ' \n' + + ' {{#translate}}Tracker Count{{/translate}}\n' + + '
\n' + + ' {{email}}\n' + + ' \n' + + ' {{tracker_count}}\n' + + '
\n' + + '
' + }); + + } else if (wizard === 'subscribers-grouped') { + this.populateFormValues({ + name: '', + description: 'Generates a campaign report with results are aggregated by some "Country" custom field.', + mime_type: 'text/html', + user_fields: + '[\n' + + ' {\n' + + ' "id": "campaign",\n' + + ' "name": "Campaign",\n' + + ' "type": "campaign",\n' + + ' "minOccurences": 1,\n' + + ' "maxOccurences": 1\n' + + ' }\n' + + ']', + js: + 'campaigns.results(inputs.campaign, ["custom_country", "count(*) AS count_all", "SUM(IF(tracker.count IS NULL, 0, 1)) AS count_opened"], "GROUP BY custom_country", (err, results) => {\n' + + ' if (err) {\n' + + ' return callback(err);\n' + + ' }\n' + + '\n' + + ' for (let row of results) {\n' + + ' row["percentage"] = Math.round((row.count_opened / row.count_all) * 100);\n' + + ' }\n' + + '\n' + + ' let data = {\n' + + ' results: results\n' + + ' };\n' + + '\n' + + ' return callback(null, data);\n' + + '});', + hbs: + '

{{title}}

\n' + + '\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' {{#if results}}\n' + + ' \n' + + ' {{#each results}}\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' {{/each}}\n' + + ' \n' + + ' {{/if}}\n' + + '
\n' + + ' {{#translate}}Country{{/translate}}\n' + + ' \n' + + ' {{#translate}}Opened{{/translate}}\n' + + ' \n' + + ' {{#translate}}All{{/translate}}\n' + + ' \n' + + ' {{#translate}}Percentage{{/translate}}\n' + + '
\n' + + ' {{custom_country}}\n' + + ' \n' + + ' {{count_opened}}\n' + + ' \n' + + ' {{count_all}}\n' + + ' \n' + + ' {{percentage}}%\n' + + '
\n' + + '
' + }); + + } else if (wizard === 'export-list-csv') { + this.populateFormValues({ + name: '', + description: 'Exports a list as a CSV file.', + mime_type: 'text/csv', + user_fields: + '[\n' + + ' {\n' + + ' "id": "list",\n' + + ' "name": "List",\n' + + ' "type": "list",\n' + + ' "minOccurences": 1,\n' + + ' "maxOccurences": 1\n' + + ' }\n' + + ']', + js: + 'subscriptions.list(inputs.list.id,0,0, (err, results) => {\n' + + ' if (err) {\n' + + ' return callback(err);\n' + + ' }\n' + + '\n' + + ' let data = {\n' + + ' results: results\n' + + ' };\n' + + '\n' + + ' return callback(null, data);\n' + + '});', + hbs: + '{{#each results}}\n' + + '{{firstName}},{{lastName}},{{email}}\n' + + '{{/each}}' + }); + + } else { + this.populateFormValues({ + name: '', + description: '', + mime_type: 'text/html', + user_fields: '', + js: '', + hbs: '' + }); + } + } + } + + localValidateFormValues(state) { + const t = this.props.t; + const edit = this.props.edit; + + if (!state.getIn(['name', 'value'])) { + state.setIn(['name', 'error'], t('Name must not be empty')); + } else { + state.setIn(['name', 'error'], null); + } + + if (!state.getIn(['mime_type', 'value'])) { + state.setIn(['mime_type', 'error'], t('MIME Type must be selected')); + } else { + state.setIn(['mime_type', 'error'], null); + } + + try { + const userFields = JSON.parse(state.getIn(['user_fields', 'value'])); + state.setIn(['user_fields', 'error'], null); + } catch (err) { + if (err instanceof SyntaxError) { + state.setIn(['user_fields', 'error'], t('Syntax error in the user fields specification')); + } + } + } + + async submitAndStay() { + await Form.handleChangedError(this, async () => await this.doSubmit(true)); + } + + async submitAndLeave() { + await Form.handleChangedError(this, async () => await this.doSubmit(false)); + } + + async doSubmit(stay) { + const t = this.props.t; + const edit = this.props.edit; + + let sendMethod, url; + if (edit) { + sendMethod = FormSendMethod.PUT; + url = `/rest/report-templates/${this.state.entityId}` + } else { + sendMethod = FormSendMethod.POST; + url = '/rest/report-templates' + } + + this.disableForm(); + this.setFormStatusMessage('info', t('Saving report template ...')); + + const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { + delete data.password2; + }); + + if (submitSuccessful) { + if (stay) { + this.enableForm(); + this.setFormStatusMessage('success', t('Report template saved')); + } else { + this.navigateToWithFlashMessage('/reports/templates', 'success', t('Report template saved')); + } + } else { + this.enableForm(); + this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); + } + } + + async showDeleteModal() { + this.navigateTo(`/reports/templates/edit/${this.state.entityId}/delete`); + } + + async hideDeleteModal() { + this.navigateTo(`/reports/templates/edit/${this.state.entityId}`); + } + + async performDelete() { + const t = this.props.t; + + await this.hideDeleteModal(); + + this.disableForm(); + this.setFormStatusMessage('info', t('Deleting report template...')); + + await axios.delete(`/rest/report-templates/${this.state.entityId}`); + + this.navigateToWithFlashMessage('/reports/templates', 'success', t('Report template deleted')); + } + + render() { + const t = this.props.t; + const edit = this.props.edit; + + return ( +
+ {edit && + + } + + {edit ? t('Edit Report Template') : t('Create Report Template')} + +
+ +