Fixed sandbox. Multiple tabs work now.

WiP on selectable mosaico templates.

TODO: Make files always point to trusted URL, such that we don't have to rebase them. They are public anyway. The same goes for mosaico endpoints: /mosaico/templates and /mosaico/img
This commit is contained in:
Tomas Bures 2018-05-09 04:07:01 +02:00
parent a4ee1534cc
commit 7788b0bc67
79 changed files with 724 additions and 390 deletions

View file

@ -1,30 +1,42 @@
'use strict';
import React, { Component } from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page'
import {
withForm,
Trans,
translate
} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page'
import {
ACEEditor,
AlignedRow,
Button,
ButtonRow,
Dropdown,
Form,
FormSendMethod,
InputField,
StaticField,
TextArea,
Dropdown,
ACEEditor,
ButtonRow,
Button,
AlignedRow,
StaticField
withForm
} from '../lib/form';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
import {withErrorHandling} from '../lib/error-handling';
import {
NamespaceSelect,
validateNamespace
} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import { getTemplateTypes } from './helpers';
import {getTemplateTypes} from './helpers';
import {ActionLink} from "../lib/bootstrap-components";
import axios from '../lib/axios';
import styles from "../lib/styles.scss";
import {getUrl} from "../lib/urls";
@translate()
@ -43,7 +55,11 @@ export default class CUD extends Component {
elementInFullscreen: false
};
this.initForm();
this.initForm({
onChangeBeforeValidation: {
type: ::this.onTypeChanged
}
});
}
static propTypes = {
@ -52,9 +68,17 @@ export default class CUD extends Component {
entity: PropTypes.object
}
onTypeChanged(mutState, key, oldType, type) {
if (type) {
this.templateTypes[type].afterTypeChange(mutState);
}
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity);
this.getFormValuesFromEntity(this.props.entity, data => {
this.templateTypes[data.type].afterLoad(data);
});
} else {
this.populateFormValues({
name: '',
@ -63,7 +87,8 @@ export default class CUD extends Component {
type: mailtrainConfig.editors[0],
text: '',
html: '',
data: {}
data: {},
...this.templateTypes[mailtrainConfig.editors[0]].initData()
});
}
}
@ -77,13 +102,18 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], null);
}
if (!state.getIn(['type', 'value'])) {
const typeKey = state.getIn(['type', 'value']);
if (!typeKey) {
state.setIn(['type', 'error'], t('Type must be selected'));
} else {
state.setIn(['type', 'error'], null);
}
validateNamespace(t, state);
if (typeKey) {
this.templateTypes[typeKey].validate(state);
}
}
async submitHandler() {
@ -91,22 +121,23 @@ export default class CUD extends Component {
if (this.props.entity) {
const typeKey = this.getFormValue('type');
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
await this.templateTypes[typeKey].exportHTMLEditorData(this);
}
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/templates/${this.props.entity.id}`
url = `rest/templates/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/templates'
url = 'rest/templates'
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
this.templateTypes[data.type].beforeSave(data);
});
if (submitResponse) {
@ -123,7 +154,7 @@ export default class CUD extends Component {
async extractPlainText() {
const typeKey = this.getFormValue('type');
await this.templateTypes[typeKey].htmlEditorBeforeSave(this);
await this.templateTypes[typeKey].exportHTMLEditorData(this);
const html = this.getFormValue('html');
if (!html) {
@ -137,7 +168,7 @@ export default class CUD extends Component {
this.disableForm();
const response = await axios.post('/rest/html-to-text', { html });
const response = await axios.post(getUrl('rest/html-to-text', { html }));
this.updateFormValue('text', response.data.text);
@ -258,6 +289,14 @@ export default class CUD extends Component {
</div>
}
let typeForm = null;
if (typeKey) {
typeForm = <div>
{this.templateTypes[typeKey].getTypeForm(this, isEdit)}
</div>;
}
return (
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
@ -265,7 +304,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/templates/${this.props.entity.id}`}
deleteUrl={`rest/templates/${this.props.entity.id}`}
cudUrl={`/templates/${this.props.entity.id}/edit`}
listUrl="/templates"
deletingMsg={t('Deleting template ...')}
@ -287,6 +326,7 @@ export default class CUD extends Component {
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
}
{typeForm}
<NamespaceSelect/>

View file

@ -9,6 +9,7 @@ import { Table } from '../lib/table';
import axios from '../lib/axios';
import moment from 'moment';
import { getTemplateTypes } from './helpers';
import {checkPermissions} from "../lib/permissions";
@translate()
@withPageHelpers
@ -25,7 +26,7 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createTemplate']
@ -38,9 +39,7 @@ export default class List extends Component {
entityTypeId: 'mosaicoTemplate',
requiredOperations: ['view']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createTemplate,
@ -105,7 +104,7 @@ export default class List extends Component {
<Title>{t('Templates')}</Title>
<Table withHeader dataUrl="/rest/templates-table" columns={columns} />
<Table withHeader dataUrl="rest/templates-table" columns={columns} />
</div>
);
}

View file

@ -4,46 +4,161 @@ import React from "react";
import {
ACEEditor,
AlignedRow,
CKEditor
CKEditor,
TableSelect
} from "../lib/form";
import 'brace/mode/text';
import 'brace/mode/html'
import 'brace/mode/html';
import {MosaicoEditor, ResourceType} from "../lib/mosaico";
import {
MosaicoEditor,
ResourceType
} from "../lib/mosaico";
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
export function getTemplateTypes(t) {
const templateTypes = {};
function initFieldsIfMissing(mutState, templateType) {
const initVals = templateTypes[templateType].initData();
for (const key in initVals) {
if (!mutState.hasIn([key])) {
mutState.setIn([key, 'value'], initVals[key]);
}
}
}
function clearBeforeSave(data) {
for (const templateKey in templateTypes) {
const initVals = templateTypes[templateKey].initData();
for (const fieldKey in initVals) {
delete data[fieldKey];
}
}
}
const mosaicoTemplateTypes = getMosaicoTemplateTypes(t);
const mosaicoTemplatesColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => mosaicoTemplateTypes[data].typeName },
{ data: 5, title: t('Namespace') },
];
templateTypes.mosaico = {
typeName: t('Mosaico'),
getHTMLEditor: owner => <AlignedRow label={t('Template content (HTML)')}><MosaicoEditor ref={node => owner.editorNode = node} entity={owner.props.entity} entityTypeId={ResourceType.TEMPLATE} title={t('Mosaico Template Designer')} onFullscreenAsync={::owner.setElementInFullscreen}/></AlignedRow>,
htmlEditorBeforeSave: async owner => {
getTypeForm: (owner, isEdit) =>
<TableSelect id="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
ref={node => owner.editorNode = node}
entity={owner.props.entity}
initialModel={owner.getFormValue('mosaicoData').model}
initialMetadata={owner.getFormValue('mosaicoData').metadata}
templateId={owner.getFormValue('mosaicoTemplate')}
entityTypeId={ResourceType.TEMPLATE}
title={t('Mosaico Template Designer')}
onFullscreenAsync={::owner.setElementInFullscreen}/>
</AlignedRow>,
exportHTMLEditorData: async owner => {
const {html, metadata, model} = await owner.editorNode.exportState();
owner.updateFormValue('html', html);
owner.updateFormValue('data', {metadata, model});
owner.updateFormValue('mosaicoData', {
metadata,
model
});
},
initData: () => ({
mosaicoTemplate: '',
mosaicoData: {}
}),
afterLoad: data => {
data.mosaicoTemplate = data.data.mosaicoTemplate;
data.html = data.data.html;
data.mosaicoData = {
metadata: data.data.metadata,
model: data.data.model
};
},
beforeSave: data => {
data.data = {
mosaicoTemplate: data.mosaicoTemplate,
metadata: data.mosaicoData.metadata,
model: data.mosaicoData.model
};
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, 'mosaico');
},
validate: state => {
const mosaicoTemplate = state.getIn(['mosaicoTemplate', 'value']);
if (!mosaicoTemplate) {
state.setIn(['mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
} else {
state.setIn(['mosaicoTemplate', 'error'], null);
}
}
};
templateTypes.grapejs = {
typeName: t('GrapeJS')
templateTypes.grapejs = { // TODO
typeName: t('GrapeJS'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => null,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
templateTypes.ckeditor = {
typeName: t('CKEditor'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <CKEditor id="html" height="600px" label={t('Template content (HTML)')}/>,
htmlEditorBeforeSave: async owner => {}
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
templateTypes.codeeditor = {
typeName: t('Code Editor'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <ACEEditor id="html" height="600px" mode="html" label={t('Template content (HTML)')}/>,
htmlEditorBeforeSave: async owner => {}
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
templateTypes.mjml = {
typeName: t('MJML')
templateTypes.mjml = { // TODO
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => null,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
validate: state => {}
};
return templateTypes;

View file

@ -124,10 +124,10 @@ export default class CUD extends Component {
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `/rest/mosaico-templates/${this.props.entity.id}`
url = `rest/mosaico-templates/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = '/rest/mosaico-templates'
url = 'rest/mosaico-templates'
}
this.disableForm();
@ -139,7 +139,7 @@ export default class CUD extends Component {
if (submitSuccessful) {
if (stay) {
await this.getFormValuesFromURL(`/rest/mosaico-templates/${this.props.entity.id}`, data => {
await this.getFormValuesFromURL(`rest/mosaico-templates/${this.props.entity.id}`, data => {
this.templateTypes[data.type].afterLoad(data);
});
this.enableForm();
@ -170,7 +170,7 @@ export default class CUD extends Component {
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`/rest/templates/mosaico/${this.props.entity.id}`}
deleteUrl={`rest/mosaico-templates/${this.props.entity.id}`}
cudUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
listUrl="/templates/mosaico"
deletingMsg={t('Deleting Mosaico template ...')}

View file

@ -9,6 +9,7 @@ import { Table } from '../../lib/table';
import axios from '../../lib/axios';
import moment from 'moment';
import { getTemplateTypes } from './helpers';
import {checkPermissions} from "../../lib/permissions";
@translate()
@ -26,14 +27,12 @@ export default class List extends Component {
@withAsyncErrorHandler
async fetchPermissions() {
const request = {
const result = await checkPermissions({
createMosaicoTemplate: {
entityTypeId: 'namespace',
requiredOperations: ['createMosaicoTemplate']
}
};
const result = await axios.post('/rest/permissions-check', request);
});
this.setState({
createPermitted: result.data.createMosaicoTemplate
@ -90,7 +89,7 @@ export default class List extends Component {
<Title>{t('Mosaico Templates')}</Title>
<Table withHeader dataUrl="/rest/mosaico-templates-table" columns={columns} />
<Table withHeader dataUrl="rest/mosaico-templates-table" columns={columns} />
</div>
);
}

View file

@ -20,7 +20,7 @@ function getMenus(t) {
':templateId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.template.name}),
resolve: {
template: params => `/rest/templates/${params.templateId}`
template: params => `rest/templates/${params.templateId}`
},
link: params => `/templates/${params.templateId}/edit`,
navs: {
@ -56,7 +56,7 @@ function getMenus(t) {
':mosaiceTemplateId([0-9]+)': {
title: resolved => t('Mosaico Template "{{name}}"', {name: resolved.mosaicoTemplate.name}),
resolve: {
mosaicoTemplate: params => `/rest/mosaico-templates/${params.mosaiceTemplateId}`
mosaicoTemplate: params => `rest/mosaico-templates/${params.mosaiceTemplateId}`
},
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
navs: {