From 6706d93bc1d94d0f6ab60323e84cb99d1fff869f Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Mon, 2 Apr 2018 19:05:22 +0200 Subject: [PATCH] Basic support for Mosaico templates. TODO: - Allow choosing a mosaico template in a mosaico-based template - Integrate the custom mosaico templates with templates (endpoint for retrieving a mosaico template, replacement of URL_BASE and PLACEHOLDER tags - Implement support for MJML-based Mosaico templates - Implement support for MJML-based templates - Implement support for GrapeJS-based templates --- app-builder.js | 2 + client/src/reports/root.js | 2 +- client/src/templates/List.js | 25 +- client/src/templates/mosaico/CUD.js | 192 ++ client/src/templates/mosaico/List.js | 97 ++ client/src/templates/mosaico/helpers.js | 48 + client/src/templates/root.js | 41 + config/default.toml | 13 +- lib/permissions.js | 12 +- models/files.js | 17 +- models/mosaico-templates.js | 110 ++ models/templates.js | 6 +- routes/mosaico.js | 1 + routes/rest/mosaico-templates.js | 36 + .../20170731072050_upgrade_custom_fields.js | 2 +- .../20170814174051_upgrade_segments.js | 2 +- .../migrations/20180110120444_add_files.js | 2 +- .../20180111120444_upgrade_templates.js | 2 +- ...20180401120444_create_mosaico_templates.js | 61 + shared/interoperable-errors.js | 10 +- shared/mosaico-templates.js | 1537 +++++++++++++++++ 21 files changed, 2192 insertions(+), 26 deletions(-) create mode 100644 client/src/templates/mosaico/CUD.js create mode 100644 client/src/templates/mosaico/List.js create mode 100644 client/src/templates/mosaico/helpers.js create mode 100644 models/mosaico-templates.js create mode 100644 routes/rest/mosaico-templates.js create mode 100644 setup/knex/migrations/20180401120444_create_mosaico_templates.js create mode 100644 shared/mosaico-templates.js diff --git a/app-builder.js b/app-builder.js index 6d34202b..56171622 100644 --- a/app-builder.js +++ b/app-builder.js @@ -45,6 +45,7 @@ const sharesRest = require('./routes/rest/shares'); const segmentsRest = require('./routes/rest/segments'); const subscriptionsRest = require('./routes/rest/subscriptions'); const templatesRest = require('./routes/rest/templates'); +const mosaicoTemplatesRest = require('./routes/rest/mosaico-templates'); const blacklistRest = require('./routes/rest/blacklist'); const editorsRest = require('./routes/rest/editors'); const filesRest = require('./routes/rest/files'); @@ -279,6 +280,7 @@ function createApp(trusted) { app.use('/rest', segmentsRest); app.use('/rest', subscriptionsRest); app.use('/rest', templatesRest); + app.use('/rest', mosaicoTemplatesRest); app.use('/rest', blacklistRest); app.use('/rest', editorsRest); app.use('/rest', filesRest); diff --git a/client/src/reports/root.js b/client/src/reports/root.js index de6c33e4..f9bb7006 100644 --- a/client/src/reports/root.js +++ b/client/src/reports/root.js @@ -61,7 +61,7 @@ function getMenus(t) { title: t('Create'), panelRender: props => }, - 'templates': { + templates: { title: t('Templates'), link: '/reports/templates', panelComponent: ReportTemplatesList, diff --git a/client/src/templates/List.js b/client/src/templates/List.js index da70dc36..f5852611 100644 --- a/client/src/templates/List.js +++ b/client/src/templates/List.js @@ -29,13 +29,22 @@ export default class List extends Component { createTemplate: { entityTypeId: 'namespace', requiredOperations: ['createTemplate'] + }, + createMosaicoTemplate: { + entityTypeId: 'namespace', + requiredOperations: ['createMosaicoTemplate'] + }, + viewMosaicoTemplate: { + entityTypeId: 'mosaicoTemplate', + requiredOperations: ['view'] } }; const result = await axios.post('/rest/permissions-check', request); this.setState({ - createPermitted: result.data.createTemplate + createPermitted: result.data.createTemplate, + mosaicoTemplatesPermitted: result.data.createMosaicoTemplate || result.data.viewMosaicoTemplate }); } @@ -85,14 +94,16 @@ export default class List extends Component { return (
- {this.state.createPermitted && - - + + {this.state.createPermitted && + + } + {this.state.mosaicoTemplatesPermitted && - - } + } + - {t(' Templates')} + {t('Templates')} diff --git a/client/src/templates/mosaico/CUD.js b/client/src/templates/mosaico/CUD.js new file mode 100644 index 00000000..b8599b50 --- /dev/null +++ b/client/src/templates/mosaico/CUD.js @@ -0,0 +1,192 @@ +'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, Dropdown, ButtonRow, Button } from '../../lib/form'; +import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; +import { validateNamespace, NamespaceSelect } from '../../lib/namespace'; +import {DeleteModalDialog} from "../../lib/modals"; + +import { versafix } from "../../../../shared/mosaico-templates"; +import { getTemplateTypes } from "./helpers"; + +@translate() +@withForm +@withPageHelpers +@withErrorHandling +@requiresAuthenticatedUser +export default class CUD extends Component { + constructor(props) { + super(props); + + this.templateTypes = getTemplateTypes(props.t); + + this.state = {}; + + this.initForm(); + } + + static propTypes = { + action: PropTypes.string.isRequired, + wizard: PropTypes.string, + entity: PropTypes.object + } + + @withAsyncErrorHandler + async loadFormValues() { + await this.getFormValuesFromURL(`/rest/mosaico-templates/${this.props.entity.id}`, data => { + this.templateTypes[data.type].afterLoad(data); + }); + } + + componentDidMount() { + if (this.props.entity) { + this.getFormValuesFromEntity(this.props.entity, data => { + this.templateTypes[data.type].afterLoad(data); + }); + + } else { + const wizard = this.props.wizard; + + if (wizard === 'versafix') { + this.populateFormValues({ + name: '', + description: '', + namespace: mailtrainConfig.user.namespace, + type: 'html', + html: versafix + }); + + } else { + this.populateFormValues({ + name: '', + description: '', + namespace: mailtrainConfig.user.namespace, + type: 'html', + html: '' + }); + } + } + } + + 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); + } + + if (!state.getIn(['type', 'value'])) { + state.setIn(['type', 'error'], t('Type must be selected')); + } else { + state.setIn(['type', 'error'], null); + } + + validateNamespace(t, state); + } + + async submitAndStay() { + await this.formHandleChangedError(async () => await this.doSubmit(true)); + } + + async submitAndLeave() { + await this.formHandleChangedError(async () => await this.doSubmit(false)); + } + + async doSubmit(stay) { + const t = this.props.t; + + let sendMethod, url; + if (this.props.entity) { + sendMethod = FormSendMethod.PUT; + url = `/rest/mosaico-templates/${this.props.entity.id}` + } else { + sendMethod = FormSendMethod.POST; + url = '/rest/mosaico-templates' + } + + this.disableForm(); + this.setFormStatusMessage('info', t('Saving ...')); + + const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { + this.templateTypes[data.type].beforeSave(data); + }); + + if (submitSuccessful) { + if (stay) { + await this.loadFormValues(); + this.enableForm(); + this.setFormStatusMessage('success', t('Mosaico template saved')); + } else { + this.navigateToWithFlashMessage('/templates/mosaico', 'success', t('Mosaico template saved')); + } + } else { + this.enableForm(); + this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.')); + } + } + + render() { + const t = this.props.t; + const isEdit = !!this.props.entity; + const canDelete = isEdit && this.props.entity.permissions.includes('delete'); + + const typeKey = this.getFormValue('type'); + let form = null; + if (typeKey) { + form = this.templateTypes[typeKey].getForm(this); + } + + const typeOptions = []; + for (const type of ['html', 'mjml']) { + typeOptions.push({ + key: type, + label: this.templateTypes.typeName + }); + } + + return ( +
+ {canDelete && + + } + + {isEdit ? t('Edit Mosaico Template') : t('Create Mosaico Template')} + +
+ +