Basic support for Mosaico-based email templates.
This commit is contained in:
parent
b5cdf57f72
commit
7b5642e911
38 changed files with 1271 additions and 751 deletions
BIN
client/public/mailtrain-notext.png
Normal file
BIN
client/public/mailtrain-notext.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -61,7 +61,7 @@ export default class Login extends Component {
|
|||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/login');
|
||||
|
||||
if (submitSuccessful) {
|
||||
const nextUrl = qs.parse(this.props.location.search).next || '/';
|
||||
const nextUrl = qs.parse(this.props.location.search).next || mailtrainConfig.urlBase;
|
||||
|
||||
/* This ensures we get config for the authenticated user */
|
||||
window.location = nextUrl;
|
||||
|
|
|
@ -25,7 +25,6 @@ import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birth
|
|||
import styles from "./styles.scss";
|
||||
import moment from "moment";
|
||||
|
||||
|
||||
const FormState = {
|
||||
Loading: 0,
|
||||
LoadingWithNotice: 1,
|
||||
|
|
22
client/src/lib/mosaico-sandbox-root.js
Normal file
22
client/src/lib/mosaico-sandbox-root.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
I18nextProvider,
|
||||
} from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import styles from "./mosaico.scss";
|
||||
import { MosaicoSandbox } from './mosaico';
|
||||
import { UntrustedContentRoot } from './untrusted';
|
||||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={ i18n }>
|
||||
<UntrustedContentRoot render={props => <MosaicoSandbox {...props} />} />
|
||||
</I18nextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
|
166
client/src/lib/mosaico.js
Normal file
166
client/src/lib/mosaico.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
I18nextProvider,
|
||||
translate
|
||||
} from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./mosaico.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
import { UntrustedContentHost } from './untrusted';
|
||||
import {
|
||||
Button,
|
||||
Icon
|
||||
} from "./bootstrap-components";
|
||||
|
||||
export const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
export class MosaicoEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fullscreen: false
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entityTypeId: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
title: PropTypes.string,
|
||||
onFullscreenAsync: PropTypes.func
|
||||
}
|
||||
|
||||
async toggleFullscreenAsync() {
|
||||
const fullscreen = !this.state.fullscreen;
|
||||
this.setState({
|
||||
fullscreen
|
||||
});
|
||||
await this.props.onFullscreenAsync(fullscreen);
|
||||
}
|
||||
|
||||
async exportState() {
|
||||
return await this.contentNode.ask('exportState');
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const mosaicoData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id,
|
||||
model: this.props.entity.data.model,
|
||||
metadata: this.props.entity.data.metadata
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
|
||||
<div className={styles.navbar}>
|
||||
{this.state.fullscreen && <img className={styles.logo} src="/public/mailtrain-notext.png"/>}
|
||||
<div className={styles.title}>{this.props.title}</div>
|
||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||
</div>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} contentProps={mosaicoData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={mosaicoData}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MosaicoEditor.prototype.exportState = async function() {
|
||||
return await this.getWrappedInstance().exportState();
|
||||
};
|
||||
|
||||
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
export class MosaicoSandbox extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.viewModel = null;
|
||||
this.state = {
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entityTypeId: PropTypes.string,
|
||||
entityId: PropTypes.number,
|
||||
model: PropTypes.object,
|
||||
metadata: PropTypes.object
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const publicPath = '/public/mosaico';
|
||||
|
||||
if (!Mosaico.isCompatible()) {
|
||||
alert('Update your browser!');
|
||||
return;
|
||||
}
|
||||
|
||||
const plugins = [...window.mosaicoPlugins];
|
||||
|
||||
plugins.push(viewModel => {
|
||||
this.viewModel = viewModel;
|
||||
});
|
||||
|
||||
// (Custom) HTML postRenderers
|
||||
plugins.push(viewModel => {
|
||||
viewModel.originalExportHTML = viewModel.exportHTML;
|
||||
viewModel.exportHTML = () => {
|
||||
let html = viewModel.originalExportHTML();
|
||||
for (const portRender of window.mosaicoHTMLPostRenderers) {
|
||||
html = postRender(html);
|
||||
}
|
||||
return html;
|
||||
};
|
||||
});
|
||||
|
||||
plugins.unshift(vm => {
|
||||
// This is an override of the default paths in Mosaico
|
||||
vm.logoPath = publicPath + '/img/mosaico32.png';
|
||||
vm.logoUrl = '#';
|
||||
});
|
||||
|
||||
const config = {
|
||||
imgProcessorBackend: `/mosaico/img/${this.props.entityTypeId}/${this.props.entityId}`,
|
||||
emailProcessorBackend: '/mosaico/dl/',
|
||||
fileuploadConfig: {
|
||||
url: `/mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`
|
||||
},
|
||||
strings: window.mosaicoLanguageStrings
|
||||
};
|
||||
|
||||
const metadata = this.props.metadata;
|
||||
const model = this.props.model;
|
||||
const template = publicPath + '/templates/versafix-1/index.html';
|
||||
|
||||
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
||||
|
||||
Mosaico.start(config, template, metadata, model, allPlugins);
|
||||
}
|
||||
|
||||
async onMethodAsync(method, params) {
|
||||
if (method === 'exportState') {
|
||||
return {
|
||||
html: this.viewModel.exportHTML(),
|
||||
model: this.viewModel.exportJS(),
|
||||
metadata: this.viewModel.metadata
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
|
||||
MosaicoSandbox.prototype.onMethodAsync = async function(method, params) {
|
||||
return await this.getWrappedInstance().onMethodAsync(method, params);
|
||||
};
|
75
client/src/lib/mosaico.scss
Normal file
75
client/src/lib/mosaico.scss
Normal file
|
@ -0,0 +1,75 @@
|
|||
$navbarHeight: 34px;
|
||||
|
||||
.editor {
|
||||
.host {
|
||||
height: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
.editorFullscreen {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
margin-top: $navbarHeight;
|
||||
|
||||
.navbar {
|
||||
margin-top: -$navbarHeight;
|
||||
}
|
||||
|
||||
.host {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:global .mo-standalone {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #DE4320;
|
||||
width: 100%;
|
||||
height: $navbarHeight;
|
||||
}
|
||||
|
||||
.logo {
|
||||
float: left;
|
||||
height: $navbarHeight;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 5px 0 5px 10px;
|
||||
font-size: 18px;
|
||||
font-family: sans-serif;
|
||||
font-family: "Ubuntu",Tahoma,"Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
color: white;
|
||||
height: $navbarHeight;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
float: right;
|
||||
padding: 0px 15px;
|
||||
line-height: $navbarHeight;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #b1381e;
|
||||
color: white;
|
||||
}
|
|
@ -319,7 +319,6 @@ class SectionContent extends Component {
|
|||
|
||||
ensureAuthenticated() {
|
||||
if (!mailtrainConfig.isAuthenticated) {
|
||||
/* FIXME, once we turn Mailtrain to single-page application, this should become navigateTo */
|
||||
this.navigateTo('/account/login?next=' + encodeURIComponent(window.location.pathname));
|
||||
}
|
||||
}
|
||||
|
@ -383,7 +382,7 @@ class Section extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Router>
|
||||
<Router basename={mailtrainConfig.urlBase}>
|
||||
<SectionContent root={this.props.root} structure={this.structure} />
|
||||
</Router>
|
||||
);
|
||||
|
|
|
@ -100,3 +100,15 @@
|
|||
font-size: 20px;
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
|
||||
.untrustedContent {
|
||||
border: 0px none;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.withElementInFullscreen {
|
||||
height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -13,7 +13,7 @@ import 'datatables.net-bs/css/dataTables.bootstrap.css';
|
|||
|
||||
import axios from './axios';
|
||||
|
||||
import { withPageHelpers } from '../lib/page'
|
||||
import { withPageHelpers } from './page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import styles from "./styles.scss";
|
||||
|
||||
|
@ -394,7 +394,7 @@ class Table extends Component {
|
|||
The reference to the table can be obtained by ref.
|
||||
*/
|
||||
Table.prototype.refresh = function() {
|
||||
this.getWrappedInstance().refresh()
|
||||
this.getWrappedInstance().refresh();
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
|
@ -12,7 +12,7 @@ import '../../vendor/fancytree/skin-bootstrap/ui.fancytree.min.css';
|
|||
import './tree.css';
|
||||
import axios from './axios';
|
||||
|
||||
import { withPageHelpers } from '../lib/page'
|
||||
import { withPageHelpers } from './page'
|
||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import styles from "./styles.scss";
|
||||
|
||||
|
|
227
client/src/lib/untrusted.js
Normal file
227
client/src/lib/untrusted.js
Normal file
|
@ -0,0 +1,227 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {translate} from "react-i18next";
|
||||
import {requiresAuthenticatedUser, withPageHelpers} from "./page";
|
||||
import {withAsyncErrorHandler, withErrorHandling} from "./error-handling";
|
||||
import axios from "./axios";
|
||||
import styles from "./styles.scss";
|
||||
import {getTrustedUrl, getSandboxUrl} from "./urls";
|
||||
import {Table} from "./table";
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export class UntrustedContentHost extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.refreshAccessTokenTimeout = null;
|
||||
this.accessToken = null;
|
||||
this.contentNodeIsLoaded = false;
|
||||
|
||||
this.state = {
|
||||
hasAccessToken: false,
|
||||
};
|
||||
|
||||
this.receiveMessageHandler = ::this.receiveMessage;
|
||||
|
||||
this.rpcCounter = 0;
|
||||
this.rpcResolves = new Map();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
contentSrc: PropTypes.string,
|
||||
contentProps: PropTypes.object,
|
||||
tokenMethod: PropTypes.string,
|
||||
tokenParams: PropTypes.object,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
isInitialized() {
|
||||
return !!this.accessToken;
|
||||
}
|
||||
|
||||
receiveMessage(evt) {
|
||||
const msg = evt.data;
|
||||
console.log(msg);
|
||||
|
||||
if (msg.type === 'initNeeded') {
|
||||
if (this.isInitialized()) {
|
||||
this.sendMessage('init', {
|
||||
accessToken: this.accessToken,
|
||||
contentProps: this.props.contentProps
|
||||
});
|
||||
}
|
||||
} else if (msg.type === 'rpcResponse') {
|
||||
const resolve = this.rpcResolves.get(msg.data.msgId);
|
||||
resolve(msg.data.ret);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(type, data) {
|
||||
if (this.contentNodeIsLoaded) { // This is to avoid errors "common.js:45744 Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://localhost:8081') does not match the recipient window's origin ('http://localhost:3000')"
|
||||
this.contentNode.contentWindow.postMessage({type, data}, getSandboxUrl(''));
|
||||
}
|
||||
}
|
||||
|
||||
async ask(method, params) {
|
||||
if (this.contentNodeIsLoaded) {
|
||||
this.rpcCounter += 1;
|
||||
const msgId = this.rpcCounter;
|
||||
|
||||
this.sendMessage('rpcRequest', {
|
||||
method,
|
||||
params,
|
||||
msgId
|
||||
});
|
||||
|
||||
return await (new Promise((resolve, reject) => {
|
||||
this.rpcResolves.set(msgId, resolve);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async refreshAccessToken() {
|
||||
const result = await axios.post(getTrustedUrl('rest/restricted-access-token'), {
|
||||
method: this.props.tokenMethod,
|
||||
params: this.props.tokenParams
|
||||
});
|
||||
|
||||
this.accessToken = result.data;
|
||||
|
||||
if (!this.state.hasAccessToken) {
|
||||
this.setState({
|
||||
hasAccessToken: true
|
||||
})
|
||||
}
|
||||
|
||||
this.sendMessage('accessToken', this.accessToken);
|
||||
}
|
||||
|
||||
scheduleRefreshAccessToken() {
|
||||
this.refreshAccessTokenTimeout = setTimeout(() => {
|
||||
this.refreshAccessToken();
|
||||
this.scheduleRefreshAccessToken();
|
||||
}, 60 * 1000);
|
||||
}
|
||||
|
||||
handleUpdate() {
|
||||
if (this.isInitialized()) {
|
||||
this.sendMessage('initAvailable');
|
||||
}
|
||||
|
||||
if (!this.state.hasAccessToken) {
|
||||
this.refreshAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.scheduleRefreshAccessToken();
|
||||
window.addEventListener('message', this.receiveMessageHandler, false);
|
||||
|
||||
this.handleUpdate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.refreshAccessTokenTimeout);
|
||||
window.removeEventListener('message', this.receiveMessageHandler, false);
|
||||
}
|
||||
|
||||
contentNodeLoaded() {
|
||||
this.contentNodeIsLoaded = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<iframe className={styles.untrustedContent + ' ' + this.props.className} ref={node => this.contentNode = node} src={getSandboxUrl(this.props.contentSrc)} onLoad={::this.contentNodeLoaded}> </iframe>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UntrustedContentHost.prototype.ask = async function(method, params) {
|
||||
return await this.getWrappedInstance().ask(method, params);
|
||||
};
|
||||
|
||||
|
||||
@translate()
|
||||
export class UntrustedContentRoot extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
initialized: false,
|
||||
};
|
||||
|
||||
this.receiveMessageHandler = ::this.receiveMessage;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
render: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
setAccessTokenCookie(token) {
|
||||
document.cookie = 'restricted_access_token=' + token + '; expires=' + (new Date(Date.now()+60000)).toUTCString();
|
||||
console.log(document.cookie);
|
||||
}
|
||||
|
||||
async receiveMessage(evt) {
|
||||
const msg = evt.data;
|
||||
console.log(msg);
|
||||
|
||||
if (msg.type === 'initAvailable' && !this.state.initialized) {
|
||||
this.sendMessage('initNeeded');
|
||||
|
||||
} else if (msg.type === 'init' && !this.state.initialized) {
|
||||
this.setAccessTokenCookie(msg.data.accessToken);
|
||||
this.setState({
|
||||
initialized: true,
|
||||
contentProps: msg.data.contentProps
|
||||
});
|
||||
|
||||
} else if (msg.type === 'accessToken') {
|
||||
this.setAccessTokenCookie(msg.data);
|
||||
} else if (msg.type === 'rpcRequest') {
|
||||
const ret = await this.contentNode.onMethodAsync(msg.data.method, msg.data.params);
|
||||
this.sendMessage('rpcResponse', {msgId: msg.data.msgId, ret});
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(type, data) {
|
||||
window.parent.postMessage({type, data}, getTrustedUrl(''));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.receiveMessageHandler, false);
|
||||
this.sendMessage('initNeeded');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.receiveMessageHandler, false);
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const props = {
|
||||
...this.state.contentProps,
|
||||
ref: node => this.contentNode = node
|
||||
};
|
||||
|
||||
if (this.state.initialized) {
|
||||
return this.props.render(props);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
{t('Loading...')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
37
client/src/lib/urls.js
Normal file
37
client/src/lib/urls.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
'use strict';
|
||||
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
|
||||
let urlBase;
|
||||
let sandboxUrlBase;
|
||||
|
||||
if (mailtrainConfig.urlBase.startsWith('/')) {
|
||||
urlBase = window.location.protocol + '//' + window.location.hostname + ':' + mailtrainConfig.port + mailtrainConfig.urlBase;
|
||||
} else {
|
||||
urlBase = mailtrainConfig.urlBase
|
||||
}
|
||||
|
||||
if (mailtrainConfig.sandboxUrlBase) {
|
||||
if (mailtrainConfig.urlBase.startsWith('/')) {
|
||||
sandboxUrlBase = window.location.protocol + '//' + window.location.hostname + ':' + mailtrainConfig.sandboxPort + mailtrainConfig.sandboxUrlBase;
|
||||
} else {
|
||||
sandboxUrlBase = mailtrainConfig.sandboxUrlBase
|
||||
}
|
||||
} else {
|
||||
const loc = document.createElement("a");
|
||||
loc.href = urlBase;
|
||||
sandboxUrlBase = loc.protocol + '//' + loc.hostname + ':' + mailtrainConfig.sandboxPort + loc.pathname;
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return urlBase + path;
|
||||
}
|
||||
|
||||
function getSandboxUrl(path) {
|
||||
return sandboxUrlBase + path;
|
||||
}
|
||||
|
||||
export {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl
|
||||
}
|
|
@ -1,12 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
import qs from 'querystringify';
|
||||
|
||||
import { Section } from '../lib/page';
|
||||
import ListsList from './List';
|
||||
import ListsCUD from './CUD';
|
||||
import FormsList from './forms/List';
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
I18nextProvider,
|
||||
translate
|
||||
} from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./styles.scss";
|
||||
|
||||
const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
@translate()
|
||||
class MosaicoEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.viewModel = null;
|
||||
this.state = {
|
||||
entityTypeId: ResourceType.TEMPLATE, // FIXME
|
||||
entityId: 13 // FIXME
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
//structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||
}
|
||||
|
||||
async onClose(evt) {
|
||||
const t = this.props.t;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (confirm(t('Unsaved changes will be lost. Close now?'))) {
|
||||
window.location.href = `/${this.state.entityTypeId}s/${this.state.entityId}/edit`;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const publicPath = '/public/mosaico';
|
||||
|
||||
if (!Mosaico.isCompatible()) {
|
||||
alert('Update your browser!');
|
||||
return;
|
||||
}
|
||||
|
||||
const plugins = window.mosaicoPlugins;
|
||||
|
||||
plugins.push(viewModel => {
|
||||
this.viewModel = viewModel;
|
||||
});
|
||||
|
||||
// (Custom) HTML postRenderers
|
||||
plugins.push(viewModel => {
|
||||
viewModel.originalExportHTML = viewModel.exportHTML;
|
||||
viewModel.exportHTML = () => {
|
||||
let html = viewModel.originalExportHTML();
|
||||
for (const portRender of window.mosaicoHTMLPostRenderers) {
|
||||
html = postRender(html);
|
||||
}
|
||||
return html;
|
||||
};
|
||||
});
|
||||
|
||||
plugins.unshift(vm => {
|
||||
// This is a fix for the use of hardcoded path in Mosaico
|
||||
vm.logoPath = publicPath + '/img/mosaico32.png'
|
||||
});
|
||||
|
||||
const config = {
|
||||
imgProcessorBackend: `/mosaico/img/${this.state.entityTypeId}/${this.state.entityId}`,
|
||||
emailProcessorBackend: '/mosaico/dl/',
|
||||
titleToken: "MOSAICO Responsive Email Designer",
|
||||
fileuploadConfig: {
|
||||
url: `/mosaico/upload/${this.state.entityTypeId}/${this.state.entityId}`
|
||||
},
|
||||
strings: window.mosaicoLanguageStrings
|
||||
};
|
||||
|
||||
const metadata = undefined;
|
||||
const model = undefined;
|
||||
const template = publicPath + '/templates/versafix-1/index.html';
|
||||
|
||||
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
||||
|
||||
Mosaico.start(config, template, metadata, model, allPlugins);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div className={styles.navbar}>
|
||||
<img className={styles.logo} src="/public/mailtrain-header.png"/>
|
||||
<a className={styles.btn} onClick={::this.onClose}>{t('CLOSE')}</a>
|
||||
<a className={styles.btn}><span></span></a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default function() {
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n={ i18n }><MosaicoEditor /></I18nextProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
:global .mo-standalone {
|
||||
top: 34px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #DE4320;
|
||||
overflow: hidden;
|
||||
height: 34px;
|
||||
top: -34px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 24px;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 150px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #972E15;
|
||||
}
|
|
@ -1,11 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page';
|
||||
import CUD from './CUD';
|
||||
import List from './List';
|
||||
import Share from '../shares/Share';
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page';
|
||||
import ReportsCUD from './CUD';
|
||||
import ReportsList from './List';
|
||||
import ReportsView from './View';
|
||||
|
@ -13,7 +8,7 @@ import ReportsOutput from './Output';
|
|||
import ReportTemplatesCUD from './templates/CUD';
|
||||
import ReportTemplatesList from './templates/List';
|
||||
import Share from '../shares/Share';
|
||||
import { ReportState } from '../../../shared/reports';
|
||||
import {ReportState} from '../../../shared/reports';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
|
||||
|
|
|
@ -139,7 +139,7 @@ class Root extends Component {
|
|||
|
||||
async logout() {
|
||||
await axios.post('/rest/logout');
|
||||
window.location = '/';
|
||||
window.location = mailtrainConfig.urlBase;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -39,7 +39,8 @@ export default class CUD extends Component {
|
|||
this.templateTypes = getTemplateTypes(props.t);
|
||||
|
||||
this.state = {
|
||||
showMergeTagReference: false
|
||||
showMergeTagReference: false,
|
||||
elementInFullscreen: false
|
||||
};
|
||||
|
||||
this.initForm();
|
||||
|
@ -66,7 +67,8 @@ export default class CUD extends Component {
|
|||
namespace: mailtrainConfig.user.namespace,
|
||||
type: mailtrainConfig.editors[0],
|
||||
text: '',
|
||||
html: ''
|
||||
html: '',
|
||||
data: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +94,11 @@ export default class CUD extends Component {
|
|||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (this.props.entity) {
|
||||
const typeKey = this.getFormValue('type');
|
||||
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
|
||||
}
|
||||
|
||||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
|
@ -120,6 +127,9 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
async extractPlainText() {
|
||||
const typeKey = this.getFormValue('type');
|
||||
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
|
||||
|
||||
const html = this.getFormValue('html');
|
||||
if (!html) {
|
||||
alert('Missing HTML content');
|
||||
|
@ -145,6 +155,12 @@ export default class CUD extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
async setElementInFullscreen(elementInFullscreen) {
|
||||
this.setState({
|
||||
elementInFullscreen
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
@ -241,7 +257,7 @@ export default class CUD extends Component {
|
|||
</div>}
|
||||
</AlignedRow>
|
||||
|
||||
{this.templateTypes[typeKey].form}
|
||||
{this.templateTypes[typeKey].getHTMLEditor(this)}
|
||||
|
||||
<ACEEditor id="text" height="400px" mode="text" label={t('Template content (plain text)')} help={<Trans>To extract the text from HTML click <ActionLink onClickAsync={::this.extractPlainText}>here</ActionLink>. Please note that your existing plaintext in the field above will be overwritten. This feature uses the <a href="http://premailer.dialect.ca/api">Premailer API</a>, a third party service. Their Terms of Service and Privacy Policy apply.</Trans>}/>
|
||||
</div>
|
||||
|
@ -249,7 +265,7 @@ export default class CUD extends Component {
|
|||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
|
|
|
@ -3,17 +3,27 @@
|
|||
import React from "react";
|
||||
import {
|
||||
ACEEditor,
|
||||
AlignedRow,
|
||||
CKEditor
|
||||
} from "../lib/form";
|
||||
import 'brace/mode/text';
|
||||
import 'brace/mode/html'
|
||||
|
||||
import {MosaicoEditor, ResourceType} from "../lib/mosaico";
|
||||
|
||||
|
||||
export function getTemplateTypes(t) {
|
||||
|
||||
const templateTypes = {};
|
||||
|
||||
templateTypes.mosaico = {
|
||||
typeName: t('Mosaico')
|
||||
typeName: t('Mosaico'),
|
||||
getHTMLEditor: owner => <AlignedRow label={t('Template content (HTML)')}><MosaicoEditor ref={node => owner.editorNode = node} entity={owner.props.entity} entityTypeId={ResourceType.TEMPLATE} title={t('Mosaico Template Designer')} onFullscreenAsync={::owner.setElementInFullscreen}/></AlignedRow>,
|
||||
htmlEditorBeforeSave: async owner => {
|
||||
const {html, metadata, model} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue('html', html);
|
||||
owner.updateFormValue('data', {metadata, model});
|
||||
}
|
||||
};
|
||||
|
||||
templateTypes.grapejs = {
|
||||
|
@ -22,12 +32,14 @@ export function getTemplateTypes(t) {
|
|||
|
||||
templateTypes.ckeditor = {
|
||||
typeName: t('CKEditor'),
|
||||
form: <CKEditor id="html" height="600px" label={t('Template content (HTML)')}/>
|
||||
getHTMLEditor: owner => <CKEditor id="html" height="600px" label={t('Template content (HTML)')}/>,
|
||||
htmlEditorBeforeSave: async owner => {}
|
||||
};
|
||||
|
||||
templateTypes.codeeditor = {
|
||||
typeName: t('Code Editor'),
|
||||
form: <ACEEditor id="html" height="600px" mode="html" label={t('Template content (HTML)')}/>
|
||||
getHTMLEditor: owner => <ACEEditor id="html" height="600px" mode="html" label={t('Template content (HTML)')}/>,
|
||||
htmlEditorBeforeSave: async owner => {}
|
||||
};
|
||||
|
||||
templateTypes.mjml = {
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page';
|
||||
import TemplatesCUD from './CUD';
|
||||
import TemplatesList from './List';
|
||||
import Share from '../shares/Share';
|
||||
|
|
|
@ -4,7 +4,7 @@ const path = require('path');
|
|||
module.exports = {
|
||||
entry: {
|
||||
root: ['babel-polyfill', './src/root.js'],
|
||||
mosaico: ['babel-polyfill', './src/mosaico/root.js'],
|
||||
mosaico: ['babel-polyfill', './src/lib/mosaico-sandbox-root.js'],
|
||||
},
|
||||
output: {
|
||||
library: 'MailtrainReactBody',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue