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:
parent
a4ee1534cc
commit
7788b0bc67
79 changed files with 724 additions and 390 deletions
|
@ -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/>
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ...')}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue