Send test functionality for templates and campaigns
This commit is contained in:
parent
7e52000219
commit
2c73c536b7
22 changed files with 719 additions and 69 deletions
|
@ -27,6 +27,7 @@ import {
|
|||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
import {getUrl} from "../lib/urls";
|
||||
import {TestSendModalDialog} from "./TestSendModalDialog";
|
||||
|
||||
|
||||
@translate()
|
||||
|
@ -49,10 +50,13 @@ export default class CustomContent extends Component {
|
|||
|
||||
this.state = {
|
||||
showMergeTagReference: false,
|
||||
elementInFullscreen: false
|
||||
elementInFullscreen: false,
|
||||
showTestSendModal: false
|
||||
};
|
||||
|
||||
this.initForm();
|
||||
|
||||
this.sendModalGetDataHandler = ::this.sendModalGetData;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
|
@ -84,7 +88,7 @@ export default class CustomContent extends Component {
|
|||
const t = this.props.t;
|
||||
|
||||
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
||||
const exportedData = await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
||||
|
||||
const sendMethod = FormSendMethod.PUT;
|
||||
const url = `rest/campaigns-content/${this.props.entity.id}`;
|
||||
|
@ -93,6 +97,7 @@ export default class CustomContent extends Component {
|
|||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
Object.assign(data, exportedData);
|
||||
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
|
||||
|
||||
data.data.sourceCustom = {
|
||||
|
@ -123,9 +128,10 @@ export default class CustomContent extends Component {
|
|||
|
||||
async extractPlainText() {
|
||||
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
||||
const exportedData = await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
||||
|
||||
const html = exportedData.data_sourceCustom_html;
|
||||
|
||||
const html = this.getFormValue('data_sourceCustom_html');
|
||||
if (!html) {
|
||||
return;
|
||||
}
|
||||
|
@ -155,6 +161,22 @@ export default class CustomContent extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
showTestSendModal() {
|
||||
this.setState({
|
||||
showTestSendModal: true
|
||||
});
|
||||
}
|
||||
|
||||
async sendModalGetData() {
|
||||
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
|
||||
const exportedData = await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
|
||||
|
||||
return {
|
||||
html: exportedData.data_sourceCustom_html,
|
||||
text: this.getFormValue('data_sourceCustom_text')
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
|
@ -166,6 +188,13 @@ export default class CustomContent extends Component {
|
|||
|
||||
return (
|
||||
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
||||
<TestSendModalDialog
|
||||
visible={this.state.showTestSendModal}
|
||||
onHide={() => this.setState({showTestSendModal: false})}
|
||||
getDataAsync={this.sendModalGetDataHandler}
|
||||
entity={this.props.entity}
|
||||
/>
|
||||
|
||||
<Title>{t('Edit Custom Content')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
|
|
124
client/src/campaigns/TestSendModalDialog.js
Normal file
124
client/src/campaigns/TestSendModalDialog.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {translate} from 'react-i18next';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {ModalDialog} from "../lib/bootstrap-components";
|
||||
import {
|
||||
requiresAuthenticatedUser,
|
||||
withPageHelpers
|
||||
} from "../lib/page";
|
||||
import {
|
||||
Form,
|
||||
TableSelect,
|
||||
withForm
|
||||
} from "../lib/form";
|
||||
import {withErrorHandling} from "../lib/error-handling";
|
||||
import {getMailerTypes} from "../send-configurations/helpers";
|
||||
import axios from '../lib/axios';
|
||||
import {} from '../lib/urls';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export class TestSendModalDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.mailerTypes = getMailerTypes(props.t);
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
stateOwner: PropTypes.object,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
onHide: PropTypes.func.isRequired,
|
||||
getDataAsync: PropTypes.func.isRequired,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.populateFormValues({
|
||||
testUser: null,
|
||||
});
|
||||
}
|
||||
|
||||
async hideModal() {
|
||||
this.props.onHide();
|
||||
}
|
||||
|
||||
async performAction() {
|
||||
const props = this.props;
|
||||
const t = props.t;
|
||||
|
||||
if (this.isFormWithoutErrors()) {
|
||||
|
||||
try {
|
||||
this.hideFormValidation();
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Sending test email'));
|
||||
|
||||
const data = await this.props.getDataAsync();
|
||||
|
||||
const campaignCid = props.entity.cid;
|
||||
const [listCid, subscriptionCid] = this.getFormValue('testUser').split(':');
|
||||
|
||||
data.listCid = listCid;
|
||||
data.subscriptionCid = subscriptionCid;
|
||||
data.sendConfigurationId = props.entity.send_configuration;
|
||||
data.campaignId = props.entity.id;
|
||||
|
||||
console.log(await axios.post(getUrl('rest/template-test-send'), data));
|
||||
|
||||
this.clearFormStatusMessage();
|
||||
|
||||
this.enableForm();
|
||||
await this.hideModal();
|
||||
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
this.showFormValidation();
|
||||
}
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
if (!state.getIn(['testUser', 'value'])) {
|
||||
state.setIn(['testUser', 'error'], t('Subscription has to be selected.'))
|
||||
} else {
|
||||
state.setIn(['testUser', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const testUsersColumns = [
|
||||
{ data: 1, title: t('Email') },
|
||||
{ data: 2, title: t('Subscription ID'), render: data => <code>{data}</code> },
|
||||
{ data: 3, title: t('List ID'), render: data => <code>{data}</code> },
|
||||
{ data: 4, title: t('List') },
|
||||
{ data: 5, title: t('List namespace') }
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalDialog hidden={!this.props.visible} title={t('Send Test Email')} onCloseAsync={() => this.hideModal()} buttons={[
|
||||
{ label: t('Send'), className: 'btn-danger', onClickAsync: ::this.performAction },
|
||||
{ label: t('Cancel'), className: 'btn-primary', onClickAsync: ::this.hideModal }
|
||||
]}>
|
||||
<Form stateOwner={this} format="wide">
|
||||
<TableSelect id="testUser" format="wide" label={t('Subscription')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.entity.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
|
||||
</Form>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
}
|
9
client/src/lib/bootstrap-components.js
vendored
9
client/src/lib/bootstrap-components.js
vendored
|
@ -210,7 +210,8 @@ class ModalDialog extends Component {
|
|||
onCloseAsync: PropTypes.func,
|
||||
onButtonClickAsync: PropTypes.func,
|
||||
buttons: PropTypes.array,
|
||||
hidden: PropTypes.bool
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -281,7 +282,11 @@ class ModalDialog extends Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div ref={(domElem) => { this.domModal = domElem; }} className="modal fade" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
<div
|
||||
ref={(domElem) => { this.domModal = domElem; }}
|
||||
className={'modal fade' + (props.className ? ' ' + props.className : '')}
|
||||
tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
|
|
|
@ -43,25 +43,32 @@ class CKEditorSandbox extends Component {
|
|||
const trustedUrlBase = getTrustedUrl();
|
||||
const sandboxUrlBase = getSandboxUrl();
|
||||
const publicUrlBase = getPublicUrl();
|
||||
const html = this.props.initialHtml && base(this.props.initialHtml, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
||||
const source = this.props.initialSource && base(this.props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
||||
|
||||
this.state = {
|
||||
html
|
||||
source
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entityTypeId: PropTypes.string,
|
||||
entityId: PropTypes.number,
|
||||
initialHtml: PropTypes.string
|
||||
initialSource: PropTypes.string
|
||||
}
|
||||
|
||||
async exportState(method, params) {
|
||||
const trustedUrlBase = getTrustedUrl();
|
||||
const sandboxUrlBase = getSandboxUrl();
|
||||
const publicUrlBase = getPublicUrl();
|
||||
|
||||
const preHtml = '<!doctype html><html><head><meta charset="utf-8"><title></title></head><body>';
|
||||
const postHtml = '</body></html>';
|
||||
|
||||
const unbasedSource = unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true);
|
||||
|
||||
return {
|
||||
html: unbase(this.state.html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
|
||||
source: unbasedSource,
|
||||
html: preHtml + unbasedSource + postHtml
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -119,9 +126,9 @@ class CKEditorSandbox extends Component {
|
|||
return (
|
||||
<div className={styles.sandbox}>
|
||||
<CKEditor ref={node => this.node = node}
|
||||
content={this.state.html}
|
||||
content={this.state.source}
|
||||
events={{
|
||||
change: evt => this.setState({html: evt.editor.getData()}),
|
||||
change: evt => this.setState({source: evt.editor.getData()}),
|
||||
}}
|
||||
config={config}
|
||||
/>
|
||||
|
|
|
@ -29,8 +29,9 @@ export class CKEditorHost extends Component {
|
|||
static propTypes = {
|
||||
entityTypeId: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
initialHtml: PropTypes.string,
|
||||
initialSource: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onTestSend: PropTypes.func,
|
||||
onFullscreenAsync: PropTypes.func
|
||||
}
|
||||
|
||||
|
@ -75,7 +76,7 @@ export class CKEditorHost extends Component {
|
|||
const editorData = {
|
||||
entityTypeId: this.props.entityTypeId,
|
||||
entityId: this.props.entity.id,
|
||||
initialHtml: this.props.initialHtml
|
||||
initialSource: this.props.initialSource
|
||||
};
|
||||
|
||||
const tokenData = {
|
||||
|
@ -89,6 +90,7 @@ export class CKEditorHost extends Component {
|
|||
{this.state.fullscreen && <img className={styles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
|
||||
<div className={styles.title}>{this.props.title}</div>
|
||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||
<a className={styles.btn} onClick={this.props.onTestSend}><Icon icon="send"/></a>
|
||||
</div>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="ckeditor/editor" tokenMethod="ckeditor" tokenParams={editorData}/>
|
||||
</div>
|
||||
|
|
|
@ -39,6 +39,8 @@ import {CodeEditorSourceType} from "./sandboxed-codeeditor-shared";
|
|||
import mjml2html from "mjml4-in-browser";
|
||||
import juice from "juice";
|
||||
|
||||
const refreshTimeout = 1000;
|
||||
|
||||
@translate(null, { withRef: true })
|
||||
class CodeEditorSandbox extends Component {
|
||||
constructor(props) {
|
||||
|
@ -85,6 +87,12 @@ class CodeEditorSandbox extends Component {
|
|||
source,
|
||||
preview: props.initialPreview
|
||||
};
|
||||
this.state.previewContents = this.getHtml();
|
||||
|
||||
this.onCodeChangedHandler = ::this.onCodeChanged;
|
||||
|
||||
this.refreshHandler = ::this.refresh;
|
||||
this.refreshTimeoutId = null;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
|
@ -100,6 +108,7 @@ class CodeEditorSandbox extends Component {
|
|||
const sandboxUrlBase = getSandboxUrl();
|
||||
const publicUrlBase = getPublicUrl();
|
||||
return {
|
||||
html: unbase(this.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
|
||||
source: unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
|
||||
};
|
||||
}
|
||||
|
@ -115,9 +124,12 @@ class CodeEditorSandbox extends Component {
|
|||
parentRPC.setMethodHandler('setPreview', ::this.setPreview);
|
||||
}
|
||||
|
||||
render() {
|
||||
let previewContents;
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.refreshTimeoutId);
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
let previewContents;
|
||||
if (this.props.sourceType === CodeEditorSourceType.MJML) {
|
||||
const res = mjml2html(this.state.source);
|
||||
previewContents = res.html;
|
||||
|
@ -125,6 +137,28 @@ class CodeEditorSandbox extends Component {
|
|||
previewContents = juice(this.state.source);
|
||||
}
|
||||
|
||||
return previewContents;
|
||||
}
|
||||
|
||||
onCodeChanged(data) {
|
||||
this.setState({
|
||||
source: data
|
||||
});
|
||||
|
||||
if (!this.refreshTimeoutId) {
|
||||
this.refreshTimeoutId = setTimeout(() => this.refresh(), refreshTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.refreshTimeoutId = null;
|
||||
|
||||
this.setState({
|
||||
previewContents: this.getHtml()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.sandbox}>
|
||||
<div className={this.state.preview ? styles.aceEditorWithPreview : styles.aceEditorWithoutPreview}>
|
||||
|
@ -133,7 +167,7 @@ class CodeEditorSandbox extends Component {
|
|||
theme="github"
|
||||
width="100%"
|
||||
height="100%"
|
||||
onChange={data => this.setState({source: data})}
|
||||
onChange={this.onCodeChangedHandler}
|
||||
fontSize={12}
|
||||
showPrintMargin={false}
|
||||
value={this.state.source}
|
||||
|
@ -144,7 +178,7 @@ class CodeEditorSandbox extends Component {
|
|||
{
|
||||
this.state.preview &&
|
||||
<div className={styles.preview}>
|
||||
<iframe src={"data:text/html;charset=utf-8," + escape(previewContents)}></iframe>
|
||||
<iframe src={"data:text/html;charset=utf-8," + escape(this.state.previewContents)}></iframe>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,7 @@ export class CodeEditorHost extends Component {
|
|||
initialSource: PropTypes.string,
|
||||
sourceType: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onTestSend: PropTypes.func,
|
||||
onFullscreenAsync: PropTypes.func
|
||||
}
|
||||
|
||||
|
@ -47,6 +48,7 @@ export class CodeEditorHost extends Component {
|
|||
|
||||
await this.contentNode.ask('setPreview', preview);
|
||||
}
|
||||
|
||||
async exportState() {
|
||||
return await this.contentNode.ask('exportState');
|
||||
}
|
||||
|
@ -73,6 +75,7 @@ export class CodeEditorHost extends Component {
|
|||
{this.state.fullscreen && <img className={styles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
|
||||
<div className={styles.title}>{this.props.title}</div>
|
||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||
<a className={styles.btn} onClick={this.props.onTestSend}><Icon icon="send"/></a>
|
||||
<a className={styles.btn} onClick={::this.togglePreviewAsync}><Icon icon={this.state.preview ? 'eye-close': 'eye-open'}/></a>
|
||||
</div>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="codeeditor/editor" tokenMethod="codeeditor" tokenParams={tokenData}/>
|
||||
|
|
|
@ -107,7 +107,11 @@ export class GrapesJSSandbox extends Component {
|
|||
const commandManager = editor.Commands;
|
||||
|
||||
const cmdGetCode = commandManager.get('gjs-get-inlined-html');
|
||||
html = cmdGetCode.run(editor);
|
||||
const htmlBody = cmdGetCode.run(editor);
|
||||
|
||||
const preHtml = '<!doctype html><html><head><meta charset="utf-8"><title></title></head><body>';
|
||||
const postHtml = '</body></html>';
|
||||
html = preHtml + unbase(htmlBody, trustedUrlBase, sandboxUrlBase, publicUrlBase, true) + postHtml;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export class GrapesJSHost extends Component {
|
|||
initialStyle: PropTypes.string,
|
||||
sourceType: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onTestSend: PropTypes.func,
|
||||
onFullscreenAsync: PropTypes.func
|
||||
}
|
||||
|
||||
|
@ -65,6 +66,7 @@ export class GrapesJSHost extends Component {
|
|||
{this.state.fullscreen && <img className={styles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
|
||||
<div className={styles.title}>{this.props.title}</div>
|
||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||
<a className={styles.btn} onClick={this.props.onTestSend}><Icon icon="send"/></a>
|
||||
</div>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="grapesjs/editor" tokenMethod="grapesjs" tokenParams={tokenData}/>
|
||||
</div>
|
||||
|
|
|
@ -26,6 +26,7 @@ export class MosaicoHost extends Component {
|
|||
entityTypeId: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
title: PropTypes.string,
|
||||
onTestSend: PropTypes.func,
|
||||
onFullscreenAsync: PropTypes.func,
|
||||
templateId: PropTypes.number,
|
||||
templatePath: PropTypes.string,
|
||||
|
@ -68,6 +69,7 @@ export class MosaicoHost extends Component {
|
|||
{this.state.fullscreen && <img className={styles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
|
||||
<div className={styles.title}>{this.props.title}</div>
|
||||
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||
<a className={styles.btn} onClick={this.props.onTestSend}><Icon icon="send"/></a>
|
||||
</div>
|
||||
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="mosaico/editor" tokenMethod="mosaico" tokenParams={tokenData}/>
|
||||
</div>
|
||||
|
|
|
@ -146,3 +146,15 @@
|
|||
.dependenciesList {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
:global .modal-dialog {
|
||||
@media (min-width: 768px) {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
import axios from '../lib/axios';
|
||||
import styles from "../lib/styles.scss";
|
||||
import {getUrl} from "../lib/urls";
|
||||
import {TestSendModalDialog} from "./TestSendModalDialog";
|
||||
|
||||
|
||||
@translate()
|
||||
|
@ -50,7 +51,8 @@ export default class CUD extends Component {
|
|||
|
||||
this.state = {
|
||||
showMergeTagReference: false,
|
||||
elementInFullscreen: false
|
||||
elementInFullscreen: false,
|
||||
showTestSendModal: false
|
||||
};
|
||||
|
||||
this.initForm({
|
||||
|
@ -58,6 +60,8 @@ export default class CUD extends Component {
|
|||
type: ::this.onTypeChanged
|
||||
}
|
||||
});
|
||||
|
||||
this.sendModalGetDataHandler = ::this.sendModalGetData;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
|
@ -117,9 +121,10 @@ export default class CUD extends Component {
|
|||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
let exportedData = {};
|
||||
if (this.props.entity) {
|
||||
const typeKey = this.getFormValue('type');
|
||||
await this.templateTypes[typeKey].exportHTMLEditorData(this);
|
||||
exportedData = await this.templateTypes[typeKey].exportHTMLEditorData(this);
|
||||
}
|
||||
|
||||
let sendMethod, url;
|
||||
|
@ -135,6 +140,7 @@ export default class CUD extends Component {
|
|||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
Object.assign(data, exportedData);
|
||||
this.templateTypes[data.type].beforeSave(data);
|
||||
});
|
||||
|
||||
|
@ -152,9 +158,9 @@ export default class CUD extends Component {
|
|||
|
||||
async extractPlainText() {
|
||||
const typeKey = this.getFormValue('type');
|
||||
await this.templateTypes[typeKey].exportHTMLEditorData(this);
|
||||
const exportedData = await this.templateTypes[typeKey].exportHTMLEditorData(this);
|
||||
|
||||
const html = this.getFormValue('html');
|
||||
const html = exportedData.html;
|
||||
if (!html) {
|
||||
return;
|
||||
}
|
||||
|
@ -184,6 +190,22 @@ export default class CUD extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
showTestSendModal() {
|
||||
this.setState({
|
||||
showTestSendModal: true
|
||||
});
|
||||
}
|
||||
|
||||
async sendModalGetData() {
|
||||
const typeKey = this.getFormValue('type');
|
||||
const exportedData = await this.templateTypes[typeKey].exportHTMLEditorData(this);
|
||||
|
||||
return {
|
||||
html: exportedData.html,
|
||||
text: this.getFormValue('text')
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
|
@ -194,8 +216,6 @@ export default class CUD extends Component {
|
|||
typeOptions.push({key, label: this.templateTypes[key].typeName});
|
||||
}
|
||||
|
||||
// TODO: Toggle HTML preview
|
||||
|
||||
const typeKey = this.getFormValue('type');
|
||||
|
||||
let editForm = null;
|
||||
|
@ -211,6 +231,12 @@ export default class CUD extends Component {
|
|||
|
||||
return (
|
||||
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
||||
{isEdit &&
|
||||
<TestSendModalDialog
|
||||
visible={this.state.showTestSendModal}
|
||||
onHide={() => this.setState({showTestSendModal: false})}
|
||||
getDataAsync={this.sendModalGetDataHandler}/>
|
||||
}
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
|
@ -246,6 +272,7 @@ export default class CUD extends Component {
|
|||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={isEdit ? t('Save') : t('Save and edit template')}/>
|
||||
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/templates/${this.props.entity.id}/delete`}/> }
|
||||
{isEdit && <Button className="btn-danger" icon="send" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/> }
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
153
client/src/templates/TestSendModalDialog.js
Normal file
153
client/src/templates/TestSendModalDialog.js
Normal file
|
@ -0,0 +1,153 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {translate} from 'react-i18next';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {ModalDialog} from "../lib/bootstrap-components";
|
||||
import {
|
||||
requiresAuthenticatedUser,
|
||||
withPageHelpers
|
||||
} from "../lib/page";
|
||||
import {
|
||||
Form,
|
||||
TableSelect,
|
||||
withForm
|
||||
} from "../lib/form";
|
||||
import {withErrorHandling} from "../lib/error-handling";
|
||||
import moment
|
||||
from "moment";
|
||||
import {getMailerTypes} from "../send-configurations/helpers";
|
||||
import axios from '../lib/axios';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export class TestSendModalDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.mailerTypes = getMailerTypes(props.t);
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
stateOwner: PropTypes.object,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
onHide: PropTypes.func.isRequired,
|
||||
getDataAsync: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.populateFormValues({
|
||||
list: null,
|
||||
testUser: null,
|
||||
sendConfiguration: null
|
||||
});
|
||||
}
|
||||
|
||||
async hideModal() {
|
||||
this.props.onHide();
|
||||
}
|
||||
|
||||
async performAction() {
|
||||
const props = this.props;
|
||||
const t = props.t;
|
||||
|
||||
if (this.isFormWithoutErrors()) {
|
||||
|
||||
try {
|
||||
this.hideFormValidation();
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Sending test email'));
|
||||
|
||||
const data = await this.props.getDataAsync();
|
||||
data.listCid = this.getFormValue('list');
|
||||
data.subscriptionCid = this.getFormValue('testUser');
|
||||
data.sendConfigurationId = this.getFormValue('sendConfiguration');
|
||||
|
||||
console.log(await axios.post(getUrl('rest/template-test-send'), data));
|
||||
|
||||
this.clearFormStatusMessage();
|
||||
|
||||
this.enableForm();
|
||||
await this.hideModal();
|
||||
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
this.showFormValidation();
|
||||
}
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
if (!state.getIn(['sendConfiguration', 'value'])) {
|
||||
state.setIn(['sendConfiguration', 'error'], t('Send configuration has to be selected.'))
|
||||
} else {
|
||||
state.setIn(['sendConfiguration', 'error'], null);
|
||||
}
|
||||
|
||||
if (!state.getIn(['list', 'value'])) {
|
||||
state.setIn(['list', 'error'], t('List has to be selected.'))
|
||||
} else {
|
||||
state.setIn(['list', 'error'], null);
|
||||
}
|
||||
|
||||
if (!state.getIn(['testUser', 'value'])) {
|
||||
state.setIn(['testUser', 'error'], t('Subscription has to be selected.'))
|
||||
} else {
|
||||
state.setIn(['testUser', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const listId = this.getFormValue('list');
|
||||
|
||||
const testUsersColumns = [
|
||||
{ data: 1, title: t('Subscription ID'), render: data => <code>{data}</code> },
|
||||
{ data: 2, title: t('Email') }
|
||||
];
|
||||
|
||||
const listsColumns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
|
||||
{ data: 3, title: t('Subscribers') },
|
||||
{ data: 4, title: t('Description') },
|
||||
{ data: 5, title: t('Namespace') }
|
||||
];
|
||||
|
||||
const sendConfigurationsColumns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
|
||||
{ data: 3, title: t('Description') },
|
||||
{ data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
|
||||
{ data: 5, title: t('Created'), render: data => moment(data).fromNow() },
|
||||
{ data: 6, title: t('Namespace') }
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalDialog hidden={!this.props.visible} title={t('Send Test Email')} onCloseAsync={() => this.hideModal()} buttons={[
|
||||
{ label: t('Send'), className: 'btn-danger', onClickAsync: ::this.performAction },
|
||||
{ label: t('Cancel'), className: 'btn-primary', onClickAsync: ::this.hideModal }
|
||||
]}>
|
||||
<Form stateOwner={this} format="wide">
|
||||
<TableSelect id="sendConfiguration" format="wide" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-with-send-permission-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
|
||||
<TableSelect id="list" format="wide" label={t('List')} withHeader dropdown dataUrl={`rest/lists-table`} columns={listsColumns} selectionKeyIndex={2} selectionLabelIndex={1} />
|
||||
{ listId &&
|
||||
<TableSelect id="testUser" format="wide" label={t('Subscription')} withHeader dropdown dataUrl={`rest/subscriptions-test-user-table/${listId}`} columns={testUsersColumns} selectionKeyIndex={1} selectionLabelIndex={2} />
|
||||
}
|
||||
</Form>
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,7 +29,11 @@ import {
|
|||
} from "../lib/sandboxed-codeeditor-shared";
|
||||
|
||||
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
||||
import {getSandboxUrl} from "../lib/urls";
|
||||
import {
|
||||
getPublicUrl,
|
||||
getSandboxUrl,
|
||||
getTrustedUrl
|
||||
} from "../lib/urls";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {
|
||||
ActionLink,
|
||||
|
@ -38,6 +42,10 @@ import {
|
|||
import {Trans} from "react-i18next";
|
||||
|
||||
import styles from "../lib/styles.scss";
|
||||
import {
|
||||
base,
|
||||
unbase
|
||||
} from "../../../shared/templates";
|
||||
|
||||
export const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
|
@ -90,15 +98,19 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
templateId={owner.getFormValue(prefix + 'mosaicoTemplate')}
|
||||
entityTypeId={entityTypeId}
|
||||
title={t('Mosaico Template Designer')}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}/>
|
||||
onTestSend={::owner.showTestSendModal}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}
|
||||
/>
|
||||
</AlignedRow>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const {html, metadata, model} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue(prefix + 'html', html);
|
||||
owner.updateFormValue(prefix + 'mosaicoData', {
|
||||
return {
|
||||
[prefix + 'html']: html,
|
||||
[prefix + 'mosaicoData']: {
|
||||
metadata,
|
||||
model
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
initData: () => ({
|
||||
[prefix + 'mosaicoTemplate']: '',
|
||||
|
@ -152,15 +164,19 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
templatePath={getSandboxUrl(`static/mosaico/templates/${owner.getFormValue(prefix + 'mosaicoFsTemplate')}/index.html`)}
|
||||
entityTypeId={entityTypeId}
|
||||
title={t('Mosaico Template Designer')}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}/>
|
||||
onTestSend={::owner.showTestSendModal}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}
|
||||
/>
|
||||
</AlignedRow>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const {html, metadata, model} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue(prefix + 'html', html);
|
||||
owner.updateFormValue(prefix + 'mosaicoData', {
|
||||
return {
|
||||
[prefix + 'html']: html,
|
||||
[prefix + 'mosaicoData']: {
|
||||
metadata,
|
||||
model
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
initData: () => ({
|
||||
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0].key,
|
||||
|
@ -213,16 +229,19 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
initialStyle={owner.getFormValue(prefix + 'grapesJSData').style}
|
||||
sourceType={owner.getFormValue(prefix + 'grapesJSSourceType')}
|
||||
title={t('GrapesJS Template Designer')}
|
||||
onTestSend={::owner.showTestSendModal}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}
|
||||
/>
|
||||
</AlignedRow>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const {html, source, style} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue(prefix + 'html', html);
|
||||
owner.updateFormValue(prefix + 'grapesJSData', {
|
||||
return {
|
||||
[prefix + 'html']: html,
|
||||
[prefix + 'grapesJSData']: {
|
||||
source,
|
||||
style
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
initData: () => ({
|
||||
[prefix + 'grapesJSSourceType']: GrapesJSSourceType.MJML,
|
||||
|
@ -257,36 +276,82 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
<CKEditorHost
|
||||
ref={node => owner.editorNode = node}
|
||||
entity={owner.props.entity}
|
||||
initialHtml={owner.getFormValue(prefix + 'html')}
|
||||
initialSource={owner.getFormValue(prefix + 'ckeditor4Data').source}
|
||||
entityTypeId={entityTypeId}
|
||||
title={t('CKEditor 4 Template Designer')}
|
||||
onTestSend={::owner.showTestSendModal}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}
|
||||
/>
|
||||
</AlignedRow>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const {html} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue(prefix + 'html', html);
|
||||
const {html, source} = await owner.editorNode.exportState();
|
||||
return {
|
||||
[prefix + 'html']: html,
|
||||
[prefix + 'ckeditor4Data']: {
|
||||
source
|
||||
}
|
||||
};
|
||||
},
|
||||
initData: () => ({
|
||||
[prefix + 'ckeditor4Data']: {}
|
||||
}),
|
||||
afterLoad: data => {
|
||||
data[prefix + 'ckeditor4Data'] = {
|
||||
source: data[prefix + 'data'].source
|
||||
};
|
||||
},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
beforeSave: data => {
|
||||
data[prefix + 'data'] = {
|
||||
source: data[prefix + 'ckeditor4Data'].source,
|
||||
};
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, 'ckeditor4');
|
||||
},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
templateTypes.ckeditor5 = {
|
||||
typeName: t('CKEditor 5'),
|
||||
getTypeForm: (owner, isEdit) => null,
|
||||
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
|
||||
exportHTMLEditorData: async owner => {},
|
||||
initData: () => ({}),
|
||||
afterLoad: data => {},
|
||||
getHTMLEditor: owner => <CKEditor id={prefix + 'ckeditor5Source'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const preHtml = '<!doctype html><html><head><meta charset="utf-8"><title></title></head><body>';
|
||||
const postHtml = '</body></html>';
|
||||
|
||||
const trustedUrlBase = getTrustedUrl();
|
||||
const sandboxUrlBase = getSandboxUrl();
|
||||
const publicUrlBase = getPublicUrl();
|
||||
|
||||
const unbasedSource = unbase(owner.getFormValue(prefix + 'ckeditor5Source'), trustedUrlBase, sandboxUrlBase, publicUrlBase, true);
|
||||
const html = preHtml + unbasedSource + postHtml
|
||||
|
||||
return {
|
||||
[prefix + 'ckeditor5Source']: unbasedSource,
|
||||
[prefix + 'html']: html
|
||||
}
|
||||
},
|
||||
initData: () => ({
|
||||
[prefix + 'ckeditor5Source']: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
const trustedUrlBase = getTrustedUrl();
|
||||
const sandboxUrlBase = getSandboxUrl();
|
||||
const publicUrlBase = getPublicUrl();
|
||||
const source = base(data[prefix + 'data'].source, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
||||
|
||||
data[prefix + 'ckeditor5Source'] = source;
|
||||
},
|
||||
beforeSave: data => {
|
||||
data[prefix + 'data'] = {
|
||||
source: data[prefix + 'ckeditor5Source'],
|
||||
};
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, 'ckeditor5');
|
||||
},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
|
@ -315,15 +380,18 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
|||
initialSource={owner.getFormValue(prefix + 'codeEditorData').source}
|
||||
sourceType={owner.getFormValue(prefix + 'codeEditorSourceType')}
|
||||
title={t('Code Editor Template Designer')}
|
||||
onTestSend={::owner.showTestSendModal}
|
||||
onFullscreenAsync={::owner.setElementInFullscreen}
|
||||
/>
|
||||
</AlignedRow>,
|
||||
exportHTMLEditorData: async owner => {
|
||||
const {html, source} = await owner.editorNode.exportState();
|
||||
owner.updateFormValue(prefix + 'html', html);
|
||||
owner.updateFormValue(prefix + 'codeEditorData', {
|
||||
return {
|
||||
[prefix + 'html']: html,
|
||||
[prefix + 'codeEditorData']: {
|
||||
source
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
initData: () => ({
|
||||
[prefix + 'codeEditorSourceType']: CodeEditorSourceType.HTML,
|
||||
|
|
|
@ -26,6 +26,126 @@ class CampaignSender {
|
|||
constructor() {
|
||||
}
|
||||
|
||||
static async testSend(context, listCid, subscriptionCid, campaignId, sendConfigurationId, html, text) {
|
||||
let sendConfiguration, list, fieldsGrouped, campaign, subscriptionGrouped, useVerp, useVerpSenderHeader, mergeTags, attachments;
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
sendConfiguration = await sendConfigurations.getByIdTx(tx, context, sendConfigurationId, false, true);
|
||||
list = await lists.getByCidTx(tx, context, listCid);
|
||||
fieldsGrouped = await fields.listGroupedTx(tx, list.id);
|
||||
|
||||
useVerp = config.verp.enabled && sendConfiguration.verp_hostname;
|
||||
useVerpSenderHeader = this.useVerp && config.verp.disablesenderheader !== true;
|
||||
|
||||
subscriptionGrouped = await subscriptions.getByCid(context, list.id, subscriptionCid);
|
||||
mergeTags = fields.getMergeTags(fieldsGrouped, subscriptionGrouped);
|
||||
|
||||
if (campaignId) {
|
||||
campaign = await campaigns.getByIdTx(tx, context, campaignId, false, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
||||
} else {
|
||||
// This is to fake the campaign for getMessageLinks, which is called inside formatMessage
|
||||
campaign = {
|
||||
cid: '[CAMPAIGN_ID]'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const encryptionKeys = [];
|
||||
for (const fld of fieldsGrouped) {
|
||||
if (fld.type === 'gpg' && mergeTags[fld.key]) {
|
||||
encryptionKeys.push(mergeTags[fld.key].trim());
|
||||
}
|
||||
}
|
||||
|
||||
attachments = [];
|
||||
// replace data: images with embedded attachments
|
||||
html = html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||
const cid = shortid.generate() + '-attachments';
|
||||
attachments.push({
|
||||
path: dataUri,
|
||||
cid
|
||||
});
|
||||
return prefix + 'cid:' + cid;
|
||||
});
|
||||
|
||||
html = tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, false, true);
|
||||
|
||||
text = (text || '').trim()
|
||||
? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text)
|
||||
: htmlToText.fromString(html, {wordwrap: 130});
|
||||
|
||||
|
||||
const mailer = await mailers.getOrCreateMailer(sendConfiguration.id);
|
||||
|
||||
const getOverridable = key => {
|
||||
return sendConfiguration[key];
|
||||
}
|
||||
|
||||
const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.');
|
||||
|
||||
const mail = {
|
||||
from: {
|
||||
name: getOverridable('from_name'),
|
||||
address: getOverridable('from_email')
|
||||
},
|
||||
replyTo: getOverridable('reply_to'),
|
||||
xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
|
||||
to: {
|
||||
name: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false, false),
|
||||
address: subscriptionGrouped.email
|
||||
},
|
||||
sender: this.useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
|
||||
|
||||
envelope: this.useVerp ? {
|
||||
from: campaignAddress + '@' + sendConfiguration.verp_hostname,
|
||||
to: subscriptionGrouped.email
|
||||
} : false,
|
||||
|
||||
headers: {
|
||||
'x-fbl': campaignAddress,
|
||||
// custom header for SparkPost
|
||||
'x-msys-api': JSON.stringify({
|
||||
campaign_id: campaignAddress
|
||||
}),
|
||||
// custom header for SendGrid
|
||||
'x-smtpapi': JSON.stringify({
|
||||
unique_args: {
|
||||
campaign_id: campaignAddress
|
||||
}
|
||||
}),
|
||||
// custom header for Mailgun
|
||||
'x-mailgun-variables': JSON.stringify({
|
||||
campaign_id: campaignAddress
|
||||
}),
|
||||
'List-ID': {
|
||||
prepared: true,
|
||||
value: libmime.encodeWords(list.name) + ' <' + list.cid + '.' + getPublicUrl() + '>'
|
||||
}
|
||||
},
|
||||
list: {
|
||||
unsubscribe: null
|
||||
},
|
||||
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false, false),
|
||||
html,
|
||||
text,
|
||||
|
||||
attachments,
|
||||
encryptionKeys
|
||||
};
|
||||
|
||||
|
||||
let response;
|
||||
try {
|
||||
const info = await mailer.sendMassMail(mail);
|
||||
response = info.response || info.messageId;
|
||||
} catch (err) {
|
||||
response = err.response || err.message;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
async init(settings) {
|
||||
this.listsById = new Map(); // listId -> list
|
||||
this.listsByCid = new Map(); // listCid -> list
|
||||
|
@ -51,14 +171,14 @@ class CampaignSender {
|
|||
}
|
||||
|
||||
if (campaign.source === CampaignSource.TEMPLATE) {
|
||||
this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.data.sourceTemplate, false);
|
||||
this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.data.sourceTemplate, false);
|
||||
}
|
||||
|
||||
const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', this.campaign.id);
|
||||
const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', campaign.id);
|
||||
for (const attachment of attachments) {
|
||||
this.attachments.push({
|
||||
filename: attachment.originalname,
|
||||
path: files.getFilePath('campaign', 'attachment', this.campaign.id, attachment.filename)
|
||||
path: files.getFilePath('campaign', 'attachment', campaign.id, attachment.filename)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -111,7 +231,7 @@ class CampaignSender {
|
|||
if (replaceDataImgs) {
|
||||
// replace data: images with embedded attachments
|
||||
html = html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||
const cid = shortid.generate() + '-attachments@' + campaign.address.split('@').pop();
|
||||
const cid = shortid.generate() + '-attachments';
|
||||
attachments.push({
|
||||
path: dataUri,
|
||||
cid
|
||||
|
|
|
@ -254,6 +254,7 @@ async function getByIdTx(tx, context, id, withPermissions = true, content = Cont
|
|||
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
|
||||
entity = {
|
||||
id: entity.id,
|
||||
send_configuration: entity.send_configuration,
|
||||
|
||||
data: {
|
||||
sourceCustom: entity.data.sourceCustom
|
||||
|
@ -502,13 +503,13 @@ function getMessageCid(campaignCid, listCid, subscriptionCid) {
|
|||
}
|
||||
|
||||
async function getMessageByCid(messageCid) {
|
||||
const messageCid = messageCid.split('.');
|
||||
const messageCidElems = messageCid.split('.');
|
||||
|
||||
if (messageCid.length !== 3) {
|
||||
if (messageCidElems.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [campaignCid, listCid, subscriptionCid] = messageCid;
|
||||
const [campaignCid, listCid, subscriptionCid] = messageCidElems;
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const list = await tx('lists').where('cid', listCid).select('id');
|
||||
|
|
|
@ -334,7 +334,7 @@ async function listGroupedTx(tx, listId) {
|
|||
|
||||
async function listGrouped(context, listId) {
|
||||
return await knex.transaction(async tx => {
|
||||
// It may seem odd why there is not 'manageFields' here. But it's just a result of strictly apply the "need-to-know" principle. Simply, at this point this function is needed only in managing subscriptions.
|
||||
// It may seem odd why there is not 'viewFields' here. Simply, at this point this function is needed only in managing subscriptions.
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['manageSubscriptions']);
|
||||
return await listGroupedTx(tx, listId);
|
||||
});
|
||||
|
|
|
@ -34,6 +34,18 @@ async function listDTAjax(context, params) {
|
|||
);
|
||||
}
|
||||
|
||||
async function listWithSendPermissionDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'sendConfiguration', requiredOperations: ['sendWithoutOverrides', 'sendWithAllowedOverrides', 'sendWithAnyOverrides'] }],
|
||||
params,
|
||||
builder => builder
|
||||
.from('send_configurations')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'send_configurations.namespace'),
|
||||
['send_configurations.id', 'send_configurations.name', 'send_configurations.cid', 'send_configurations.description', 'send_configurations.mailer_type', 'send_configurations.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function _getByTx(tx, context, key, id, withPermissions, withPrivateData) {
|
||||
let entity;
|
||||
|
||||
|
@ -164,6 +176,7 @@ async function getSystemSendConfiguration() {
|
|||
module.exports.MailerType = MailerType;
|
||||
module.exports.hash = hash;
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listWithSendPermissionDTAjax = listWithSendPermissionDTAjax;
|
||||
module.exports.getByIdTx = getByIdTx;
|
||||
module.exports.getById = getById;
|
||||
module.exports.getByCid = getByCid;
|
||||
|
|
|
@ -14,6 +14,7 @@ const moment = require('moment');
|
|||
const { formatDate, formatBirthday } = require('../shared/date');
|
||||
const crypto = require('crypto');
|
||||
const campaigns = require('./campaigns');
|
||||
const lists = require('./lists');
|
||||
|
||||
const allowedKeysBase = new Set(['email', 'tz', 'is_test', 'status']);
|
||||
|
||||
|
@ -340,6 +341,33 @@ async function listDTAjax(context, listId, segmentId, params) {
|
|||
});
|
||||
}
|
||||
|
||||
async function listTestUsersDTAjax(context, listCid, params) {
|
||||
return await knex.transaction(async tx => {
|
||||
const list = await lists.getByCidTx(tx, context, listCid);
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', list.id, 'viewSubscriptions');
|
||||
|
||||
const listTable = getSubscriptionTableName(list.id);
|
||||
|
||||
const columns = [
|
||||
listTable + '.id',
|
||||
listTable + '.cid',
|
||||
listTable + '.email',
|
||||
listTable + '.status',
|
||||
listTable + '.created'
|
||||
];
|
||||
|
||||
return await dtHelpers.ajaxListTx(
|
||||
tx,
|
||||
params,
|
||||
builder => builder
|
||||
.from(listTable)
|
||||
.where('is_test', true),
|
||||
columns,
|
||||
{}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function list(context, listId, grouped = true, offset, limit) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||
|
@ -714,10 +742,10 @@ async function getListsWithEmail(context, email) {
|
|||
// FIXME - this methods is rather suboptimal if there are many lists. It quite needs permission caching in shares.js
|
||||
|
||||
return await knex.transaction(async tx => {
|
||||
const lists = await tx('lists').select(['id', 'name']);
|
||||
const lsts = await tx('lists').select(['id', 'name']);
|
||||
const result = [];
|
||||
|
||||
for (const list of lists) {
|
||||
for (const list of lsts) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', list.id, 'viewSubscriptions');
|
||||
const entity = await tx(getSubscriptionTableName(list.id)).where('email', email).first();
|
||||
if (entity) {
|
||||
|
@ -737,6 +765,7 @@ module.exports.getByCid = getByCid;
|
|||
module.exports.getByEmail = getByEmail;
|
||||
module.exports.list = list;
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
||||
module.exports.serverValidate = serverValidate;
|
||||
module.exports.create = create;
|
||||
module.exports.getGroupedFieldsMap = getGroupedFieldsMap;
|
||||
|
|
|
@ -40,4 +40,8 @@ router.postAsync('/send-configurations-table', passport.loggedIn, async (req, re
|
|||
return res.json(await sendConfigurations.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/send-configurations-with-send-permission-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await sendConfigurations.listWithSendPermissionDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -12,6 +12,10 @@ router.postAsync('/subscriptions-table/:listId/:segmentId?', passport.loggedIn,
|
|||
return res.json(await subscriptions.listDTAjax(req.context, castToInteger(req.params.listId), req.params.segmentId ? castToInteger(req.params.segmentId) : null, req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/subscriptions-test-user-table/:listCid', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await subscriptions.listTestUsersDTAjax(req.context, req.params.listCid, req.body));
|
||||
});
|
||||
|
||||
router.getAsync('/subscriptions/:listId/:subscriptionId', passport.loggedIn, async (req, res) => {
|
||||
const entity = await subscriptions.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.subscriptionId));
|
||||
entity.hash = await subscriptions.hashByList(castToInteger(req.params.listId), entity);
|
||||
|
|
|
@ -5,6 +5,7 @@ const templates = require('../../models/templates');
|
|||
|
||||
const router = require('../../lib/router-async').create();
|
||||
const {castToInteger} = require('../../lib/helpers');
|
||||
const CampaignSender = require('../../lib/campaign-sender');
|
||||
|
||||
|
||||
router.getAsync('/templates/:templateId', passport.loggedIn, async (req, res) => {
|
||||
|
@ -34,4 +35,10 @@ router.postAsync('/templates-table', passport.loggedIn, async (req, res) => {
|
|||
return res.json(await templates.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/template-test-send', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const data = req.body;
|
||||
const result = await CampaignSender.testSend(req.context, data.listCid, data.subscriptionCid, data.campaignId, data.sendConfigurationId, data.html, data.text);
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
module.exports = router;
|
Loading…
Add table
Add a link
Reference in a new issue