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
This commit is contained in:
Tomas Bures 2018-04-02 19:05:22 +02:00
parent 7b5642e911
commit 6706d93bc1
21 changed files with 2192 additions and 26 deletions

View file

@ -61,7 +61,7 @@ function getMenus(t) {
title: t('Create'),
panelRender: props => <ReportsCUD action="create" />
},
'templates': {
templates: {
title: t('Templates'),
link: '/reports/templates',
panelComponent: ReportTemplatesList,

View file

@ -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 (
<div>
{this.state.createPermitted &&
<Toolbar>
<NavButton linkTo="/templates/create" className="btn-primary" icon="plus" label={t('Create Template')}/>
<Toolbar>
{this.state.createPermitted &&
<NavButton linkTo="/templates/create" className="btn-primary" icon="plus" label={t('Create Template')}/>
}
{this.state.mosaicoTemplatesPermitted &&
<NavButton linkTo="/templates/mosaico" className="btn-primary" label={t('Mosaico Templates')}/>
</Toolbar>
}
}
</Toolbar>
<Title>{t(' Templates')}</Title>
<Title>{t('Templates')}</Title>
<Table withHeader dataUrl="/rest/templates-table" columns={columns} />
</div>

View file

@ -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 (
<div>
{canDelete &&
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/templates/mosaico/${this.props.entity.id}`}
cudUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
listUrl="/templates/mosaico"
deletingMsg={t('Deleting mosaico template ...')}
deletedMsg={t('Mosaico template deleted')}/>
}
<Title>{isEdit ? t('Edit Mosaico Template') : t('Create Mosaico Template')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
<NamespaceSelect/>
{form}
{isEdit ?
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Stay')} onClickAsync={::this.submitAndStay}/>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save and Leave')}/>
{canDelete &&
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/templates/mosaico/${this.props.entity.id}/delete`}/>
}
</ButtonRow>
:
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
</ButtonRow>
}
</Form>
</div>
);
}
}

View file

@ -0,0 +1,97 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import {DropdownMenu, Icon} from '../../lib/bootstrap-components';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, DropdownLink } from '../../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
import { Table } from '../../lib/table';
import axios from '../../lib/axios';
import moment from 'moment';
import { getTemplateTypes } from './helpers';
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
super(props);
this.templateTypes = getTemplateTypes(props.t);
this.state = {};
}
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
createMosaicoTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createMosaicoTemplate']
}
};
const result = await axios.post('/rest/permissions-check', request);
this.setState({
createPermitted: result.data.createMosaicoTemplate
});
}
componentDidMount() {
this.fetchPermissions();
}
render() {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') },
{
actions: data => {
const actions = [];
const perms = data[6];
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/templates/mosaico/${data[0]}/edit`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
link: `/templates/mosaico/${data[0]}/share`
});
}
return actions;
}
}
];
return (
<div>
{this.state.createPermitted &&
<Toolbar>
<DropdownMenu className="btn-primary" label={t('Create Mosaico Template')}>
<DropdownLink to="/templates/mosaico/create">{t('Blank')}</DropdownLink>
<DropdownLink to="/templates/mosaico/create/versafix">{t('Versafix One')}</DropdownLink>
</DropdownMenu>
</Toolbar>
}
<Title>{t('Mosaico Templates')}</Title>
<Table withHeader dataUrl="/rest/mosaico-templates-table" columns={columns} />
</div>
);
}
}

View file

@ -0,0 +1,48 @@
'use strict';
import React from "react";
import {ACEEditor} from "../../lib/form";
import 'brace/mode/html'
import 'brace/mode/xml'
export function getTemplateTypes(t) {
const templateTypes = {};
function clearBeforeSend(data) {
delete data.html;
delete data.mjml;
}
templateTypes.html = {
typeName: t('HTML'),
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('Template content')}/>,
afterLoad: data => {
console.log(data);
data.html = data.data.html;
},
beforeSave: (data) => {
data.data = {
html: data.html
};
clearBeforeSend(data);
},
};
templateTypes.mjml = {
typeName: t('MJML'),
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('Template content')}/>,
afterLoad: data => {
data.mjml = data.data.mjml;
},
beforeSave: (data) => {
data.data = {
mjml: data.mjml
};
clearBeforeSend(data);
},
};
return templateTypes;
}

View file

@ -6,6 +6,8 @@ import TemplatesCUD from './CUD';
import TemplatesList from './List';
import Share from '../shares/Share';
import Files from "../lib/files";
import MosaicoCUD from './mosaico/CUD';
import MosaicoList from './mosaico/List';
function getMenus(t) {
@ -45,6 +47,45 @@ function getMenus(t) {
create: {
title: t('Create'),
panelRender: props => <TemplatesCUD action="create" />
},
mosaico: {
title: t('Mosaico Templates'),
link: '/templates/mosaico',
panelComponent: MosaicoList,
children: {
':mosaiceTemplateId([0-9]+)': {
title: resolved => t('Mosaico Template "{{name}}"', {name: resolved.mosaicoTemplate.name}),
resolve: {
mosaicoTemplate: params => `/rest/mosaico-templates/${params.mosaiceTemplateId}`
},
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'),
panelRender: props => <MosaicoCUD action={props.match.params.action} entity={props.resolved.mosaicoTemplate} />
},
files: {
title: t('Files'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/files`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'),
panelRender: props => <Files title={t('Files')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" />
},
share: {
title: t('Share'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/share`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.mosaicoTemplate} entityTypeId="mosaicoTemplate" />
}
}
},
create: {
title: t('Create'),
extraParams: [':wizard?'],
panelRender: props => <MosaicoCUD action="create" wizard={props.match.params.wizard} />
}
}
}
}
}