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
|
@ -45,6 +45,7 @@ const sharesRest = require('./routes/rest/shares');
|
||||||
const segmentsRest = require('./routes/rest/segments');
|
const segmentsRest = require('./routes/rest/segments');
|
||||||
const subscriptionsRest = require('./routes/rest/subscriptions');
|
const subscriptionsRest = require('./routes/rest/subscriptions');
|
||||||
const templatesRest = require('./routes/rest/templates');
|
const templatesRest = require('./routes/rest/templates');
|
||||||
|
const mosaicoTemplatesRest = require('./routes/rest/mosaico-templates');
|
||||||
const blacklistRest = require('./routes/rest/blacklist');
|
const blacklistRest = require('./routes/rest/blacklist');
|
||||||
const editorsRest = require('./routes/rest/editors');
|
const editorsRest = require('./routes/rest/editors');
|
||||||
const filesRest = require('./routes/rest/files');
|
const filesRest = require('./routes/rest/files');
|
||||||
|
@ -279,6 +280,7 @@ function createApp(trusted) {
|
||||||
app.use('/rest', segmentsRest);
|
app.use('/rest', segmentsRest);
|
||||||
app.use('/rest', subscriptionsRest);
|
app.use('/rest', subscriptionsRest);
|
||||||
app.use('/rest', templatesRest);
|
app.use('/rest', templatesRest);
|
||||||
|
app.use('/rest', mosaicoTemplatesRest);
|
||||||
app.use('/rest', blacklistRest);
|
app.use('/rest', blacklistRest);
|
||||||
app.use('/rest', editorsRest);
|
app.use('/rest', editorsRest);
|
||||||
app.use('/rest', filesRest);
|
app.use('/rest', filesRest);
|
||||||
|
|
|
@ -61,7 +61,7 @@ function getMenus(t) {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
panelRender: props => <ReportsCUD action="create" />
|
panelRender: props => <ReportsCUD action="create" />
|
||||||
},
|
},
|
||||||
'templates': {
|
templates: {
|
||||||
title: t('Templates'),
|
title: t('Templates'),
|
||||||
link: '/reports/templates',
|
link: '/reports/templates',
|
||||||
panelComponent: ReportTemplatesList,
|
panelComponent: ReportTemplatesList,
|
||||||
|
|
|
@ -29,13 +29,22 @@ export default class List extends Component {
|
||||||
createTemplate: {
|
createTemplate: {
|
||||||
entityTypeId: 'namespace',
|
entityTypeId: 'namespace',
|
||||||
requiredOperations: ['createTemplate']
|
requiredOperations: ['createTemplate']
|
||||||
|
},
|
||||||
|
createMosaicoTemplate: {
|
||||||
|
entityTypeId: 'namespace',
|
||||||
|
requiredOperations: ['createMosaicoTemplate']
|
||||||
|
},
|
||||||
|
viewMosaicoTemplate: {
|
||||||
|
entityTypeId: 'mosaicoTemplate',
|
||||||
|
requiredOperations: ['view']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await axios.post('/rest/permissions-check', request);
|
const result = await axios.post('/rest/permissions-check', request);
|
||||||
|
|
||||||
this.setState({
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.state.createPermitted &&
|
<Toolbar>
|
||||||
<Toolbar>
|
{this.state.createPermitted &&
|
||||||
<NavButton linkTo="/templates/create" className="btn-primary" icon="plus" label={t('Create Template')}/>
|
<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')}/>
|
<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} />
|
<Table withHeader dataUrl="/rest/templates-table" columns={columns} />
|
||||||
</div>
|
</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 TemplatesList from './List';
|
||||||
import Share from '../shares/Share';
|
import Share from '../shares/Share';
|
||||||
import Files from "../lib/files";
|
import Files from "../lib/files";
|
||||||
|
import MosaicoCUD from './mosaico/CUD';
|
||||||
|
import MosaicoList from './mosaico/List';
|
||||||
|
|
||||||
|
|
||||||
function getMenus(t) {
|
function getMenus(t) {
|
||||||
|
@ -45,6 +47,45 @@ function getMenus(t) {
|
||||||
create: {
|
create: {
|
||||||
title: t('Create'),
|
title: t('Create'),
|
||||||
panelRender: props => <TemplatesCUD action="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} />
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,7 +204,7 @@ rootNamespaceRole="master"
|
||||||
[roles.namespace.master]
|
[roles.namespace.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "manageUsers"]
|
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "manageUsers"]
|
||||||
|
|
||||||
[roles.namespace.master.children]
|
[roles.namespace.master.children]
|
||||||
list=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
list=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
||||||
|
@ -213,7 +213,8 @@ campaign=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
template=["view", "edit", "delete", "share", "manageFiles"]
|
template=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "manageUsers"]
|
mosaicoTemplate=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
|
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "manageUsers"]
|
||||||
|
|
||||||
[roles.list.master]
|
[roles.list.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
|
@ -245,6 +246,10 @@ name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "execute"]
|
permissions=["view", "edit", "delete", "share", "execute"]
|
||||||
|
|
||||||
|
[roles.mosaicoTemplate.master]
|
||||||
|
name="Master"
|
||||||
|
description="All permissions"
|
||||||
|
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
||||||
|
|
||||||
|
|
||||||
[roles.global.editor]
|
[roles.global.editor]
|
||||||
|
@ -297,3 +302,7 @@ name="Editor"
|
||||||
description="XXX"
|
description="XXX"
|
||||||
permissions=[]
|
permissions=[]
|
||||||
|
|
||||||
|
[roles.mosaicoTemplate.editor]
|
||||||
|
name="Editor"
|
||||||
|
description="All permissions"
|
||||||
|
permissions=[]
|
||||||
|
|
|
@ -19,12 +19,14 @@ const entityTypes = {
|
||||||
campaign: {
|
campaign: {
|
||||||
entitiesTable: 'campaigns',
|
entitiesTable: 'campaigns',
|
||||||
sharesTable: 'shares_campaign',
|
sharesTable: 'shares_campaign',
|
||||||
permissionsTable: 'permissions_campaign'
|
permissionsTable: 'permissions_campaign',
|
||||||
|
filesTable: 'files_campaign'
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
entitiesTable: 'templates',
|
entitiesTable: 'templates',
|
||||||
sharesTable: 'shares_template',
|
sharesTable: 'shares_template',
|
||||||
permissionsTable: 'permissions_template'
|
permissionsTable: 'permissions_template',
|
||||||
|
filesTable: 'files_template'
|
||||||
},
|
},
|
||||||
report: {
|
report: {
|
||||||
entitiesTable: 'reports',
|
entitiesTable: 'reports',
|
||||||
|
@ -35,6 +37,12 @@ const entityTypes = {
|
||||||
entitiesTable: 'report_templates',
|
entitiesTable: 'report_templates',
|
||||||
sharesTable: 'shares_report_template',
|
sharesTable: 'shares_report_template',
|
||||||
permissionsTable: 'permissions_report_template'
|
permissionsTable: 'permissions_report_template'
|
||||||
|
},
|
||||||
|
mosaicoTemplate: {
|
||||||
|
entitiesTable: 'mosaico_templates',
|
||||||
|
sharesTable: 'shares_mosaico_template',
|
||||||
|
permissionsTable: 'permissions_mosaico_template',
|
||||||
|
filesTable: 'files_mosaico_template'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,21 +7,26 @@ const shares = require('./shares');
|
||||||
const fs = require('fs-extra-promise');
|
const fs = require('fs-extra-promise');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const interoperableErrors = require('../shared/interoperable-errors');
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
const permissions = require('../lib/permissions');
|
||||||
|
|
||||||
|
const entityTypes = permissions.getEntityTypes();
|
||||||
|
|
||||||
const filesDir = path.join(__dirname, '..', 'files');
|
const filesDir = path.join(__dirname, '..', 'files');
|
||||||
|
|
||||||
const permittedTypes = new Set(['template']);
|
function enforceTypePermitted(type) {
|
||||||
|
enforce(type in entityTypes && entityTypes[type].filesTable);
|
||||||
|
}
|
||||||
|
|
||||||
function getFilePath(type, entityId, filename) {
|
function getFilePath(type, entityId, filename) {
|
||||||
return path.join(path.join(filesDir, type, entityId.toString()), filename);
|
return path.join(path.join(filesDir, type, entityId.toString()), filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilesTable(type) {
|
function getFilesTable(type) {
|
||||||
return 'files_' + type;
|
return entityTypes[type].filesTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listDTAjax(context, type, entityId, params) {
|
async function listDTAjax(context, type, entityId, params) {
|
||||||
enforce(permittedTypes.has(type));
|
enforceTypePermitted(type);
|
||||||
await shares.enforceEntityPermission(context, type, entityId, 'manageFiles');
|
await shares.enforceEntityPermission(context, type, entityId, 'manageFiles');
|
||||||
return await dtHelpers.ajaxList(
|
return await dtHelpers.ajaxList(
|
||||||
params,
|
params,
|
||||||
|
@ -38,7 +43,7 @@ async function list(context, type, entityId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileById(context, type, id) {
|
async function getFileById(context, type, id) {
|
||||||
enforce(permittedTypes.has(type));
|
enforceTypePermitted(type);
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
const file = await tx(getFilesTable(type)).where('id', id).first();
|
const file = await tx(getFilesTable(type)).where('id', id).first();
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
||||||
|
@ -57,7 +62,7 @@ async function getFileById(context, type, id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileByFilename(context, type, entityId, name) {
|
async function getFileByFilename(context, type, entityId, name) {
|
||||||
enforce(permittedTypes.has(type));
|
enforceTypePermitted(type);
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
|
||||||
const file = await tx(getFilesTable(type)).where({entity: entityId, filename: name}).first();
|
const file = await tx(getFilesTable(type)).where({entity: entityId, filename: name}).first();
|
||||||
|
@ -76,7 +81,7 @@ async function getFileByFilename(context, type, entityId, name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFiles(context, type, entityId, files, dontReplace = false) {
|
async function createFiles(context, type, entityId, files, dontReplace = false) {
|
||||||
enforce(permittedTypes.has(type));
|
enforceTypePermitted(type);
|
||||||
if (files.length == 0) {
|
if (files.length == 0) {
|
||||||
// No files uploaded
|
// No files uploaded
|
||||||
return {uploaded: 0};
|
return {uploaded: 0};
|
||||||
|
|
110
models/mosaico-templates.js
Normal file
110
models/mosaico-templates.js
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const knex = require('../lib/knex');
|
||||||
|
const hasher = require('node-object-hash')();
|
||||||
|
const { enforce, filterObject } = require('../lib/helpers');
|
||||||
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||||
|
const shares = require('./shares');
|
||||||
|
|
||||||
|
const allowedKeys = new Set(['name', 'description', 'type', 'data', 'namespace']);
|
||||||
|
|
||||||
|
function hash(entity) {
|
||||||
|
return hasher.hash(filterObject(entity, allowedKeys));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getById(context, id) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'mosaicoTemplate', id, 'view');
|
||||||
|
const entity = await tx('mosaico_templates').where('id', id).first();
|
||||||
|
entity.data = JSON.parse(entity.data);
|
||||||
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'mosaicoTemplate', id);
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listDTAjax(context, params) {
|
||||||
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
|
context,
|
||||||
|
[{ entityTypeId: 'mosaicoTemplate', requiredOperations: ['view'] }],
|
||||||
|
params,
|
||||||
|
builder => builder.from('mosaico_templates').innerJoin('namespaces', 'namespaces.id', 'mosaico_templates.namespace'),
|
||||||
|
[ 'mosaico_templates.id', 'mosaico_templates.name', 'mosaico_templates.description', 'mosaico_templates.type', 'mosaico_templates.created', 'namespaces.name' ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _validateAndPreprocess(tx, entity) {
|
||||||
|
entity.data = JSON.stringify(entity.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(context, entity) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createMosaicoTemplate');
|
||||||
|
|
||||||
|
await _validateAndPreprocess(tx, entity);
|
||||||
|
|
||||||
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
|
|
||||||
|
const ids = await tx('mosaico_templates').insert(filterObject(entity, allowedKeys));
|
||||||
|
const id = ids[0];
|
||||||
|
|
||||||
|
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'mosaicoTemplate', entityId: id });
|
||||||
|
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWithConsistencyCheck(context, entity) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
await shares.enforceGlobalPermission(context, 'createJavascriptWithROAccess');
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'mosaicoTemplate', entity.id, 'edit');
|
||||||
|
|
||||||
|
const existing = await tx('mosaico_templates').where('id', entity.id).first();
|
||||||
|
if (!existing) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.data = JSON.parse(existing.data);
|
||||||
|
|
||||||
|
const existingHash = hash(existing);
|
||||||
|
if (existingHash !== entity.originalHash) {
|
||||||
|
throw new interoperableErrors.ChangedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _validateAndPreprocess(tx, entity);
|
||||||
|
|
||||||
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
|
await namespaceHelpers.validateMove(context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
|
||||||
|
|
||||||
|
await tx('mosaico_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||||
|
|
||||||
|
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'mosaicoTemplate', entityId: entity.id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(context, id) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
const rows = await tx('templates').where('type', 'mosaico').select(['data']);
|
||||||
|
for (const row of rows) {
|
||||||
|
const data = JSON.parse(row.data);
|
||||||
|
if (data.template === id) {
|
||||||
|
throw new interoperableErrors.DependencyPresentError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'mosaicoTemplate', id, 'delete');
|
||||||
|
|
||||||
|
await tx('mosaico_templates').where('id', id).del();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hash,
|
||||||
|
getById,
|
||||||
|
listDTAjax,
|
||||||
|
create,
|
||||||
|
updateWithConsistencyCheck,
|
||||||
|
remove
|
||||||
|
};
|
|
@ -36,7 +36,7 @@ async function listDTAjax(context, params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _validateAndPreprocess(tx, entity, isCreate) {
|
async function _validateAndPreprocess(tx, entity) {
|
||||||
entity.data = JSON.stringify(entity.data);
|
entity.data = JSON.stringify(entity.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ async function create(context, entity) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createTemplate');
|
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createTemplate');
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, entity, true);
|
await _validateAndPreprocess(tx, entity);
|
||||||
|
|
||||||
await namespaceHelpers.validateEntity(tx, entity);
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
||||||
throw new interoperableErrors.ChangedError();
|
throw new interoperableErrors.ChangedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, entity, false);
|
await _validateAndPreprocess(tx, entity);
|
||||||
|
|
||||||
await namespaceHelpers.validateEntity(tx, entity);
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');
|
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');
|
||||||
|
|
|
@ -123,6 +123,7 @@ router.getAsync('/img/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||||
} else {
|
} else {
|
||||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||||
height = sanitizeSize(height, 1, 2048, 300, true);
|
height = sanitizeSize(height, 1, 2048, 300, true);
|
||||||
|
// TODO - validate that one has the rights to read this ???
|
||||||
image = await resizedImage(req.query.src, method, width, height);
|
image = await resizedImage(req.query.src, method, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
36
routes/rest/mosaico-templates.js
Normal file
36
routes/rest/mosaico-templates.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const passport = require('../../lib/passport');
|
||||||
|
const mosaicoTemplates = require('../../models/mosaico-templates');
|
||||||
|
|
||||||
|
const router = require('../../lib/router-async').create();
|
||||||
|
|
||||||
|
|
||||||
|
router.getAsync('/mosaico-templates/:mosaicoTemplateId', passport.loggedIn, async (req, res) => {
|
||||||
|
const mosaicoTemplate = await mosaicoTemplates.getById(req.context, req.params.mosaicoTemplateId);
|
||||||
|
mosaicoTemplate.hash = mosaicoTemplates.hash(mosaicoTemplate);
|
||||||
|
return res.json(mosaicoTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/mosaico-templates', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
return res.json(await mosaicoTemplates.create(req.context, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.putAsync('/mosaico-templates/:mosaicoTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
const mosaicoTemplate = req.body;
|
||||||
|
mosaicoTemplate.id = parseInt(req.params.mosaicoTemplateId);
|
||||||
|
|
||||||
|
await mosaicoTemplates.updateWithConsistencyCheck(req.context, mosaicoTemplate);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.deleteAsync('/mosaico-templates/:mosaicoTemplateId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
await mosaicoTemplates.remove(req.context, req.params.mosaicoTemplateId);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/mosaico-templates-table', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await mosaicoTemplates.listDTAjax(req.context, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
await knex.schema.table('custom_fields', table => {
|
await knex.schema.table('custom_fields', table => {
|
||||||
table.json('settings');
|
table.text('settings');
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.table('custom_fields', table => {
|
await knex.schema.table('custom_fields', table => {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
await knex.schema.table('segments', table => {
|
await knex.schema.table('segments', table => {
|
||||||
table.json('settings');
|
table.text('settings');
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.table('segments', table => {
|
await knex.schema.table('segments', table => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
|
|
||||||
await knex.schema.createTable(`files_${entityType}`, table => {
|
await knex.schema.createTable(`files_${entityType}`, table => {
|
||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
table.integer('entity').unsigned().notNullable().references('templates.id');
|
table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`);
|
||||||
table.string('filename');
|
table.string('filename');
|
||||||
table.string('originalname');
|
table.string('originalname');
|
||||||
table.string('mimetype');
|
table.string('mimetype');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
await knex.schema.table('templates', table => {
|
await knex.schema.table('templates', table => {
|
||||||
table.json('data');
|
table.text('data', 'longtext');
|
||||||
table.string('type');
|
table.string('type');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
const mosaicoTemplates = require('../../../shared/mosaico-templates');
|
||||||
|
|
||||||
|
exports.up = (knex, Promise) => (async() => {
|
||||||
|
await knex.schema.createTable('mosaico_templates', table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('name');
|
||||||
|
table.text('description');
|
||||||
|
table.string('type');
|
||||||
|
table.text('data', 'longtext');
|
||||||
|
table.timestamp('created').defaultTo(knex.fn.now());
|
||||||
|
table.integer('namespace').unsigned().references('namespaces.id');
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTable(`shares_mosaico_template`, table => {
|
||||||
|
table.integer('entity').unsigned().notNullable().references(`mosaico_templates.id`).onDelete('CASCADE');
|
||||||
|
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||||
|
table.string('role', 128).notNullable();
|
||||||
|
table.boolean('auto').defaultTo(false);
|
||||||
|
table.primary(['entity', 'user']);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTable(`permissions_mosaico_template`, table => {
|
||||||
|
table.integer('entity').unsigned().notNullable().references(`mosaico_templates.id`).onDelete('CASCADE');
|
||||||
|
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||||
|
table.string('operation', 128).notNullable();
|
||||||
|
table.primary(['entity', 'user', 'operation']);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTable(`files_mosaico_template`, table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.integer('entity').unsigned().notNullable().references('mosaico_templates.id');
|
||||||
|
table.string('filename');
|
||||||
|
table.string('originalname');
|
||||||
|
table.string('mimetype');
|
||||||
|
table.string('encoding');
|
||||||
|
table.integer('size');
|
||||||
|
table.timestamp('created').defaultTo(knex.fn.now());
|
||||||
|
table.index(['entity', 'originalname'])
|
||||||
|
});
|
||||||
|
|
||||||
|
const versafix = {
|
||||||
|
name: 'Versafix One',
|
||||||
|
description: 'Default Mosaico Template',
|
||||||
|
type: 'html',
|
||||||
|
namespace: 1,
|
||||||
|
data: JSON.stringify({
|
||||||
|
html: mosaicoTemplates.versafix
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
await knex('mosaico_templates').insert(versafix);
|
||||||
|
})();
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) => (async() => {
|
||||||
|
await knex.schema
|
||||||
|
.dropTable('shares_mosaico_template')
|
||||||
|
.dropTable('permissions_mosaico_template')
|
||||||
|
.dropTable('files_mosaico_template')
|
||||||
|
.dropTable('mosaico_templates')
|
||||||
|
;
|
||||||
|
})();
|
|
@ -106,6 +106,13 @@ class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DependencyPresentError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('DependencyPresentError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const errorTypes = {
|
const errorTypes = {
|
||||||
InteroperableError,
|
InteroperableError,
|
||||||
NotLoggedInError,
|
NotLoggedInError,
|
||||||
|
@ -123,7 +130,8 @@ const errorTypes = {
|
||||||
PermissionDeniedError,
|
PermissionDeniedError,
|
||||||
InvalidConfirmationForSubscriptionError,
|
InvalidConfirmationForSubscriptionError,
|
||||||
InvalidConfirmationForAddressChangeError,
|
InvalidConfirmationForAddressChangeError,
|
||||||
InvalidConfirmationForUnsubscriptionError
|
InvalidConfirmationForUnsubscriptionError,
|
||||||
|
DependencyPresentError
|
||||||
};
|
};
|
||||||
|
|
||||||
function deserialize(errorObj) {
|
function deserialize(errorObj) {
|
||||||
|
|
1537
shared/mosaico-templates.js
Normal file
1537
shared/mosaico-templates.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue