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:
Tomas Bures 2018-11-03 21:46:23 +01:00
parent 213039c141
commit eacdc74c29
43 changed files with 12499 additions and 1382 deletions

View file

@ -1,4 +0,0 @@
{
"presets": ["es2015", "stage-1"],
"plugins": ["transform-react-jsx", "transform-decorators-legacy", "transform-function-bind"]
}

5578
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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()

View file

@ -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()

View file

@ -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>

View 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
View 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}
/>
);
}
}

View file

@ -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>
}

View file

@ -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)}
/>
);
}

View file

@ -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);

View file

@ -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;

View 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')
);
};

View 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>
);
}
}

View file

@ -0,0 +1,10 @@
.editor {
.host {
}
}
.sandbox {
:global .ck-editor__editable {
height: 500px;
}
}

View file

@ -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() {

View file

@ -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();
};

View file

@ -121,6 +121,12 @@
color: #808080;
}
.dropZoneActive{
border-color: #90EE90;
color: #000;
background-color: #DDFFDD;
}
.untrustedContent {
border: 0px none;

View file

@ -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,

View file

@ -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`

View file

@ -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>

View file

@ -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: {

View file

@ -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`

View file

@ -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>

View file

@ -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 => {},

View file

@ -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'),