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:
parent
7b5642e911
commit
6706d93bc1
21 changed files with 2192 additions and 26 deletions
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
192
client/src/templates/mosaico/CUD.js
Normal file
192
client/src/templates/mosaico/CUD.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
97
client/src/templates/mosaico/List.js
Normal file
97
client/src/templates/mosaico/List.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
48
client/src/templates/mosaico/helpers.js
Normal file
48
client/src/templates/mosaico/helpers.js
Normal 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;
|
||||
}
|
|
@ -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} />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue