Some fixes and optimizations in sandboxes.

Start of a sandbox for GrapeJS
This commit is contained in:
Tomas Bures 2018-11-06 13:30:50 +01:00
parent 02a7275ae4
commit e2093e22fe
22 changed files with 742 additions and 294 deletions

View file

@ -24,6 +24,7 @@ const reports = require('./routes/reports');
const subscription = require('./routes/subscription');
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
const sandboxedGrapesJS = require('./routes/sandboxed-grapesjs');
const files = require('./routes/files');
const links = require('./routes/links');
const archive = require('./routes/archive');
@ -224,6 +225,7 @@ function createApp(appType) {
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
useWith404Fallback('/grapesjs', sandboxedGrapesJS.getRouter(appType));
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
if (config.reports && config.reports.enabled === true) {

View file

@ -3837,6 +3837,16 @@
"ua-parser-js": "0.7.17"
}
},
"file-loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-2.0.0.tgz",
"integrity": "sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ==",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
"schema-utils": "1.0.0"
}
},
"filename-regex": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",

View file

@ -71,6 +71,7 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-1": "^6.24.1",
"css-loader": "^0.28.4",
"file-loader": "^2.0.0",
"i18next-conv": "^3.0.3",
"node-sass": "^4.5.3",
"postcss-loader": "^3.0.0",

View file

@ -1,48 +0,0 @@
'use strict';
import React, {Component} from 'react';
import 'grapesjs/dist/css/grapes.min.css';
import grapesjs from 'grapesjs';
import grapesjsMjml from 'grapesjs-mjml';
export default class GrapesJs extends Component {
constructor(props) {
super(props);
this.state = {
};
}
componentDidMount() {
const editor = grapesjs.init({
container: this.canvasNode,
height: '100%',
width: 'auto',
noticeOnUnload: 0,
storageManager:{autoload: 0},
fromElement: false,
components:
' <mj-container>\n' +
' <mj-section>\n' +
' <mj-column>\n' +
' <mj-text>My Company</mj-text>\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' <mj-container>',
plugins: ['gjs-mjml'],
pluginsOpts: {
'gjs-mjml': {}
}
});
}
render() {
return (
<div>
<div ref={node => this.canvasNode = node}/>
</div>
);
}
}

View file

@ -1,5 +0,0 @@
.gjs-cv-canvas {
top: 0;
width: 100%;
height: 100%;
}

View file

@ -2,4 +2,4 @@
import {getUrl} from "./urls";
__webpack_public_path__ = getUrl();
__webpack_public_path__ = getUrl('mailtrain/');

View file

@ -2,12 +2,133 @@
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import {I18nextProvider,} from 'react-i18next';
import i18n from './i18n';
import {CKEditorSandbox} from './sandboxed-ckeditor';
import {UntrustedContentRoot, parentRPC} from './untrusted';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {
parentRPC,
UntrustedContentRoot
} from './untrusted';
import PropTypes
from "prop-types";
import styles
from "./sandboxed-ckeditor.scss";
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
import CKEditor
from "react-ckeditor-component";
import { initialHeight } from "./sandboxed-ckeditor-shared";
@translate(null, { withRef: true })
class CKEditorSandbox extends Component {
constructor(props) {
super(props);
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
const html = this.props.initialHtml && base(this.props.initialHtml, trustedUrlBase, sandboxUrlBase, publicUrlBase);
this.state = {
html
};
}
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
initialHtml: PropTypes.string
}
async exportState(method, params) {
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
return {
html: unbase(this.state.html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
};
}
async setHeight(methods, params) {
this.node.editorInstance.resize('100%', params);
}
componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
parentRPC.setMethodHandler('setHeight', ::this.setHeight);
}
render() {
const config = {
toolbarGroups: [
{
name: "document",
groups: ["document", "doctools"]
},
{
name: "clipboard",
groups: ["clipboard", "undo"]
},
{name: "styles"},
{
name: "basicstyles",
groups: ["basicstyles", "cleanup"]
},
{
name: "editing",
groups: ["find", "selection", "spellchecker"]
},
{name: "forms"},
{
name: "paragraph",
groups: ["list",
"indent", "blocks", "align", "bidi"]
},
{name: "links"},
{name: "insert"},
{name: "colors"},
{name: "tools"},
{name: "others"},
{
name: "document-mode",
groups: ["mode"]
}
],
removeButtons: 'Underline,Subscript,Superscript,Maximize',
resize_enabled: false,
height: initialHeight
};
return (
<div className={styles.sandbox}>
<CKEditor ref={node => this.node = node}
content={this.state.html}
events={{
change: evt => this.setState({html: evt.editor.getData()}),
}}
config={config}
/>
</div>
);
}
}
export default function() {
parentRPC.init();

View file

@ -0,0 +1,3 @@
'use strict';
export const initialHeight = 600;

View file

@ -2,23 +2,16 @@
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import PropTypes from "prop-types";
import styles from "./sandboxed-ckeditor.scss";
import PropTypes
from "prop-types";
import styles
from "./sandboxed-ckeditor.scss";
import {UntrustedContentHost, parentRPC} from './untrusted';
import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
import CKEditor from "react-ckeditor-component";
import {getTrustedUrl} from "./urls";
const initialHeight = 600;
import { initialHeight } from "./sandboxed-ckeditor-shared";
const navbarHeight = 34; // Sync this with navbarheight in sandboxed-ckeditor.scss
@translate(null, { withRef: true })
@ -108,62 +101,3 @@ CKEditorHost.prototype.exportState = async function() {
};
@translate(null, { withRef: true })
export class CKEditorSandbox extends Component {
constructor(props) {
super(props);
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
const html = this.props.initialHtml && base(this.props.initialHtml, trustedUrlBase, sandboxUrlBase, publicUrlBase);
this.state = {
html
};
}
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
initialHtml: PropTypes.string
}
async exportState(method, params) {
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
return {
html: unbase(this.state.html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
};
}
async setHeight(methods, params) {
this.node.editorInstance.resize('100%', params);
}
componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
parentRPC.setMethodHandler('setHeight', ::this.setHeight);
}
render() {
const config = {
removeButtons: 'Underline,Subscript,Superscript,Maximize',
resize_enabled: false,
height: initialHeight
};
return (
<div className={styles.sandbox}>
<CKEditor ref={node => this.node = node}
content={this.state.html}
events={{
change: evt => this.setState({html: evt.editor.getData()}),
}}
config={config}
/>
</div>
);
}
}

View file

@ -0,0 +1,157 @@
'use strict';
import './public-path';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {
parentRPC,
UntrustedContentRoot
} from './untrusted';
import PropTypes
from "prop-types";
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
import 'grapesjs/dist/css/grapes.min.css';
import grapesjs from 'grapesjs';
import "grapesjs-mjml";
import "./sandboxed-grapesjs.scss";
grapesjs.plugins.add('mailtrain', (editor, opts = {}) => {
const panelManager = editor.Panels;
panelManager.removeButton('options','fullscreen')
});
@translate(null, { withRef: true })
export class GrapesJSSandbox extends Component {
constructor(props) {
super(props);
this.state = {
};
}
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
initialModel: PropTypes.object
}
async exportState(method, params) {
const editor = this.editor;
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
let html;
html = unbase(editor.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true);
const model = {
css: editor.getCss(),
source: editor.getHtml(),
};
console.log(model.css);
console.log(model.source);
return {
html,
model
};
}
componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
const model = this.props.initialModel || {}
const source = model.source && base(model.source, trustedUrlBase, sandboxUrlBase, publicUrlBase);
const css = model.css && base(model.css, trustedUrlBase, sandboxUrlBase, publicUrlBase);
/*
' <mj-container>\n' +
' <mj-section>\n' +
' <mj-column>\n' +
' <mj-text>My Company</mj-text>\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' <mj-container>',
*/
this.editor = grapesjs.init({
container: this.canvasNode,
height: '100%',
width: '100%',
storageManager:{
type: 'none'
},
assetManager: {
assets: [],
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}',
uploadText: 'Drop images here or click to upload',
headers: {
'X-CSRF-TOKEN': '{{csrfToken}}',
},
},
styleManager: {
clearProperties: true,
},
fromElement: false,
components: source,
style: css,
plugins: [
'mailtrain',
'gjs-mjml'
],
pluginsOpts: {
'gjs-mjml': {}
}
});
}
render() {
return (
<div>
<div ref={node => this.canvasNode = node}/>
</div>
);
}
}
export default function() {
parentRPC.init();
ReactDOM.render(
<I18nextProvider i18n={ i18n }>
<UntrustedContentRoot render={props => <GrapesJSSandbox {...props} />} />
</I18nextProvider>,
document.getElementById('root')
);
};

View file

@ -0,0 +1,73 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import PropTypes
from "prop-types";
import styles
from "./sandboxed-grapesjs.scss";
import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {getTrustedUrl} from "./urls";
@translate(null, { withRef: true })
export class GrapesJSHost extends Component {
constructor(props) {
super(props);
this.state = {
fullscreen: false
}
}
static propTypes = {
entityTypeId: PropTypes.string,
entity: PropTypes.object,
initialModel: 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 editorData = {
entityTypeId: this.props.entityTypeId,
entityId: this.props.entity.id,
initialModel: this.props.initialModel
};
const tokenData = {
entityTypeId: this.props.entityTypeId,
entityId: this.props.entity.id
};
return (
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
<div className={styles.navbar}>
{this.state.fullscreen && <img className={styles.logo} src={getTrustedUrl('static/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} singleToken={true} contentProps={editorData} contentSrc="grapesjs/editor" tokenMethod="grapesjs" tokenParams={tokenData}/>
</div>
);
}
}
GrapesJSHost.prototype.exportState = async function() {
return await this.getWrappedInstance().exportState();
};

View file

@ -0,0 +1,86 @@
$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%;
}
}
.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;
}
:global .grapesjs-body {
margin: 0px;
}
:global .gjs-editor-cont {
position: absolute;
}
:global .gjs-devices-c .gjs-devices {
padding-right: 15px;
}
:global .gjs-pn-devices-c, :global .gjs-pn-views {
padding: 4px;
}

View file

@ -2,12 +2,120 @@
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import {I18nextProvider,} from 'react-i18next';
import i18n from './i18n';
import {MosaicoSandbox} from './sandboxed-mosaico';
import {UntrustedContentRoot, parentRPC} from './untrusted';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {
parentRPC,
UntrustedContentRoot
} from './untrusted';
import PropTypes
from "prop-types";
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
@translate(null, { withRef: true })
class MosaicoSandbox extends Component {
constructor(props) {
super(props);
this.viewModel = null;
this.state = {
};
}
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
templateId: PropTypes.number,
templatePath: PropTypes.string,
initialModel: PropTypes.string,
initialMetadata: PropTypes.string
}
async exportState(method, params) {
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
return {
html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase, publicUrlBase),
metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase, publicUrlBase)
};
}
componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
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 = getTrustedUrl('static/mosaico/img/mosaico32.png');
vm.logoUrl = '#';
});
const config = {
imgProcessorBackend: getTrustedUrl('mosaico/img'),
emailProcessorBackend: getSandboxUrl('mosaico/dl'),
fileuploadConfig: {
url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
},
strings: window.mosaicoLanguageStrings
};
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase, publicUrlBase));
const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase, publicUrlBase));
const template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath;
const allPlugins = plugins.concat(window.mosaicoPlugins);
Mosaico.start(config, template, metadata, model, allPlugins);
}
render() {
return <div/>;
}
}
export default function() {
parentRPC.init();

View file

@ -2,24 +2,18 @@
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import PropTypes from "prop-types";
import styles from "./sandboxed-mosaico.scss";
import PropTypes
from "prop-types";
import styles
from "./sandboxed-mosaico.scss";
import {UntrustedContentHost, parentRPC} from './untrusted';
import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
import {getTrustedUrl} from "./urls";
@translate(null, { withRef: true })
export class MosaicoEditorHost extends Component {
export class MosaicoHost extends Component {
constructor(props) {
super(props);
@ -81,96 +75,7 @@ export class MosaicoEditorHost extends Component {
}
}
MosaicoEditorHost.prototype.exportState = async function() {
MosaicoHost.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,
templateId: PropTypes.number,
templatePath: PropTypes.string,
initialModel: PropTypes.string,
initialMetadata: PropTypes.string
}
async exportState(method, params) {
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
return {
html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase, publicUrlBase),
metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase, publicUrlBase)
};
}
componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
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 = getTrustedUrl('static/mosaico/img/mosaico32.png');
vm.logoUrl = '#';
});
const config = {
imgProcessorBackend: getTrustedUrl('mosaico/img'),
emailProcessorBackend: getSandboxUrl('mosaico/dl'),
fileuploadConfig: {
url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
},
strings: window.mosaicoLanguageStrings
};
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase, publicUrlBase));
const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase, publicUrlBase));
const template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath;
const allPlugins = plugins.concat(window.mosaicoPlugins);
Mosaico.start(config, template, metadata, model, allPlugins);
}
render() {
return <div/>;
}
}

View file

@ -12,9 +12,9 @@ import {
import 'brace/mode/text';
import 'brace/mode/html';
import { MosaicoEditorHost } from "../lib/sandboxed-mosaico";
import { MosaicoHost } from "../lib/sandboxed-mosaico";
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
import GrapesJS from "../lib/grapesjs";
import { GrapesJSHost } from "../lib/sandboxed-grapesjs";
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
import {getSandboxUrl} from "../lib/urls";
@ -70,7 +70,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<MosaicoEditorHost
<MosaicoHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
@ -132,7 +132,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
},
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<MosaicoEditorHost
<MosaicoHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
@ -175,38 +175,33 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
validate: state => {}
};
templateTypes.xgrapesjs = { // FIXME
typeName: t('GrapeJS'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => null,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
templateTypes.grapesjs = {
typeName: t('GrapeJS (fake)'),
typeName: t('GrapeJS'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<GrapesJS
<GrapesJSHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
initialHtml={owner.getFormValue(prefix + 'html')}
entityTypeId={entityTypeId}/>
initialModel={owner.getFormValue(prefix + 'grapesJSData')}
entityTypeId={entityTypeId}
title={t('GrapesJS Template Designer')}
onFullscreenAsync={::owner.setElementInFullscreen}
/>
</AlignedRow>,
exportHTMLEditorData: async owner => {
const {html} = await owner.editorNode.exportState();
const {html, model} = await owner.editorNode.exportState();
owner.updateFormValue(prefix + 'html', html);
owner.updateFormValue(prefix + 'grapesJSData', model);
},
initData: () => ({
[prefix + 'grapesJSData']: {}
}),
afterLoad: data => {
data[prefix + 'grapesJSData'] = data[prefix + 'data'];
},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
data[prefix + 'data'] = data[prefix + 'grapesJSData'];
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
@ -217,7 +212,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
typeName: t('CKEditor 4'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner =>
<AlignedRow label={t('Template content')}>
<AlignedRow label={t('Template content (HTML)')}>
<CKEditorHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
@ -243,7 +238,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
templateTypes.ckeditor5 = {
typeName: t('CKEditor 5'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content')}/>,
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},

View file

@ -16,6 +16,7 @@ module.exports = {
"root": ['babel-polyfill', './src/root.js'],
"mosaico-root": ['babel-polyfill', './src/lib/sandboxed-mosaico-root.js'],
"ckeditor-root": ['babel-polyfill', './src/lib/sandboxed-ckeditor-root.js'],
"grapesjs-root": ['babel-polyfill', './src/lib/sandboxed-grapesjs-root.js'],
},
output: {
library: 'MailtrainReactBody',
@ -51,21 +52,23 @@ module.exports = {
test: /\.css$/,
use: [
{
loader: 'style-loader',
options: {
singleton: true
}
loader: 'style-loader'
},
{
loader: 'css-loader'
},
}
]
},
{
test: /ckeditor5-[^/]+\/theme\/[\w-/]+\.css$/,
use: [
{
loader: 'postcss-loader',
options: styles.getPostCssConfig( {
themeImporter: {
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
},
minify: false
minify: true
} )
}
]
@ -81,10 +84,6 @@ module.exports = {
}
]
},
{
test: /\.svg$/,
use: [ 'raw-loader' ]
},
{
test: /\.scss$/,
exclude: path.join(__dirname, 'node_modules'),
@ -97,11 +96,21 @@ module.exports = {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
},
'sass-loader' ]
'sass-loader'
]
},
{
test: /\.(otf|woff2|woff|ttf|eot)$/,
use: [ 'raw-loader' ]
test: /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg|ckeditor-insert-image\.svg$/,
use: [
'raw-loader'
]
},
{
test: /\.(svg|otf|woff2|woff|ttf|eot)$/,
exclude: /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg|ckeditor-insert-image\.svg$/,
use: [
'url-loader'
]
}
]
},
@ -116,6 +125,6 @@ module.exports = {
],
watchOptions: {
ignored: 'node_modules/',
poll: 1000
poll: 2000
}
};

View file

@ -35,10 +35,8 @@ users.registerRestrictedAccessTokenMethod('ckeditor', async ({entityTypeId, enti
function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file');
if (appType === AppType.SANDBOXED) {
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);

View file

@ -0,0 +1,59 @@
'use strict';
const routerFactory = require('../lib/router-async');
const passport = require('../lib/passport');
const clientHelpers = require('../lib/client-helpers');
const users = require('../models/users');
const files = require('../models/files');
const fileHelpers = require('../lib/file-helpers');
const templates = require('../models/templates');
const contextHelpers = require('../lib/context-helpers');
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
const { AppType } = require('../shared/app');
users.registerRestrictedAccessTokenMethod('grapesjs', async ({entityTypeId, entityId}) => {
if (entityTypeId === 'template') {
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
if (tmpl.type === 'grapesjs') {
return {
permissions: {
'template': {
[entityId]: new Set(['manageFiles', 'view'])
}
}
};
}
}
});
function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
res.render('grapesjs/root', {
layout: 'grapesjs/layout',
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getSandboxUrl('mailtrain/common.js'),
getSandboxUrl('mailtrain/grapesjs-root.js')
],
publicPath: getSandboxUrl()
});
});
}
return router;
}
module.exports.getRouter = getRouter;

View file

@ -10,9 +10,7 @@
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
<link rel="icon" href="{{publicPath}}static/favicon.ico">
<title>Mailtrain
{{#if title}} | {{title}}{{/if}}
</title>
<title>Mailtrain</title>
<link rel="stylesheet" href="{{publicPath}}static/bootstrap/themes/united.min.css">
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>

33
views/grapesjs/layout.hbs Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
<link rel="icon" href="{{publicPath}}static/favicon.ico">
<title>Mailtrain</title>
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>
{{#if mailtrainConfig}}
<script>
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>
{{#each scriptFiles}}
<script src="{{this}}"></script>
{{/each}}
{{/if}}
</head>
<body class="grapesjs-body">
{{{body}}}
</body>
</html>

6
views/grapesjs/root.hbs Normal file
View file

@ -0,0 +1,6 @@
<div id="root"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
MailtrainReactBody.default();
});
</script>

View file

@ -1,11 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=1024, initial-scale=1">
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
<link rel="icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
<link rel="icon" href="{{publicPath}}static/favicon.ico">
<title>Mailtrain</title>