CKEditor components replaced by CKEditor 5.
Remains of the sandboxed CKEditor - will be removed, but the version here may be useful for another editor that is prone to XSS (like Summernote).
This commit is contained in:
parent
213039c141
commit
eacdc74c29
43 changed files with 12499 additions and 1382 deletions
3
TODO.md
3
TODO.md
|
@ -10,6 +10,9 @@
|
||||||
### Message delivery
|
### Message delivery
|
||||||
- Better integration with ZoneMTA to allow multiple send configurations (with different DKIM) against one ZoneMTA instance via different HTTP configuration of ZoneMTA. This may need an extension of ZoneMTA to provide some header entry that identifies the campaign.
|
- Better integration with ZoneMTA to allow multiple send configurations (with different DKIM) against one ZoneMTA instance via different HTTP configuration of ZoneMTA. This may need an extension of ZoneMTA to provide some header entry that identifies the campaign.
|
||||||
|
|
||||||
|
### Lists
|
||||||
|
- CSV Export
|
||||||
|
|
||||||
### Campaigns
|
### Campaigns
|
||||||
- Statistics for a sent campaign
|
- Statistics for a sent campaign
|
||||||
- List of sent RSS campaigns (?)
|
- List of sent RSS campaigns (?)
|
||||||
|
|
|
@ -22,7 +22,8 @@ const api = require('./routes/api');
|
||||||
// These are routes for the new React-based client
|
// These are routes for the new React-based client
|
||||||
const reports = require('./routes/reports');
|
const reports = require('./routes/reports');
|
||||||
const subscription = require('./routes/subscription');
|
const subscription = require('./routes/subscription');
|
||||||
const mosaico = require('./routes/mosaico');
|
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
|
||||||
|
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
|
||||||
const files = require('./routes/files');
|
const files = require('./routes/files');
|
||||||
const links = require('./routes/links');
|
const links = require('./routes/links');
|
||||||
const archive = require('./routes/archive');
|
const archive = require('./routes/archive');
|
||||||
|
@ -221,7 +222,8 @@ function createApp(appType) {
|
||||||
useWith404Fallback('/files', files);
|
useWith404Fallback('/files', files);
|
||||||
}
|
}
|
||||||
|
|
||||||
useWith404Fallback('/mosaico', mosaico.getRouter(appType));
|
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
|
||||||
|
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
|
||||||
|
|
||||||
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
|
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"presets": ["es2015", "stage-1"],
|
|
||||||
"plugins": ["transform-react-jsx", "transform-decorators-legacy", "transform-function-bind"]
|
|
||||||
}
|
|
5578
client/package-lock.json
generated
5578
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -16,6 +16,24 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"homepage": "https://mailtrain.org/",
|
"homepage": "https://mailtrain.org/",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ckeditor/ckeditor5-alignment": "^10.0.3",
|
||||||
|
"@ckeditor/ckeditor5-basic-styles": "^10.0.3",
|
||||||
|
"@ckeditor/ckeditor5-block-quote": "^10.1.0",
|
||||||
|
"@ckeditor/ckeditor5-core": "^11.0.1",
|
||||||
|
"@ckeditor/ckeditor5-easy-image": "^10.0.3",
|
||||||
|
"@ckeditor/ckeditor5-editor-classic": "^11.0.1",
|
||||||
|
"@ckeditor/ckeditor5-essentials": "^10.1.2",
|
||||||
|
"@ckeditor/ckeditor5-heading": "^10.1.0",
|
||||||
|
"@ckeditor/ckeditor5-image": "^11.0.0",
|
||||||
|
"@ckeditor/ckeditor5-link": "^10.0.4",
|
||||||
|
"@ckeditor/ckeditor5-list": "^11.0.2",
|
||||||
|
"@ckeditor/ckeditor5-media-embed": "^10.0.0",
|
||||||
|
"@ckeditor/ckeditor5-paragraph": "^10.0.3",
|
||||||
|
"@ckeditor/ckeditor5-react": "^1.0.0",
|
||||||
|
"@ckeditor/ckeditor5-table": "^11.0.0",
|
||||||
|
"@ckeditor/ckeditor5-theme-lark": "^11.1.0",
|
||||||
|
"@ckeditor/ckeditor5-ui": "^11.1.0",
|
||||||
|
"@ckeditor/ckeditor5-upload": "^10.0.3",
|
||||||
"axios": "^0.16.2",
|
"axios": "^0.16.2",
|
||||||
"datatables.net": "^1.10.15",
|
"datatables.net": "^1.10.15",
|
||||||
"datatables.net-bs": "^1.10.15",
|
"datatables.net-bs": "^1.10.15",
|
||||||
|
@ -28,7 +46,6 @@
|
||||||
"querystringify": "^1.0.0",
|
"querystringify": "^1.0.0",
|
||||||
"react": "^15.6.1",
|
"react": "^15.6.1",
|
||||||
"react-ace": "^5.1.0",
|
"react-ace": "^5.1.0",
|
||||||
"react-ckeditor-component": "^1.0.7",
|
|
||||||
"react-day-picker": "^6.1.0",
|
"react-day-picker": "^6.1.0",
|
||||||
"react-dnd-html5-backend": "^2.4.1",
|
"react-dnd-html5-backend": "^2.4.1",
|
||||||
"react-dnd-touch-backend": "^0.3.13",
|
"react-dnd-touch-backend": "^0.3.13",
|
||||||
|
@ -41,16 +58,20 @@
|
||||||
"url-parse": "^1.1.9"
|
"url-parse": "^1.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ckeditor/ckeditor5-dev-utils": "^11.0.1",
|
||||||
|
"@ckeditor/ckeditor5-dev-webpack-plugin": "^7.0.1",
|
||||||
"babel-cli": "^6.24.1",
|
"babel-cli": "^6.24.1",
|
||||||
"babel-loader": "^7.1.1",
|
"babel-loader": "^7.1.1",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
"babel-plugin-transform-function-bind": "^6.22.0",
|
"babel-plugin-transform-function-bind": "^6.22.0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-env": "^1.7.0",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
"babel-preset-stage-1": "^6.24.1",
|
"babel-preset-stage-1": "^6.24.1",
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.4",
|
||||||
"i18next-conv": "^3.0.3",
|
"i18next-conv": "^3.0.3",
|
||||||
"node-sass": "^4.5.3",
|
"node-sass": "^4.5.3",
|
||||||
|
"postcss-loader": "^3.0.0",
|
||||||
|
"raw-loader": "^0.5.1",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"url-loader": "^0.5.9",
|
"url-loader": "^0.5.9",
|
||||||
|
|
|
@ -36,7 +36,8 @@ import {DeleteModalDialog} from "../lib/modals";
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
import {
|
import {
|
||||||
getTemplateTypes,
|
getTemplateTypes,
|
||||||
getTypeForm
|
getTypeForm,
|
||||||
|
ResourceType
|
||||||
} from '../templates/helpers';
|
} from '../templates/helpers';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import styles from "../lib/styles.scss";
|
import styles from "../lib/styles.scss";
|
||||||
|
@ -50,7 +51,6 @@ import {
|
||||||
} from "../../../shared/campaigns";
|
} from "../../../shared/campaigns";
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {getMailerTypes} from "../send-configurations/helpers";
|
import {getMailerTypes} from "../send-configurations/helpers";
|
||||||
import {ResourceType} from "../lib/mosaico";
|
|
||||||
import {getCampaignLabels} from "./helpers";
|
import {getCampaignLabels} from "./helpers";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
|
|
@ -21,12 +21,12 @@ import mailtrainConfig from 'mailtrainConfig';
|
||||||
import {
|
import {
|
||||||
getEditForm,
|
getEditForm,
|
||||||
getTemplateTypes,
|
getTemplateTypes,
|
||||||
getTypeForm
|
getTypeForm,
|
||||||
|
ResourceType
|
||||||
} from '../templates/helpers';
|
} from '../templates/helpers';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import styles from "../lib/styles.scss";
|
import styles from "../lib/styles.scss";
|
||||||
import {getUrl} from "../lib/urls";
|
import {getUrl} from "../lib/urls";
|
||||||
import {ResourceType} from "../lib/mosaico";
|
|
||||||
|
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default class List extends Component {
|
||||||
actions: data => {
|
actions: data => {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers')) {
|
if (mailtrainConfig.globalPermissions.setupAutomation && this.props.campaign.permissions.includes('manageTriggers')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
|
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
|
||||||
|
@ -77,7 +77,7 @@ export default class List extends Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{tableDeleteDialogRender(this, `rest/triggers/${this.props.campaign.id}`, t('Deleting trigger ...'), t('Trigger deleted'))}
|
{tableDeleteDialogRender(this, `rest/triggers/${this.props.campaign.id}`, t('Deleting trigger ...'), t('Trigger deleted'))}
|
||||||
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.campaign.permissions.includes('manageTriggers') &&
|
{mailtrainConfig.globalPermissions.setupAutomation && this.props.campaign.permissions.includes('manageTriggers') &&
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
|
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
16
client/src/lib/ckeditor-insert-image.svg
Normal file
16
client/src/lib/ckeditor-insert-image.svg
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
>
|
||||||
|
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-rule:nonzero;stroke-width:0.89999998"
|
||||||
|
d="m 4.9296875,1.5273438 c -0.549,0 -0.9902344,0.4233124 -0.9902344,0.9453124 V 8.1542969 C 3.9929718,8.1073482 4.0362288,8.0501836 4.09375,8.0078125 4.4641691,7.6414817 4.869899,7.3679645 5.2890625,7.1503906 V 2.8496094 H 17.501953 V 11.957031 L 15.234375,9.2851562 c -0.220904,-0.204796 -0.562299,-0.204796 -0.783203,0 l -2.00586,1.8535158 -0.451171,-0.421875 c 0.101968,0.808412 -0.008,1.673794 -0.398438,2.529297 -0.04665,0.163457 -0.103202,0.325377 -0.201172,0.464844 l -0.002,0.002 -0.257812,0.36914 h 6.736328 c 0.54,0 0.982422,-0.422125 0.982422,-0.953125 V 2.4726562 c 0,-0.522 -0.442422,-0.9453124 -0.982422,-0.9453124 z M 14.714844,3.8828125 C 13.350658,3.8741006 12.49215,5.3495586 13.173828,6.53125 14.349609,8.5716138 17.410156,6.8079419 16.234375,4.7675781 15.921123,4.2235411 15.342608,3.8866937 14.714844,3.8828125 Z"
|
||||||
|
id="path2"/>
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-rule:evenodd;stroke-width:0.69999999"
|
||||||
|
d="m 6.8138243,16.679773 0.6937,-0.9912 a 0.52500478,0.52500478 0 1 1 0.8603,0.602 l -0.8036,1.148 a 0.5236,0.5236 0 0 1 -0.1519,0.1442 3.6757,3.6757 0 0 1 -5.9521,-4.1685 c 0.014,-0.0665 0.042,-0.1323 0.084,-0.1918 l 0.8029,-1.1473 a 0.525,0.525 0 1 1 0.8596,0.602 l -0.6937,0.9926 0.0042,0.0021 a 2.625,2.625 0 0 0 4.2924,3.0058 l 0.0042,0.0028 z m 3.8457997,-3.7345 a 0.5236,0.5236 0 0 1 -0.084,0.1918 l -0.8028997,1.1473 a 0.525,0.525 0 1 1 -0.8596,-0.602 l 0.602,-0.861 a 2.625,2.625 0 0 0 -4.3008,-3.0106998 l -0.602,0.8602998 a 0.52500478,0.52500478 0 0 1 -0.8603,-0.602 l 0.8036,-1.1479998 a 0.5236,0.5236 0 0 1 0.1519,-0.1442 3.6757,3.6757 0 0 1 5.9520997,4.1684998 z m -3.1940997,-1.7724 a 0.525,0.525 0 0 1 0.1288,0.7315 l -2.2085,3.1535 a 0.52500478,0.52500478 0 1 1 -0.8603,-0.602 l 2.2085,-3.1542 a 0.525,0.525 0 0 1 0.7315,-0.1288 z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2 KiB |
190
client/src/lib/ckeditor.js
vendored
Normal file
190
client/src/lib/ckeditor.js
vendored
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
|
import ClassicEditorBase from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
|
||||||
|
|
||||||
|
import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials';
|
||||||
|
import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold';
|
||||||
|
import UnderlinePlugin from '@ckeditor/ckeditor5-basic-styles/src/underline';
|
||||||
|
import StrikethroughPlugin from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
|
||||||
|
import CodePlugin from '@ckeditor/ckeditor5-basic-styles/src/code';
|
||||||
|
import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic';
|
||||||
|
import BlockQuotePlugin from '@ckeditor/ckeditor5-block-quote/src/blockquote';
|
||||||
|
import EasyImagePlugin from '@ckeditor/ckeditor5-easy-image/src/easyimage';
|
||||||
|
import HeadingPlugin from '@ckeditor/ckeditor5-heading/src/heading';
|
||||||
|
import ImagePlugin from '@ckeditor/ckeditor5-image/src/image';
|
||||||
|
import ImageCaptionPlugin from '@ckeditor/ckeditor5-image/src/imagecaption';
|
||||||
|
import ImageStylePlugin from '@ckeditor/ckeditor5-image/src/imagestyle';
|
||||||
|
import ImageToolbarPlugin from '@ckeditor/ckeditor5-image/src/imagetoolbar';
|
||||||
|
import ImageUploadPlugin from '@ckeditor/ckeditor5-image/src/imageupload';
|
||||||
|
import LinkPlugin from '@ckeditor/ckeditor5-link/src/link';
|
||||||
|
import ListPlugin from '@ckeditor/ckeditor5-list/src/list';
|
||||||
|
import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph';
|
||||||
|
import AlignmentPlugin from '@ckeditor/ckeditor5-alignment/src/alignment';
|
||||||
|
import TablePlugin from '@ckeditor/ckeditor5-table/src/table';
|
||||||
|
import TableToolbarPlugin from '@ckeditor/ckeditor5-table/src/tabletoolbar';
|
||||||
|
|
||||||
|
import CKEditor from '@ckeditor/ckeditor5-react';
|
||||||
|
|
||||||
|
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||||
|
import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
|
||||||
|
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
|
||||||
|
|
||||||
|
import insertImageIcon from './ckeditor-insert-image.svg';
|
||||||
|
|
||||||
|
class InsertImage extends Plugin {
|
||||||
|
init() {
|
||||||
|
const editor = this.editor;
|
||||||
|
|
||||||
|
editor.ui.componentFactory.add( 'insertImage', locale => {
|
||||||
|
const view = new ButtonView( locale );
|
||||||
|
|
||||||
|
view.set( {
|
||||||
|
label: 'Insert image',
|
||||||
|
icon: insertImageIcon,
|
||||||
|
tooltip: true
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Callback executed once the image is clicked.
|
||||||
|
view.on( 'execute', () => {
|
||||||
|
let url = '';
|
||||||
|
const selectedElement = editor.model.document.selection.getSelectedElement();
|
||||||
|
if (selectedElement) {
|
||||||
|
if (selectedElement.is('element', 'image')) {
|
||||||
|
url = selectedElement.getAttribute('src');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = prompt('Image URL', url);
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
editor.model.change( writer => {
|
||||||
|
const imageElement = writer.createElement( 'image', {
|
||||||
|
src: imageUrl
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Insert the image in the current selection location.
|
||||||
|
editor.model.insertContent( imageElement, editor.model.document.selection );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
return view;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UploadAdapter {
|
||||||
|
constructor(loader, url, t) {
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
async upload() {
|
||||||
|
console.log(this.loader);
|
||||||
|
return {
|
||||||
|
default: 'http://server/default-size.image.png'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MailtrainUploadAdapter extends Plugin {
|
||||||
|
static get requires() {
|
||||||
|
return [ FileRepository ];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get pluginName() {
|
||||||
|
return 'MailtrainUploadAdapter';
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.editor.plugins.get(FileRepository).createUploadAdapter = loader => new UploadAdapter(loader, this.editor.t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ClassicEditor extends ClassicEditorBase {}
|
||||||
|
|
||||||
|
ClassicEditor.builtinPlugins = [
|
||||||
|
EssentialsPlugin,
|
||||||
|
BoldPlugin,
|
||||||
|
ItalicPlugin,
|
||||||
|
UnderlinePlugin,
|
||||||
|
StrikethroughPlugin,
|
||||||
|
CodePlugin,
|
||||||
|
BlockQuotePlugin,
|
||||||
|
HeadingPlugin,
|
||||||
|
ImagePlugin,
|
||||||
|
ImageCaptionPlugin,
|
||||||
|
ImageStylePlugin,
|
||||||
|
ImageToolbarPlugin,
|
||||||
|
ImageUploadPlugin,
|
||||||
|
LinkPlugin,
|
||||||
|
ListPlugin,
|
||||||
|
ParagraphPlugin,
|
||||||
|
AlignmentPlugin,
|
||||||
|
TablePlugin,
|
||||||
|
TableToolbarPlugin,
|
||||||
|
MailtrainUploadAdapter,
|
||||||
|
InsertImage
|
||||||
|
];
|
||||||
|
|
||||||
|
ClassicEditor.defaultConfig = {
|
||||||
|
toolbar: {
|
||||||
|
items: [
|
||||||
|
'heading',
|
||||||
|
'|',
|
||||||
|
'bold',
|
||||||
|
'italic',
|
||||||
|
'underline',
|
||||||
|
'strikethrough',
|
||||||
|
'code',
|
||||||
|
'|',
|
||||||
|
'alignment',
|
||||||
|
'|',
|
||||||
|
'link',
|
||||||
|
'bulletedList',
|
||||||
|
'numberedList',
|
||||||
|
'|',
|
||||||
|
'insertImage',
|
||||||
|
'imageUpload',
|
||||||
|
'blockQuote',
|
||||||
|
'|',
|
||||||
|
'insertTable',
|
||||||
|
'|',
|
||||||
|
'undo',
|
||||||
|
'redo'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
alignment: {
|
||||||
|
options: [ 'left', 'center', 'right', 'justify' ]
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
toolbar: [
|
||||||
|
'imageStyle:full',
|
||||||
|
'imageStyle:side',
|
||||||
|
'|',
|
||||||
|
'imageTextAlternative'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
|
||||||
|
},
|
||||||
|
language: 'en'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class CKEditorWrapper extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CKEditor
|
||||||
|
editor={ ClassicEditor }
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -160,7 +160,7 @@ export default class Files extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
this.props.entity.permissions.includes(this.props.managePermission) &&
|
this.props.entity.permissions.includes(this.props.managePermission) &&
|
||||||
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
|
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName={styles.dropZoneActive}>
|
||||||
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import ACEEditorRaw from 'react-ace';
|
||||||
import 'brace/theme/github';
|
import 'brace/theme/github';
|
||||||
import 'brace/ext/searchbox';
|
import 'brace/ext/searchbox';
|
||||||
|
|
||||||
import CKEditorRaw from "react-ckeditor-component";
|
import CKEditorRaw from './ckeditor';
|
||||||
|
|
||||||
import DayPicker from 'react-day-picker';
|
import DayPicker from 'react-day-picker';
|
||||||
import 'react-day-picker/lib/style.css';
|
import 'react-day-picker/lib/style.css';
|
||||||
|
@ -892,11 +892,11 @@ class CKEditor extends Component {
|
||||||
|
|
||||||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||||
<CKEditorRaw
|
<CKEditorRaw
|
||||||
events={{
|
onChange={(event, editor) => owner.updateFormValue(id, editor.getData())}
|
||||||
"change": evt => owner.updateFormValue(id, evt.editor.getData())
|
onInit={ editor => {
|
||||||
|
editor.ui.view.editable.editableElement.style.height = props.height;
|
||||||
} }
|
} }
|
||||||
content={owner.getFormValue(id)}
|
data={owner.getFormValue(id)}
|
||||||
config={{width: '100%', height: props.height}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class RestActionModalDialog extends Component {
|
||||||
|
|
||||||
async hideModal(isBack) {
|
async hideModal(isBack) {
|
||||||
if (this.props.backUrl) {
|
if (this.props.backUrl) {
|
||||||
this.props.stateOwner.navigateTo(this.props.backUrl);
|
this.navigateTo(this.props.backUrl);
|
||||||
} else {
|
} else {
|
||||||
if (isBack) {
|
if (isBack) {
|
||||||
this.props.onBack();
|
this.props.onBack();
|
||||||
|
@ -68,7 +68,7 @@ export class RestActionModalDialog extends Component {
|
||||||
await axios.method(props.actionMethod, getUrl(props.actionUrl));
|
await axios.method(props.actionMethod, getUrl(props.actionUrl));
|
||||||
|
|
||||||
if (props.successUrl) {
|
if (props.successUrl) {
|
||||||
owner.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
|
this.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
|
||||||
} else {
|
} else {
|
||||||
props.onSuccess();
|
props.onSuccess();
|
||||||
this.setFlashMessage('success', props.actionDoneMsg);
|
this.setFlashMessage('success', props.actionDoneMsg);
|
||||||
|
|
|
@ -345,7 +345,7 @@ class SectionContent extends Component {
|
||||||
async closeFlashMessage() {
|
async closeFlashMessage() {
|
||||||
this.setState({
|
this.setState({
|
||||||
flashMessageText: ''
|
flashMessageText: ''
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRoute(route) {
|
renderRoute(route) {
|
||||||
|
@ -462,8 +462,12 @@ function requiresAuthenticatedUser(target) {
|
||||||
const comp1 = withPageHelpers(target);
|
const comp1 = withPageHelpers(target);
|
||||||
|
|
||||||
function comp2(props, context) {
|
function comp2(props, context) {
|
||||||
|
if (!new.target) {
|
||||||
|
throw new TypeError();
|
||||||
|
}
|
||||||
|
|
||||||
context.sectionContent.ensureAuthenticated();
|
context.sectionContent.ensureAuthenticated();
|
||||||
comp1.call(this, props, context);
|
return Reflect.construct(comp1, [props, context], new.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
comp2.prototype = comp1.prototype;
|
comp2.prototype = comp1.prototype;
|
||||||
|
|
23
client/src/lib/sandboxed-ckeditor-root.js
Normal file
23
client/src/lib/sandboxed-ckeditor-root.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
parentRPC.init();
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<I18nextProvider i18n={ i18n }>
|
||||||
|
<UntrustedContentRoot render={props => <CKEditorSandbox {...props} />} />
|
||||||
|
</I18nextProvider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
110
client/src/lib/sandboxed-ckeditor.js
Normal file
110
client/src/lib/sandboxed-ckeditor.js
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import {translate} from 'react-i18next';
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import styles from "./sandboxed-ckeditor.scss";
|
||||||
|
|
||||||
|
import {UntrustedContentHost, parentRPC} from './untrusted';
|
||||||
|
import {Icon} from "./bootstrap-components";
|
||||||
|
import {
|
||||||
|
getPublicUrl,
|
||||||
|
getSandboxUrl,
|
||||||
|
getTrustedUrl
|
||||||
|
} from "./urls";
|
||||||
|
import {
|
||||||
|
base,
|
||||||
|
unbase
|
||||||
|
} from "../../../shared/templates";
|
||||||
|
import CKEditor from './ckeditor';
|
||||||
|
|
||||||
|
@translate(null, { withRef: true })
|
||||||
|
export class CKEditorHost extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
entityTypeId: PropTypes.string,
|
||||||
|
entity: PropTypes.object,
|
||||||
|
initialHtml: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
initialHtml: this.props.initialHtml
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenData = {
|
||||||
|
entityTypeId: this.props.entityTypeId,
|
||||||
|
entityId: this.props.entity.id
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.editor}>
|
||||||
|
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="ckeditor/editor" tokenMethod="ckeditor" tokenParams={editorData}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CKEditorHost.prototype.exportState = async function() {
|
||||||
|
return await this.getWrappedInstance().exportState();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
parentRPC.setMethodHandler('exportState', ::this.exportState);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={styles.sandbox}>
|
||||||
|
<CKEditor
|
||||||
|
onChange={(event, editor) => this.setState({html: editor.getData()})}
|
||||||
|
data={this.state.html}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
10
client/src/lib/sandboxed-ckeditor.scss
Normal file
10
client/src/lib/sandboxed-ckeditor.scss
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.editor {
|
||||||
|
.host {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sandbox {
|
||||||
|
:global .ck-editor__editable {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import {I18nextProvider,} from 'react-i18next';
|
import {I18nextProvider,} from 'react-i18next';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import {MosaicoSandbox} from './mosaico';
|
import {MosaicoSandbox} from './sandboxed-mosaico';
|
||||||
import {UntrustedContentRoot, parentRPC} from './untrusted';
|
import {UntrustedContentRoot, parentRPC} from './untrusted';
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
|
@ -3,7 +3,7 @@
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {translate} from 'react-i18next';
|
import {translate} from 'react-i18next';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import styles from "./mosaico.scss";
|
import styles from "./sandboxed-mosaico.scss";
|
||||||
|
|
||||||
import {UntrustedContentHost, parentRPC} from './untrusted';
|
import {UntrustedContentHost, parentRPC} from './untrusted';
|
||||||
import {Icon} from "./bootstrap-components";
|
import {Icon} from "./bootstrap-components";
|
||||||
|
@ -18,13 +18,8 @@ import {
|
||||||
} from "../../../shared/templates";
|
} from "../../../shared/templates";
|
||||||
|
|
||||||
|
|
||||||
export const ResourceType = {
|
|
||||||
TEMPLATE: 'template',
|
|
||||||
CAMPAIGN: 'campaign'
|
|
||||||
}
|
|
||||||
|
|
||||||
@translate(null, { withRef: true })
|
@translate(null, { withRef: true })
|
||||||
export class MosaicoEditor extends Component {
|
export class MosaicoEditorHost extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -59,7 +54,7 @@ export class MosaicoEditor extends Component {
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
|
|
||||||
const mosaicoData = {
|
const editorData = {
|
||||||
entityTypeId: this.props.entityTypeId,
|
entityTypeId: this.props.entityTypeId,
|
||||||
entityId: this.props.entity.id,
|
entityId: this.props.entity.id,
|
||||||
templateId: this.props.templateId,
|
templateId: this.props.templateId,
|
||||||
|
@ -68,6 +63,11 @@ export class MosaicoEditor extends Component {
|
||||||
initialMetadata: this.props.initialMetadata
|
initialMetadata: this.props.initialMetadata
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tokenData = {
|
||||||
|
entityTypeId: this.props.entityTypeId,
|
||||||
|
entityId: this.props.entity.id
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
|
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
|
||||||
<div className={styles.navbar}>
|
<div className={styles.navbar}>
|
||||||
|
@ -75,13 +75,13 @@ export class MosaicoEditor extends Component {
|
||||||
<div className={styles.title}>{this.props.title}</div>
|
<div className={styles.title}>{this.props.title}</div>
|
||||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||||
</div>
|
</div>
|
||||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={mosaicoData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={mosaicoData}/>
|
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={tokenData}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MosaicoEditor.prototype.exportState = async function() {
|
MosaicoEditorHost.prototype.exportState = async function() {
|
||||||
return await this.getWrappedInstance().exportState();
|
return await this.getWrappedInstance().exportState();
|
||||||
};
|
};
|
||||||
|
|
|
@ -121,6 +121,12 @@
|
||||||
color: #808080;
|
color: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropZoneActive{
|
||||||
|
border-color: #90EE90;
|
||||||
|
color: #000;
|
||||||
|
background-color: #DDFFDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.untrustedContent {
|
.untrustedContent {
|
||||||
border: 0px none;
|
border: 0px none;
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { withPageHelpers } from './page'
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||||
import styles from "./styles.scss";
|
import styles from "./styles.scss";
|
||||||
import {getUrl} from "./urls";
|
import {getUrl} from "./urls";
|
||||||
import {Table} from "./table";
|
|
||||||
|
|
||||||
const TreeSelectMode = {
|
const TreeSelectMode = {
|
||||||
NONE: 0,
|
NONE: 0,
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default class List extends Component {
|
||||||
const perms = data[9];
|
const perms = data[9];
|
||||||
const campaignId = data[8];
|
const campaignId = data[8];
|
||||||
|
|
||||||
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && perms.includes('manageTriggers')) {
|
if (mailtrainConfig.globalPermissions.setupAutomation && perms.includes('manageTriggers')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
|
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
|
||||||
|
|
|
@ -66,7 +66,7 @@ export default class List extends Component {
|
||||||
refreshTimeout = 1000;
|
refreshTimeout = 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports')) {
|
if (mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
link: `/lists/${this.props.list.id}/imports/${data[0]}/edit`
|
link: `/lists/${this.props.list.id}/imports/${data[0]}/edit`
|
||||||
|
@ -90,7 +90,7 @@ export default class List extends Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{tableDeleteDialogRender(this, `rest/imports/${this.props.list.id}`, t('Deleting import ...'), t('Import deleted'))}
|
{tableDeleteDialogRender(this, `rest/imports/${this.props.list.id}`, t('Deleting import ...'), t('Import deleted'))}
|
||||||
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') &&
|
{mailtrainConfig.globalPermissions.setupAutomation && this.props.list.permissions.includes('manageImports') &&
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
|
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
|
@ -76,7 +76,7 @@ function getMenus(t) {
|
||||||
':action(edit|delete)': {
|
':action(edit|delete)': {
|
||||||
title: t('Edit'),
|
title: t('Edit'),
|
||||||
link: params => `/reports/templates/${params.templateId}/edit`,
|
link: params => `/reports/templates/${params.templateId}/edit`,
|
||||||
visible: resolved => mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && resolved.template.permissions.includes('edit'),
|
visible: resolved => mailtrainConfig.globalPermissions.createJavascriptWithROAccess && resolved.template.permissions.includes('edit'),
|
||||||
panelRender: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
panelRender: props => <ReportTemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default class List extends Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess')
|
createPermitted: result.data.createReportTemplate && mailtrainConfig.globalPermissions.createJavascriptWithROAccess
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ export default class List extends Component {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const perms = data[5];
|
const perms = data[5];
|
||||||
|
|
||||||
if (mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && perms.includes('edit')) {
|
if (mailtrainConfig.globalPermissions.createJavascriptWithROAccess && perms.includes('edit')) {
|
||||||
actions.push({
|
actions.push({
|
||||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||||
link: `/reports/templates/${data[0]}/edit`
|
link: `/reports/templates/${data[0]}/edit`
|
||||||
|
|
|
@ -84,9 +84,9 @@ class Root extends Component {
|
||||||
<DropdownMenuItem label={t('Administration')}>
|
<DropdownMenuItem label={t('Administration')}>
|
||||||
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</MenuLink>
|
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</MenuLink>
|
||||||
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</MenuLink>
|
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</MenuLink>
|
||||||
{mailtrainConfig.globalPermissions.includes('manageSettings') && <MenuLink to="/settings"><Icon icon='cog'/> {t('Global Settings')}</MenuLink>}
|
{mailtrainConfig.globalPermissions.manageSettings && <MenuLink to="/settings"><Icon icon='cog'/> {t('Global Settings')}</MenuLink>}
|
||||||
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('Send Configurations')}</MenuLink>
|
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('Send Configurations')}</MenuLink>
|
||||||
{mailtrainConfig.globalPermissions.includes('manageBlacklist') && <MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>}
|
{mailtrainConfig.globalPermissions.manageBlacklist && <MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>}
|
||||||
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('API')}</MenuLink>
|
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('API')}</MenuLink>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -12,10 +12,8 @@ import {
|
||||||
import 'brace/mode/text';
|
import 'brace/mode/text';
|
||||||
import 'brace/mode/html';
|
import 'brace/mode/html';
|
||||||
|
|
||||||
import {
|
import { MosaicoEditorHost } from "../lib/sandboxed-mosaico";
|
||||||
MosaicoEditor,
|
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
|
||||||
ResourceType
|
|
||||||
} from "../lib/mosaico";
|
|
||||||
|
|
||||||
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
||||||
import {getSandboxUrl} from "../lib/urls";
|
import {getSandboxUrl} from "../lib/urls";
|
||||||
|
@ -28,6 +26,10 @@ import {Trans} from "react-i18next";
|
||||||
|
|
||||||
import styles from "../lib/styles.scss";
|
import styles from "../lib/styles.scss";
|
||||||
|
|
||||||
|
export const ResourceType = {
|
||||||
|
TEMPLATE: 'template',
|
||||||
|
CAMPAIGN: 'campaign'
|
||||||
|
}
|
||||||
|
|
||||||
export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEMPLATE) {
|
export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEMPLATE) {
|
||||||
// The prefix is used to to enable use within other forms (i.e. campaign form)
|
// The prefix is used to to enable use within other forms (i.e. campaign form)
|
||||||
|
@ -67,7 +69,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} />,
|
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
|
||||||
getHTMLEditor: owner =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content (HTML)')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<MosaicoEditor
|
<MosaicoEditorHost
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||||
|
@ -129,7 +131,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
},
|
},
|
||||||
getHTMLEditor: owner =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content (HTML)')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<MosaicoEditor
|
<MosaicoEditorHost
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||||
|
@ -172,7 +174,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
validate: state => {}
|
validate: state => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
templateTypes.grapejs = { // TODO
|
templateTypes.grapejs = { // FIXME
|
||||||
typeName: t('GrapeJS'),
|
typeName: t('GrapeJS'),
|
||||||
getTypeForm: (owner, isEdit) => null,
|
getTypeForm: (owner, isEdit) => null,
|
||||||
getHTMLEditor: owner => null,
|
getHTMLEditor: owner => null,
|
||||||
|
@ -186,10 +188,34 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
validate: state => {}
|
validate: state => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
templateTypes.xckeditor = {
|
||||||
|
typeName: t('CKEditor'),
|
||||||
|
getTypeForm: (owner, isEdit) => null,
|
||||||
|
getHTMLEditor: owner =>
|
||||||
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
|
<CKEditorHost
|
||||||
|
ref={node => owner.editorNode = node}
|
||||||
|
entity={owner.props.entity}
|
||||||
|
initialHtml={owner.getFormValue(prefix + 'html')}
|
||||||
|
entityTypeId={entityTypeId}/>
|
||||||
|
</AlignedRow>,
|
||||||
|
exportHTMLEditorData: async owner => {
|
||||||
|
const {html} = await owner.editorNode.exportState();
|
||||||
|
owner.updateFormValue(prefix + 'html', html);
|
||||||
|
},
|
||||||
|
initData: () => ({}),
|
||||||
|
afterLoad: data => {},
|
||||||
|
beforeSave: data => {
|
||||||
|
clearBeforeSave(data);
|
||||||
|
},
|
||||||
|
afterTypeChange: mutState => {},
|
||||||
|
validate: state => {}
|
||||||
|
};
|
||||||
|
|
||||||
templateTypes.ckeditor = {
|
templateTypes.ckeditor = {
|
||||||
typeName: t('CKEditor'),
|
typeName: t('CKEditor'),
|
||||||
getTypeForm: (owner, isEdit) => null,
|
getTypeForm: (owner, isEdit) => null,
|
||||||
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" label={t('Template content (HTML)')}/>,
|
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
|
||||||
exportHTMLEditorData: async owner => {},
|
exportHTMLEditorData: async owner => {},
|
||||||
initData: () => ({}),
|
initData: () => ({}),
|
||||||
afterLoad: data => {},
|
afterLoad: data => {},
|
||||||
|
@ -214,7 +240,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
validate: state => {}
|
validate: state => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
templateTypes.mjml = { // TODO
|
templateTypes.mjml = { // FIXME
|
||||||
getTypeForm: (owner, isEdit) => null,
|
getTypeForm: (owner, isEdit) => null,
|
||||||
getHTMLEditor: owner => null,
|
getHTMLEditor: owner => null,
|
||||||
exportHTMLEditorData: async owner => {},
|
exportHTMLEditorData: async owner => {},
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
// The CKEditor part comes from https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/advanced-setup.html
|
||||||
|
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' );
|
||||||
|
const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
new CKEditorWebpackPlugin( {
|
||||||
|
// See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html
|
||||||
|
language: 'en'
|
||||||
|
} )
|
||||||
|
],
|
||||||
entry: {
|
entry: {
|
||||||
root: ['babel-polyfill', './src/root.js'],
|
root: ['babel-polyfill', './src/root.js'],
|
||||||
mosaico: ['babel-polyfill', './src/lib/mosaico-sandbox-root.js'],
|
mosaico: ['babel-polyfill', './src/lib/sandboxed-mosaico-root.js'],
|
||||||
|
ckeditor: ['babel-polyfill', './src/lib/sandboxed-ckeditor-root.js'],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
library: 'MailtrainReactBody',
|
library: 'MailtrainReactBody',
|
||||||
|
@ -15,12 +26,51 @@ module.exports = {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.(js|jsx)$/,
|
test: /\.(js|jsx)$/,
|
||||||
exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
exclude: path.join(__dirname, 'node_modules'),
|
||||||
use: [ 'babel-loader' ]
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: [
|
||||||
|
['env', {
|
||||||
|
targets: {
|
||||||
|
"chrome": "58",
|
||||||
|
"edge": "15",
|
||||||
|
"firefox": "55",
|
||||||
|
"ios": "10"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
'stage-1'
|
||||||
|
],
|
||||||
|
plugins: ['transform-react-jsx', 'transform-decorators-legacy', 'transform-function-bind']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||||
|
// use: [ 'babel-loader' ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: [ 'style-loader', 'css-loader' ]
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'style-loader',
|
||||||
|
options: {
|
||||||
|
singleton: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'css-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'postcss-loader',
|
||||||
|
options: styles.getPostCssConfig( {
|
||||||
|
themeImporter: {
|
||||||
|
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
|
||||||
|
},
|
||||||
|
minify: false
|
||||||
|
} )
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpg|gif)$/,
|
test: /\.(png|jpg|gif)$/,
|
||||||
|
@ -33,6 +83,13 @@ module.exports = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Or /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg$/ if you want to limit this loader
|
||||||
|
// to CKEditor 5 icons only.
|
||||||
|
test: /\.svg$/,
|
||||||
|
|
||||||
|
use: [ 'raw-loader' ]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
exclude: path.join(__dirname, 'node_modules'),
|
exclude: path.join(__dirname, 'node_modules'),
|
||||||
|
|
|
@ -5,7 +5,6 @@ const config = require('config');
|
||||||
const forms = require('../models/forms');
|
const forms = require('../models/forms');
|
||||||
const shares = require('../models/shares');
|
const shares = require('../models/shares');
|
||||||
const urls = require('./urls');
|
const urls = require('./urls');
|
||||||
const { AppType } = require('../shared/app');
|
|
||||||
|
|
||||||
|
|
||||||
async function getAnonymousConfig(context, appType) {
|
async function getAnonymousConfig(context, appType) {
|
||||||
|
@ -26,6 +25,11 @@ async function getAnonymousConfig(context, appType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAuthenticatedConfig(context) {
|
async function getAuthenticatedConfig(context) {
|
||||||
|
const globalPermissions = {};
|
||||||
|
for (const perm of shares.getGlobalPermissions(context)) {
|
||||||
|
globalPermissions[perm] = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultCustomFormValues: await forms.getDefaultCustomFormValues(),
|
defaultCustomFormValues: await forms.getDefaultCustomFormValues(),
|
||||||
user: {
|
user: {
|
||||||
|
@ -33,7 +37,7 @@ async function getAuthenticatedConfig(context) {
|
||||||
username: context.user.username,
|
username: context.user.username,
|
||||||
namespace: context.user.namespace
|
namespace: context.user.namespace
|
||||||
},
|
},
|
||||||
globalPermissions: shares.getGlobalPermissions(context),
|
globalPermissions,
|
||||||
editors: config.editors,
|
editors: config.editors,
|
||||||
mosaico: config.mosaico,
|
mosaico: config.mosaico,
|
||||||
verpEnabled: config.verp.enabled
|
verpEnabled: config.verp.enabled
|
||||||
|
|
|
@ -55,4 +55,4 @@ async function ensureNoDependencies(tx, context, id, depSpecs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.ensureNoDependencies = ensureNoDependencies
|
module.exports.ensureNoDependencies = ensureNoDependencies;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let _ = require('./translate')._;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
enforce,
|
enforce,
|
||||||
cleanupFromPost,
|
cleanupFromPost,
|
||||||
|
|
|
@ -24,7 +24,7 @@ async function listTree(context) {
|
||||||
|
|
||||||
// Build a tree
|
// Build a tree
|
||||||
const rows = await knex('namespaces')
|
const rows = await knex('namespaces')
|
||||||
.innerJoin(entityType.permissionsTable, {
|
.leftJoin(entityType.permissionsTable, {
|
||||||
[entityType.permissionsTable + '.entity']: 'namespaces.id',
|
[entityType.permissionsTable + '.entity']: 'namespaces.id',
|
||||||
[entityType.permissionsTable + '.user']: context.user.id
|
[entityType.permissionsTable + '.user']: context.user.id
|
||||||
})
|
})
|
||||||
|
@ -63,7 +63,7 @@ async function listTree(context) {
|
||||||
entry.key = row.id;
|
entry.key = row.id;
|
||||||
entry.title = row.name;
|
entry.title = row.name;
|
||||||
entry.description = row.description;
|
entry.description = row.description;
|
||||||
entry.permissions = row.permissions.split(';');
|
entry.permissions = row.permissions ? row.permissions.split(';') : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prune out the inaccessible namespaces
|
// Prune out the inaccessible namespaces
|
||||||
|
@ -160,7 +160,7 @@ async function updateWithConsistencyCheck(context, entity) {
|
||||||
throw new interoperableErrors.DependencyNotFoundError();
|
throw new interoperableErrors.DependencyNotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iter.id == entity.id) {
|
if (iter.id === entity.id) {
|
||||||
throw new interoperableErrors.LoopDetectedError();
|
throw new interoperableErrors.LoopDetectedError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ const { enforce } = require('../lib/helpers');
|
||||||
const dtHelpers = require('../lib/dt-helpers');
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
const entitySettings = require('../lib/entity-settings');
|
const entitySettings = require('../lib/entity-settings');
|
||||||
const interoperableErrors = require('../shared/interoperable-errors');
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
const log = require('npmlog');
|
const log = require('../lib/log');
|
||||||
const {getGlobalNamespaceId} = require('../shared/namespaces');
|
const {getGlobalNamespaceId} = require('../shared/namespaces');
|
||||||
const {getAdminId} = require('../shared/users');
|
const {getAdminId} = require('../shared/users');
|
||||||
|
|
||||||
|
|
61
routes/sandboxed-ckeditor.js
Normal file
61
routes/sandboxed-ckeditor.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
'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('ckeditor', async ({entityTypeId, entityId}) => {
|
||||||
|
if (entityTypeId === 'template') {
|
||||||
|
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
|
||||||
|
|
||||||
|
if (tmpl.type === 'ckeditor') {
|
||||||
|
return {
|
||||||
|
permissions: {
|
||||||
|
'template': {
|
||||||
|
[entityId]: new Set(['manageFiles', 'view'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function getRouter(appType) {
|
||||||
|
const router = routerFactory.create();
|
||||||
|
|
||||||
|
if (appType === AppType.SANDBOXED) {
|
||||||
|
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file');
|
||||||
|
|
||||||
|
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||||
|
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
|
||||||
|
|
||||||
|
res.render('ckeditor/root', {
|
||||||
|
layout: 'ckeditor/layout',
|
||||||
|
reactCsrfToken: req.csrfToken(),
|
||||||
|
mailtrainConfig: JSON.stringify(mailtrainConfig),
|
||||||
|
scriptFiles: [
|
||||||
|
getSandboxUrl('mailtrain/common.js'),
|
||||||
|
getSandboxUrl('mailtrain/ckeditor.js')
|
||||||
|
],
|
||||||
|
publicPath: getSandboxUrl()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getRouter = getRouter;
|
|
@ -1,8 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const express = require('express');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const express = require('express');
|
||||||
const routerFactory = require('../lib/router-async');
|
const routerFactory = require('../lib/router-async');
|
||||||
const passport = require('../lib/passport');
|
const passport = require('../lib/passport');
|
||||||
const clientHelpers = require('../lib/client-helpers');
|
const clientHelpers = require('../lib/client-helpers');
|
||||||
|
@ -201,7 +201,7 @@ function getRouter(appType) {
|
||||||
getSandboxUrl('mailtrain/common.js'),
|
getSandboxUrl('mailtrain/common.js'),
|
||||||
getSandboxUrl('mailtrain/mosaico.js')
|
getSandboxUrl('mailtrain/mosaico.js')
|
||||||
],
|
],
|
||||||
mosaicoPublicPath: getSandboxUrl('static/mosaico')
|
publicPath: getSandboxUrl()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -136,7 +136,6 @@ const errorTypes = {
|
||||||
|
|
||||||
function deserialize(errorObj) {
|
function deserialize(errorObj) {
|
||||||
if (errorObj.type) {
|
if (errorObj.type) {
|
||||||
|
|
||||||
const ctor = errorTypes[errorObj.type];
|
const ctor = errorTypes[errorObj.type];
|
||||||
if (ctor) {
|
if (ctor) {
|
||||||
return new ctor(errorObj.message, errorObj.data);
|
return new ctor(errorObj.message, errorObj.data);
|
||||||
|
|
7532
shared/package-lock.json
generated
7532
shared/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -16,5 +16,21 @@
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
"moment-timezone": "^0.5.13",
|
"moment-timezone": "^0.5.13",
|
||||||
"owasp-password-strength-test": "github:bures/owasp-password-strength-test"
|
"owasp-password-strength-test": "github:bures/owasp-password-strength-test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.24.1",
|
||||||
|
"babel-loader": "^7.1.1",
|
||||||
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
|
"babel-plugin-transform-function-bind": "^6.22.0",
|
||||||
|
"babel-preset-env": "^1.7.0",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-preset-stage-1": "^6.24.1",
|
||||||
|
"css-loader": "^0.28.4",
|
||||||
|
"i18next-conv": "^3.0.3",
|
||||||
|
"node-sass": "^4.5.3",
|
||||||
|
"sass-loader": "^6.0.6",
|
||||||
|
"style-loader": "^0.18.2",
|
||||||
|
"url-loader": "^0.5.9",
|
||||||
|
"webpack": "^2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
37
views/ckeditor/layout.hbs
Normal file
37
views/ckeditor/layout.hbs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<!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
|
||||||
|
{{#if title}} | {{title}}{{/if}}
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{publicPath}}static/bootstrap/themes/united.min.css">
|
||||||
|
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>
|
||||||
|
<script src="{{publicPath}}static/bootstrap/js/bootstrap.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>
|
||||||
|
{{{body}}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
6
views/ckeditor/root.hbs
Normal file
6
views/ckeditor/root.hbs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
MailtrainReactBody.default();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -7,6 +7,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
|
<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="/static/favicon.ico">
|
<link rel="icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
<title>Mailtrain
|
<title>Mailtrain
|
||||||
|
|
|
@ -4,27 +4,27 @@
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
<meta name="viewport" content="width=1024, initial-scale=1">
|
<meta name="viewport" content="width=1024, initial-scale=1">
|
||||||
|
|
||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||||
|
|
||||||
<title>Mailtrain</title>
|
<title>Mailtrain</title>
|
||||||
|
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery.min.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery-migrate.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery-migrate.min.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/knockout.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/knockout.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery-ui.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery-ui.min.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.ui.touch-punch.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery.ui.touch-punch.min.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/load-image.all.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/load-image.all.min.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/canvas-to-blob.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/canvas-to-blob.min.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.iframe-transport.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery.iframe-transport.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-process.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload-process.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-image.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload-image.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/jquery.fileupload-validate.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/jquery.fileupload-validate.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/knockout-jqueryui.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/knockout-jqueryui.min.js"></script>
|
||||||
<script src="{{mosaicoPublicPath}}/vendor/tinymce.min.js"></script>
|
<script src="{{publicPath}}static/mosaico/vendor/tinymce.min.js"></script>
|
||||||
|
|
||||||
<script src="{{mosaicoPublicPath}}/mosaico.min.js?v=0.16"></script>
|
<script src="{{publicPath}}static/mosaico/mosaico.min.js?v=0.16"></script>
|
||||||
|
|
||||||
{{#if languageStrings}}<script> window.mosaicoLanguageStrings = {{{languageStrings}}}; </script>{{/if}}
|
{{#if languageStrings}}<script> window.mosaicoLanguageStrings = {{{languageStrings}}}; </script>{{/if}}
|
||||||
<script> window.mosaicoPlugins = []; </script>
|
<script> window.mosaicoPlugins = []; </script>
|
||||||
|
@ -45,13 +45,11 @@
|
||||||
<script src="{{this}}"></script>
|
<script src="{{this}}"></script>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{mosaicoPublicPath}}/mosaico-material.min.css?v=0.10" />
|
<link rel="stylesheet" href="{{publicPath}}static/mosaico/mosaico-material.min.css?v=0.10" />
|
||||||
<link rel="stylesheet" href="{{mosaicoPublicPath}}/vendor/notoregular/stylesheet.css" />
|
<link rel="stylesheet" href="{{publicPath}}static/mosaico/vendor/notoregular/stylesheet.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="mo-standalone">
|
<body class="mo-standalone">
|
||||||
{{{body}}}
|
{{{body}}}
|
||||||
|
|
||||||
{{> tracking_scripts}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue