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
|
@ -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",
|
||||
"homepage": "https://mailtrain.org/",
|
||||
"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",
|
||||
"datatables.net": "^1.10.15",
|
||||
"datatables.net-bs": "^1.10.15",
|
||||
|
@ -28,7 +46,6 @@
|
|||
"querystringify": "^1.0.0",
|
||||
"react": "^15.6.1",
|
||||
"react-ace": "^5.1.0",
|
||||
"react-ckeditor-component": "^1.0.7",
|
||||
"react-day-picker": "^6.1.0",
|
||||
"react-dnd-html5-backend": "^2.4.1",
|
||||
"react-dnd-touch-backend": "^0.3.13",
|
||||
|
@ -41,16 +58,20 @@
|
|||
"url-parse": "^1.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-utils": "^11.0.1",
|
||||
"@ckeditor/ckeditor5-dev-webpack-plugin": "^7.0.1",
|
||||
"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-es2015": "^6.24.1",
|
||||
"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",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.18.2",
|
||||
"url-loader": "^0.5.9",
|
||||
|
|
|
@ -36,7 +36,8 @@ import {DeleteModalDialog} from "../lib/modals";
|
|||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {
|
||||
getTemplateTypes,
|
||||
getTypeForm
|
||||
getTypeForm,
|
||||
ResourceType
|
||||
} from '../templates/helpers';
|
||||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
|
@ -50,7 +51,6 @@ import {
|
|||
} from "../../../shared/campaigns";
|
||||
import moment from 'moment';
|
||||
import {getMailerTypes} from "../send-configurations/helpers";
|
||||
import {ResourceType} from "../lib/mosaico";
|
||||
import {getCampaignLabels} from "./helpers";
|
||||
|
||||
@translate()
|
||||
|
|
|
@ -21,12 +21,12 @@ import mailtrainConfig from 'mailtrainConfig';
|
|||
import {
|
||||
getEditForm,
|
||||
getTemplateTypes,
|
||||
getTypeForm
|
||||
getTypeForm,
|
||||
ResourceType
|
||||
} from '../templates/helpers';
|
||||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
import {getUrl} from "../lib/urls";
|
||||
import {ResourceType} from "../lib/mosaico";
|
||||
|
||||
|
||||
@translate()
|
||||
|
|
|
@ -58,7 +58,7 @@ export default class List extends Component {
|
|||
actions: data => {
|
||||
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({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
|
||||
|
@ -77,7 +77,7 @@ export default class List extends Component {
|
|||
return (
|
||||
<div>
|
||||
{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>
|
||||
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
|
||||
</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) &&
|
||||
<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')}
|
||||
</Dropzone>
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import ACEEditorRaw from 'react-ace';
|
|||
import 'brace/theme/github';
|
||||
import 'brace/ext/searchbox';
|
||||
|
||||
import CKEditorRaw from "react-ckeditor-component";
|
||||
import CKEditorRaw from './ckeditor';
|
||||
|
||||
import DayPicker from 'react-day-picker';
|
||||
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,
|
||||
<CKEditorRaw
|
||||
events={{
|
||||
"change": evt => owner.updateFormValue(id, evt.editor.getData())
|
||||
}}
|
||||
content={owner.getFormValue(id)}
|
||||
config={{width: '100%', height: props.height}}
|
||||
onChange={(event, editor) => owner.updateFormValue(id, editor.getData())}
|
||||
onInit={ editor => {
|
||||
editor.ui.view.editable.editableElement.style.height = props.height;
|
||||
} }
|
||||
data={owner.getFormValue(id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export class RestActionModalDialog extends Component {
|
|||
|
||||
async hideModal(isBack) {
|
||||
if (this.props.backUrl) {
|
||||
this.props.stateOwner.navigateTo(this.props.backUrl);
|
||||
this.navigateTo(this.props.backUrl);
|
||||
} else {
|
||||
if (isBack) {
|
||||
this.props.onBack();
|
||||
|
@ -68,7 +68,7 @@ export class RestActionModalDialog extends Component {
|
|||
await axios.method(props.actionMethod, getUrl(props.actionUrl));
|
||||
|
||||
if (props.successUrl) {
|
||||
owner.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
|
||||
this.navigateToWithFlashMessage(props.successUrl, 'success', props.actionDoneMsg);
|
||||
} else {
|
||||
props.onSuccess();
|
||||
this.setFlashMessage('success', props.actionDoneMsg);
|
||||
|
|
|
@ -345,7 +345,7 @@ class SectionContent extends Component {
|
|||
async closeFlashMessage() {
|
||||
this.setState({
|
||||
flashMessageText: ''
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
renderRoute(route) {
|
||||
|
@ -462,8 +462,12 @@ function requiresAuthenticatedUser(target) {
|
|||
const comp1 = withPageHelpers(target);
|
||||
|
||||
function comp2(props, context) {
|
||||
if (!new.target) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
context.sectionContent.ensureAuthenticated();
|
||||
comp1.call(this, props, context);
|
||||
return Reflect.construct(comp1, [props, context], new.target);
|
||||
}
|
||||
|
||||
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 {I18nextProvider,} from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
import {MosaicoSandbox} from './mosaico';
|
||||
import {MosaicoSandbox} from './sandboxed-mosaico';
|
||||
import {UntrustedContentRoot, parentRPC} from './untrusted';
|
||||
|
||||
export default function() {
|
|
@ -3,7 +3,7 @@
|
|||
import React, {Component} from 'react';
|
||||
import {translate} from 'react-i18next';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./mosaico.scss";
|
||||
import styles from "./sandboxed-mosaico.scss";
|
||||
|
||||
import {UntrustedContentHost, parentRPC} from './untrusted';
|
||||
import {Icon} from "./bootstrap-components";
|
||||
|
@ -18,13 +18,8 @@ import {
|
|||
} from "../../../shared/templates";
|
||||
|
||||
|
||||
export const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
export class MosaicoEditor extends Component {
|
||||
export class MosaicoEditorHost extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -59,7 +54,7 @@ export class MosaicoEditor extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const mosaicoData = {
|
||||
const editorData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id,
|
||||
templateId: this.props.templateId,
|
||||
|
@ -68,6 +63,11 @@ export class MosaicoEditor extends Component {
|
|||
initialMetadata: this.props.initialMetadata
|
||||
};
|
||||
|
||||
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}>
|
||||
|
@ -75,13 +75,13 @@ export class MosaicoEditor extends Component {
|
|||
<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={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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MosaicoEditor.prototype.exportState = async function() {
|
||||
MosaicoEditorHost.prototype.exportState = async function() {
|
||||
return await this.getWrappedInstance().exportState();
|
||||
};
|
||||
|
|
@ -121,6 +121,12 @@
|
|||
color: #808080;
|
||||
}
|
||||
|
||||
.dropZoneActive{
|
||||
border-color: #90EE90;
|
||||
color: #000;
|
||||
background-color: #DDFFDD;
|
||||
}
|
||||
|
||||
|
||||
.untrustedContent {
|
||||
border: 0px none;
|
||||
|
|
|
@ -16,7 +16,6 @@ import { withPageHelpers } from './page'
|
|||
import { withErrorHandling, withAsyncErrorHandler } from './error-handling';
|
||||
import styles from "./styles.scss";
|
||||
import {getUrl} from "./urls";
|
||||
import {Table} from "./table";
|
||||
|
||||
const TreeSelectMode = {
|
||||
NONE: 0,
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class List extends Component {
|
|||
const perms = data[9];
|
||||
const campaignId = data[8];
|
||||
|
||||
if (mailtrainConfig.globalPermissions.includes('setupAutomation') && perms.includes('manageTriggers')) {
|
||||
if (mailtrainConfig.globalPermissions.setupAutomation && perms.includes('manageTriggers')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
|
||||
|
|
|
@ -66,7 +66,7 @@ export default class List extends Component {
|
|||
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({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/lists/${this.props.list.id}/imports/${data[0]}/edit`
|
||||
|
@ -90,7 +90,7 @@ export default class List extends Component {
|
|||
return (
|
||||
<div>
|
||||
{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>
|
||||
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/>
|
||||
</Toolbar>
|
||||
|
|
|
@ -76,7 +76,7 @@ function getMenus(t) {
|
|||
':action(edit|delete)': {
|
||||
title: t('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} />
|
||||
},
|
||||
share: {
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class List extends Component {
|
|||
});
|
||||
|
||||
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 perms = data[5];
|
||||
|
||||
if (mailtrainConfig.globalPermissions.includes('createJavascriptWithROAccess') && perms.includes('edit')) {
|
||||
if (mailtrainConfig.globalPermissions.createJavascriptWithROAccess && perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/reports/templates/${data[0]}/edit`
|
||||
|
|
|
@ -84,9 +84,9 @@ class Root extends Component {
|
|||
<DropdownMenuItem label={t('Administration')}>
|
||||
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</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>
|
||||
{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>
|
||||
</DropdownMenuItem>
|
||||
</ul>
|
||||
|
|
|
@ -12,10 +12,8 @@ import {
|
|||
import 'brace/mode/text';
|
||||
import 'brace/mode/html';
|
||||
|
||||
import {
|
||||
MosaicoEditor,
|
||||
ResourceType
|
||||
} from "../lib/mosaico";
|
||||
import { MosaicoEditorHost } from "../lib/sandboxed-mosaico";
|
||||
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
|
||||
|
||||
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
||||
import {getSandboxUrl} from "../lib/urls";
|
||||
|
@ -28,6 +26,10 @@ import {Trans} from "react-i18next";
|
|||
|
||||
import styles from "../lib/styles.scss";
|
||||
|
||||
export const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEMPLATE) {
|
||||
// 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} />,
|
||||
getHTMLEditor: owner =>
|
||||
<AlignedRow label={t('Template content (HTML)')}>
|
||||
<MosaicoEditor
|
||||
<MosaicoEditorHost
|
||||
ref={node => owner.editorNode = node}
|
||||
entity={owner.props.entity}
|
||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||
|
@ -129,7 +131,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
},
|
||||
getHTMLEditor: owner =>
|
||||
<AlignedRow label={t('Template content (HTML)')}>
|
||||
<MosaicoEditor
|
||||
<MosaicoEditorHost
|
||||
ref={node => owner.editorNode = node}
|
||||
entity={owner.props.entity}
|
||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||
|
@ -172,7 +174,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.grapejs = { // TODO
|
||||
templateTypes.grapejs = { // FIXME
|
||||
typeName: t('GrapeJS'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => null,
|
||||
|
@ -186,10 +188,34 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
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 = {
|
||||
typeName: t('CKEditor'),
|
||||
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 => {},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
|
@ -214,7 +240,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.mjml = { // TODO
|
||||
templateTypes.mjml = { // FIXME
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => null,
|
||||
exportHTMLEditorData: async owner => {},
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
const webpack = require('webpack');
|
||||
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 = {
|
||||
plugins: [
|
||||
new CKEditorWebpackPlugin( {
|
||||
// See https://ckeditor.com/docs/ckeditor5/latest/features/ui-language.html
|
||||
language: 'en'
|
||||
} )
|
||||
],
|
||||
entry: {
|
||||
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: {
|
||||
library: 'MailtrainReactBody',
|
||||
|
@ -15,12 +26,51 @@ module.exports = {
|
|||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||
use: [ 'babel-loader' ]
|
||||
exclude: path.join(__dirname, 'node_modules'),
|
||||
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$/,
|
||||
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)$/,
|
||||
|
@ -32,7 +82,14 @@ 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$/,
|
||||
exclude: path.join(__dirname, 'node_modules'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue