- Refactoring of the mail sending part. Mail queue (table 'queued') is now used also for all test emails.

- More options how to send test emails.
- Fixed problems with pausing a campaign (#593)
- Started rework of transactional sender of templates (#606), however this contains functionality regression at the moment because it does not interpret templates as HBS. It needs HBS option for templates as described in https://github.com/Mailtrain-org/mailtrain/issues/611#issuecomment-502345227

TODO:
- detect sending errors connected to not able to contact the mailer and pause/retry campaing and queued sending - don't mark the recipients as BOUNCED
- add FAILED campaign state and fall into it if sending to campaign consistently fails (i.e. the error with sending is not temporary)
- if the same happends for queued email, delete the message
This commit is contained in:
Tomas Bures 2019-06-25 07:18:06 +02:00
parent ff66a6c39e
commit 30b361290b
42 changed files with 1366 additions and 786 deletions

View file

@ -138,7 +138,7 @@ export default class API extends Component {
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/subscribe/B16uVTdW?access_token=${accessToken}`)}' \
<pre>curl -XPOST '{getUrl(`api/subscribe/B16uVTdW?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;MERGE_CHECKBOX=yes&amp;REQUIRE_CONFIRMATION=yes'</pre>
<h4>POST /api/unsubscribe/:listId {t('removeSubscription')}</h4>
@ -165,7 +165,7 @@ export default class API extends Component {
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/unsubscribe/B16uVTdW?access_token=${accessToken}`)}' \
<pre>curl -XPOST '{getUrl(`api/unsubscribe/B16uVTdW?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com'</pre>
<h4>POST /api/delete/:listId {t('deleteSubscription')}</h4>
@ -192,7 +192,7 @@ export default class API extends Component {
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/delete/B16uVTdW?access_token=${accessToken}`)}' \
<pre>curl -XPOST '{getUrl(`api/delete/B16uVTdW?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com'</pre>
<h4>POST /api/field/:listId {t('addNewCustomField')}</h4>
@ -240,7 +240,7 @@ export default class API extends Component {
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/field/B16uVTdW?access_token=${accessToken}`)}' \
<pre>curl -XPOST '{getUrl(`api/field/B16uVTdW?access_token=${accessToken}`)}' \<br/>
--data 'NAME=Birthday&amp;TYPE=birthday-us&amp;VISIBLE=yes'</pre>
<h4>GET /api/blacklist/get {t('getListOfBlacklistedEmails')}</h4>
@ -292,7 +292,7 @@ export default class API extends Component {
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token={accessToken}`)}' \
<pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token={accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;'</pre>
<h4>POST /api/blacklist/delete {t('deleteEmailFromBlacklist')}</h4>
@ -319,7 +319,7 @@ export default class API extends Component {
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/blacklist/delete?access_token=${accessToken}`)}' \
<pre>curl -XPOST '{getUrl(`api/blacklist/delete?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;'</pre>
<h4>GET /api/lists/:email {t('getTheListsAUserHasSubscribedTo')}</h4>
@ -381,15 +381,14 @@ export default class API extends Component {
<li><strong>EMAIL</strong> {t('emailAddress')} (<em>{t('required')}</em>)</li>
<li><strong>SEND_CONFIGURATION_ID</strong> {t('idOfConfigurationUsedToCreateMailer')}</li>
<li><strong>SUBJECT</strong> {t('subject')}</li>
<li><strong>DATA</strong> {t('dataPassedToTemplateWhenCompilingWith')}: <em>{'{'} "any": ["type", {'{'}"of": "data"{'}'}] {'}'}</em></li>
<li><strong>VARIABLES</strong> {t('mapOfTemplatesubjectVariablesToReplace')}: <em>{'{'} "FOO": "bar" {'}'}</em></li>
<li><strong>VARIABLES</strong> {t('mapOfTemplatesubjectVariablesToReplace')}</li>
</ul>
<p>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/templates/1/send?access_token={accessToken}`)}' \
<pre>curl -XPOST '{getUrl(`api/templates/1/send?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;SUBJECT=Test&amp;VARIABLES[FOO]=bar&amp;VARIABLES[TEST]=example'</pre>
</div>
);

View file

@ -244,7 +244,7 @@ export default class CUD extends Component {
return filterData(data, [
'name', 'description', 'segment', 'namespace', 'send_configuration',
'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override',
'from_name_override', 'from_email_override', 'reply_to_override',
'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url',
'type', 'source', 'parent', 'lists'
]);
@ -284,6 +284,8 @@ export default class CUD extends Component {
send_configuration: null,
namespace: mailtrainConfig.user.namespace,
subject: '',
click_tracking_disabled: false,
open_tracking_disabled: false,
@ -326,6 +328,10 @@ export default class CUD extends Component {
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
}
if (!state.getIn(['subject', 'value'])) {
state.setIn(['subject', 'error'], t('"Subject" line must not be empty"'));
}
if (!state.getIn(['send_configuration', 'value'])) {
state.setIn(['send_configuration', 'error'], t('sendConfigurationMustBeSelected'));
}
@ -592,7 +598,6 @@ export default class CUD extends Component {
{ 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') }
];
@ -604,10 +609,10 @@ export default class CUD extends Component {
const addOverridable = (id, label) => {
if(this.state.sendConfiguration[id + '_overridable']){
if (this.getFormValue(id + '_overriden')) {
sendSettings.push(<InputField label={t(label)} key={id + '_override'} id={id + '_override'}/>);
sendSettings.push(<InputField label={label} key={id + '_override'} id={id + '_override'}/>);
} else {
sendSettings.push(
<StaticField key={id + '_original'} label={t(label)} id={id + '_original'} className={styles.formDisabled}>
<StaticField key={id + '_original'} label={label} id={id + '_original'} className={styles.formDisabled}>
{this.state.sendConfiguration[id]}
</StaticField>
);
@ -616,7 +621,7 @@ export default class CUD extends Component {
}
else{
sendSettings.push(
<StaticField key={id + '_original'} label={t(label)} id={id + '_original'} className={styles.formDisabled}>
<StaticField key={id + '_original'} label={label} id={id + '_original'} className={styles.formDisabled}>
{this.state.sendConfiguration[id]}
</StaticField>
);
@ -626,7 +631,8 @@ export default class CUD extends Component {
addOverridable('from_name', t('fromName'));
addOverridable('from_email', t('fromEmailAddress'));
addOverridable('reply_to', t('replytoEmailAddress'));
addOverridable('subject', t('subjectLine'));
sendSettings.push(<InputField label={t('subjectLine')} key="subject" id="subject"/>);
} else {
sendSettings = <AlignedRow>{t('loadingSendConfiguration')}</AlignedRow>
}
@ -760,8 +766,6 @@ export default class CUD extends Component {
</>
}
{templateEdit}
<ButtonRow>

View file

@ -20,7 +20,7 @@ import {getEditForm, getTemplateTypes, getTypeForm, ResourceType} from '../templ
import axios from '../lib/axios';
import styles from "../lib/styles.scss";
import {getUrl} from "../lib/urls";
import {TestSendModalDialog} from "./TestSendModalDialog";
import {TestSendModalDialog, TestSendModalDialogMode} from "./TestSendModalDialog";
import {withComponentMixins} from "../lib/decorator-helpers";
import {ContentModalDialog} from "../lib/modals";
@ -234,10 +234,11 @@ export default class CustomContent extends Component {
return (
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
<TestSendModalDialog
mode={TestSendModalDialogMode.CAMPAIGN_CONTENT}
visible={this.state.showTestSendModal}
onHide={() => this.setState({showTestSendModal: false})}
getDataAsync={this.sendModalGetDataHandler}
entity={this.props.entity}
campaign={this.props.entity}
/>
<ContentModalDialog
title={this.state.exportModalTitle}
@ -261,7 +262,7 @@ export default class CustomContent extends Component {
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(CustomContent.AfterSubmitAction.LEAVE)}/>
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndGoToStatus')} onClickAsync={async () => await this.submitHandler(CustomContent.AfterSubmitAction.STATUS)}/>
<Button className="btn-success" icon="at" label={t('testSend')} onClickAsync={async () => this.setState({showTestSendModal: true})}/>
<Button className="btn-success" icon="at" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/>
</ButtonRow>
</Form>
</div>

View file

@ -16,6 +16,7 @@ import {CampaignStatus, CampaignType} from "../../../shared/campaigns";
import moment from 'moment';
import campaignsStyles from "./styles.scss";
import {withComponentMixins} from "../lib/decorator-helpers";
import {TestSendModalDialog, TestSendModalDialogMode} from "./TestSendModalDialog";
@withComponentMixins([
@ -25,7 +26,7 @@ import {withComponentMixins} from "../lib/decorator-helpers";
withPageHelpers,
requiresAuthenticatedUser
])
class TestUser extends Component {
class PreviewForTestUserModalDialog extends Component {
constructor(props) {
super(props);
this.initForm({
@ -34,7 +35,9 @@ class TestUser extends Component {
}
static propTypes = {
entity: PropTypes.object.isRequired
visible: PropTypes.bool.isRequired,
onHide: PropTypes.func.isRequired,
entity: PropTypes.object.isRequired,
}
localValidateFormValues(state) {
@ -64,6 +67,10 @@ class TestUser extends Component {
}
}
async hideModal() {
this.props.onHide();
}
render() {
const t = this.props.t;
@ -76,12 +83,14 @@ class TestUser extends Component {
];
return (
<Form stateOwner={this}>
<TableSelect id="testUser" label={t('previewCampaignAs')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.entity.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
<ButtonRow>
<Button className="btn-primary" label={t('preview')} onClickAsync={::this.previewAsync}/>
</ButtonRow>
</Form>
<ModalDialog hidden={!this.props.visible} title={t('Preview Campaign')} onCloseAsync={() => this.hideModal()} buttons={[
{ label: t('preview'), className: 'btn-primary', onClickAsync: ::this.previewAsync },
{ label: t('close'), className: 'btn-danger', onClickAsync: ::this.hideModal }
]}>
<Form stateOwner={this}>
<TableSelect id="testUser" label={t('Preview as')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.entity.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
</Form>
</ModalDialog>
);
}
}
@ -96,6 +105,12 @@ class TestUser extends Component {
class SendControls extends Component {
constructor(props) {
super(props);
this.state = {
showTestSendModal: false,
previewForTestUserVisible: false
};
this.initForm({
leaveConfirmation: false
});
@ -257,13 +272,33 @@ class SendControls extends Component {
const t = this.props.t;
const entity = this.props.entity;
const yesNoDialog = (
<ModalDialog hidden={!this.state.modalVisible} title={this.state.modalTitle} onCloseAsync={() => this.modalAction(false)} buttons={[
{ label: t('no'), className: 'btn-primary', onClickAsync: () => this.modalAction(false) },
{ label: t('yes'), className: 'btn-danger', onClickAsync: () => this.modalAction(true) }
]}>
{this.state.modalMessage}
</ModalDialog>
const dialogs = (
<>
<TestSendModalDialog
mode={TestSendModalDialogMode.CAMPAIGN_STATUS}
visible={this.state.showTestSendModal}
onHide={() => this.setState({showTestSendModal: false})}
campaign={this.props.entity}
/>
<PreviewForTestUserModalDialog
visible={this.state.previewForTestUserVisible}
onHide={() => this.setState({previewForTestUserVisible: false})}
entity={this.props.entity}
/>
<ModalDialog hidden={!this.state.modalVisible} title={this.state.modalTitle} onCloseAsync={() => this.modalAction(false)} buttons={[
{ label: t('no'), className: 'btn-primary', onClickAsync: () => this.modalAction(false) },
{ label: t('yes'), className: 'btn-danger', onClickAsync: () => this.modalAction(true) }
]}>
{this.state.modalMessage}
</ModalDialog>
</>
);
const testButtons = (
<>
<Button className="btn-success" label={t('Preview')} onClickAsync={async () => this.setState({previewForTestUserVisible: true})}/>
<Button className="btn-success" label={t('Test send')} onClickAsync={async () => this.setState({showTestSendModal: true})}/>
</>
);
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
@ -271,7 +306,7 @@ class SendControls extends Component {
const subscrInfo = entity.subscriptionsToSend === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
return (
<div>{yesNoDialog}
<div>{dialogs}
<AlignedRow label={t('sendStatus')}>
{entity.scheduled ? t('campaignIsScheduledForDelivery') : t('campaignIsReadyToBeSentOut')}
</AlignedRow>
@ -292,20 +327,36 @@ class SendControls extends Component {
:
<Button className="btn-primary" icon="send" label={t('send') + subscrInfo} onClickAsync={::this.confirmStart}/>
}
{entity.status === CampaignStatus.PAUSED && <Button className="btn-primary" icon="refresh" label={t('reset')} onClickAsync={::this.resetAsync}/>}
{entity.status === CampaignStatus.PAUSED && <LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>}
{testButtons}
</ButtonRow>
</div>
);
} else if (entity.status === CampaignStatus.PAUSING) {
return (
<div>{dialogs}
<AlignedRow label={t('sendStatus')}>
{t('Campaign is being paused. Please wait.')}
</AlignedRow>
<ButtonRow>
<LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>
{testButtons}
</ButtonRow>
</div>
);
} else if (entity.status === CampaignStatus.SENDING || (entity.status === CampaignStatus.SCHEDULED && !entity.scheduled)) {
return (
<div>{yesNoDialog}
<div>{dialogs}
<AlignedRow label={t('sendStatus')}>
{t('campaignIsBeingSentOut')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="stop" label={t('stop')} onClickAsync={::this.stopAsync}/>
<LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>
{testButtons}
</ButtonRow>
</div>
);
@ -314,7 +365,7 @@ class SendControls extends Component {
const subscrInfo = entity.subscriptionsToSend === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
return (
<div>{yesNoDialog}
<div>{dialogs}
<AlignedRow label={t('sendStatus')}>
{t('allMessagesSent!HitContinueIfYouYouWant')}
</AlignedRow>
@ -322,35 +373,38 @@ class SendControls extends Component {
<Button className="btn-primary" icon="play" label={t('continue') + subscrInfo} onClickAsync={::this.confirmStart}/>
<Button className="btn-primary" icon="refresh" label={t('reset')} onClickAsync={::this.resetAsync}/>
<LinkButton className="btn-secondary" icon="signal" label={t('viewStatistics')} to={`/campaigns/${entity.id}/statistics`}/>
{testButtons}
</ButtonRow>
</div>
);
} else if (entity.status === CampaignStatus.INACTIVE) {
return (
<div>{yesNoDialog}
<div>{dialogs}
<AlignedRow label={t('sendStatus')}>
{t('yourCampaignIsCurrentlyDisabledClick')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="play" label={t('enable')} onClickAsync={::this.enableAsync}/>
{testButtons}
</ButtonRow>
</div>
);
} else if (entity.status === CampaignStatus.ACTIVE) {
return (
<div>{yesNoDialog}
<div>{dialogs}
<AlignedRow label={t('sendStatus')}>
{t('yourCampaignIsEnabledAndSendingMessages')}
</AlignedRow>
<ButtonRow>
<Button className="btn-primary" icon="stop" label={t('disable')} onClickAsync={::this.disableAsync}/>
{testButtons}
</ButtonRow>
</div>
);
} else {
} else {
return null;
}
}
@ -441,7 +495,7 @@ export default class Status extends Component {
addOverridable('from_name', t('fromName'));
addOverridable('from_email', t('fromEmailAddress'));
addOverridable('reply_to', t('replytoEmailAddress'));
addOverridable('subject', t('subjectLine'));
sendSettings.push(<AlignedRow key="subject" label={t('subjectLine')}>{entity.subject}</AlignedRow>);
} else {
sendSettings = <AlignedRow>{t('loadingSendConfiguration')}</AlignedRow>
}
@ -491,13 +545,6 @@ export default class Status extends Component {
<Table withHeader dataUrl={`rest/lists-with-segment-by-campaign-table/${this.props.entity.id}`} columns={listsColumns} />
</AlignedRow>
{(entity.type === CampaignType.REGULAR || entity.type === CampaignType.TRIGGERED) &&
<div>
<hr/>
<TestUser entity={entity}/>
</div>
}
<hr/>
<SendControls entity={entity} refreshEntity={::this.refreshEntity}/>

View file

@ -5,13 +5,26 @@ import {withTranslation} from '../lib/i18n';
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 {CheckBox, Dropdown, Form, InputField, TableSelect, withForm} from "../lib/form";
import {withErrorHandling} from "../lib/error-handling";
import {getMailerTypes} from "../send-configurations/helpers";
import axios from '../lib/axios';
import {getUrl} from '../lib/urls';
import {withComponentMixins} from "../lib/decorator-helpers";
import {CampaignType} from "../../../shared/campaigns";
const Target = {
CAMPAIGN_ONE: 'campaign_one',
CAMPAIGN_ALL: 'campaign_all',
LIST_ONE: 'list_one',
LIST_ALL: 'list_all'
};
export const TestSendModalDialogMode = {
TEMPLATE: 0,
CAMPAIGN_CONTENT: 1,
CAMPAIGN_STATUS: 2
}
@withComponentMixins([
withTranslation,
@ -27,21 +40,39 @@ export class TestSendModalDialog extends Component {
this.mailerTypes = getMailerTypes(props.t);
this.initForm({
leaveConfirmation: false
leaveConfirmation: false,
onChangeBeforeValidation: {
list: this.onListChanged
}
});
}
static propTypes = {
stateOwner: PropTypes.object,
visible: PropTypes.bool.isRequired,
mode: PropTypes.number.isRequired,
onHide: PropTypes.func.isRequired,
getDataAsync: PropTypes.func.isRequired,
entity: PropTypes.object
getDataAsync: PropTypes.func,
campaign: PropTypes.object
}
onListChanged(mutStateData, key, oldValue, newValue) {
mutStateData.setIn(['segment', 'value'], null);
}
componentDidMount() {
const t = this.props.t;
this.populateFormValues({
testUser: null,
target: Target.CAMPAIGN_ONE,
testUserSubscriptionCid: null,
testUserListAndSubscriptionCid: null,
subjectPrepend: '',
subjectAppend: t(' [Test]'),
sendConfiguration: null,
listCid: null,
list: null,
segment: null,
useSegmentation: false
});
}
@ -52,25 +83,49 @@ export class TestSendModalDialog extends Component {
async performAction() {
const props = this.props;
const t = props.t;
const mode = this.props.mode;
if (this.isFormWithoutErrors()) {
try {
this.hideFormValidation();
this.disableForm();
this.setFormStatusMessage('info', t('sendingTestEmail'));
const data = await this.props.getDataAsync();
const data = {};
const campaignCid = props.entity.cid;
const [listCid, subscriptionCid] = this.getFormValue('testUser').split(':');
if (mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.TEMPLATE) {
const contentData = await this.props.getDataAsync();
data.html = contentData.html;
data.text = contentData.text;
}
data.listCid = listCid;
data.subscriptionCid = subscriptionCid;
data.sendConfigurationId = props.entity.send_configuration;
data.campaignId = props.entity.id;
if (mode === TestSendModalDialogMode.TEMPLATE) {
data.listCid = this.getFormValue('listCid');
data.subscriptionCid = this.getFormValue('testUserSubscriptionCid');
data.sendConfigurationId = this.getFormValue('sendConfiguration');
await axios.post(getUrl('rest/template-test-send'), data);
} else if (mode === TestSendModalDialogMode.CAMPAIGN_STATUS || mode === TestSendModalDialogMode.CAMPAIGN_CONTENT) {
data.campaignId = props.campaign.id;
data.subjectPrepend = this.getFormValue('subjectPrepend');
data.subjectAppend = this.getFormValue('subjectAppend');
const target = this.getFormValue('target');
if (target === Target.CAMPAIGN_ONE) {
const [listCid, subscriptionCid] = this.getFormValue('testUserListAndSubscriptionCid').split(':');
data.listCid = listCid;
data.subscriptionCid = subscriptionCid;
} else if (target === Target.LIST_ALL) {
data.listId = this.getFormValue('list');
data.segmentId = this.getFormValue('useSegmentation') ? this.getFormValue('segment') : null;
} else if (target === Target.LIST_ONE) {
data.listCid = this.getFormValue('listCid');
data.subscriptionCid = this.getFormValue('testUserSubscriptionCid');
}
}
await axios.post(getUrl('rest/campaign-test-send'), data);
this.clearFormStatusMessage();
@ -87,32 +142,204 @@ export class TestSendModalDialog extends Component {
localValidateFormValues(state) {
const t = this.props.t;
const props = this.props;
const target = this.getFormValue('target');
const mode = this.props.mode;
if (!state.getIn(['testUser', 'value'])) {
state.setIn(['testUser', 'error'], t('subscriptionHasToBeSelected'))
} else {
state.setIn(['testUser', 'error'], null);
state.setIn(['listCid', 'error'], null);
state.setIn(['sendConfiguration', 'error'], null);
state.setIn(['testUserSubscriptionCid', 'error'], null);
state.setIn(['testUserListAndSubscriptionCid', 'error'], null);
state.setIn(['list', 'error'], null);
state.setIn(['segment', 'error'], null);
if (mode === TestSendModalDialogMode.TEMPLATE) {
if (!state.getIn(['listCid', 'value'])) {
state.setIn(['listCid', 'error'], t('listHasToBeSelected'))
}
if (!state.getIn(['sendConfiguration', 'value'])) {
state.setIn(['sendConfiguration', 'error'], t('sendConfigurationHasToBeSelected'))
}
if (!state.getIn(['testUserSubscriptionCid', 'value'])) {
state.setIn(['testUserSubscriptionCid', 'error'], t('subscriptionHasToBeSelected'))
}
}
if ((mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) && target === Target.CAMPAIGN_ONE) {
if (!state.getIn(['testUserListAndSubscriptionCid', 'value'])) {
state.setIn(['testUserListAndSubscriptionCid', 'error'], t('subscriptionHasToBeSelected'))
}
}
if ((mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) && target === Target.LIST_ONE) {
if (!state.getIn(['listCid', 'value'])) {
state.setIn(['listCid', 'error'], t('listHasToBeSelected'))
}
if (!state.getIn(['testUserSubscriptionCid', 'value'])) {
state.setIn(['testUserSubscriptionCid', 'error'], t('subscriptionHasToBeSelected'))
}
}
if ((mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) && target === Target.LIST_ALL) {
if (!state.getIn(['list', 'value'])) {
state.setIn(['list', 'error'], t('listMustBeSelected'));
}
if (state.getIn(['useSegmentation', 'value']) && !state.getIn(['segment', 'value'])) {
state.setIn(['segment', 'error'], t('segmentMustBeSelected'));
}
}
}
render() {
const t = this.props.t;
const props = this.props;
const testUsersColumns = [
{ data: 1, title: t('email') },
{ data: 2, title: t('subscriptionId'), render: data => <code>{data}</code> },
{ data: 3, title: t('listId'), render: data => <code>{data}</code> },
{ data: 4, title: t('list') },
{ data: 5, title: t('listNamespace') }
];
const content = [];
const target = this.getFormValue('target');
const mode = this.props.mode;
if (mode === TestSendModalDialogMode.CAMPAIGN_STATUS) {
const targetOpts = [
{key: Target.CAMPAIGN_ONE, label: t('Single test user of the campaign')},
{key: Target.CAMPAIGN_ALL, label: t('All test users of the campaign')},
{key: Target.LIST_ONE, label: t('Single test user from a list')},
{key: Target.LIST_ALL, label: t('All test users from a list/segment')}
];
content.push(
<Dropdown key="target" id="target" format="wide" label={t('Select to where you want to send the test')} options={targetOpts}/>
);
}
if (mode === TestSendModalDialogMode.TEMPLATE) {
const listCid = this.getFormValue('listCid');
const testUsersColumns = [
{ data: 1, title: t('subscriptionId'), 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: 6, title: t('namespace') }
];
content.push(
<TableSelect key="sendConfiguration" id="sendConfiguration" format="wide" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-with-send-permission-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
);
content.push(
<TableSelect key="listCid" id="listCid" format="wide" label={t('list')} withHeader dropdown dataUrl={`rest/lists-table`} columns={listsColumns} selectionKeyIndex={2} selectionLabelIndex={1} />
);
if (listCid) {
content.push(
<TableSelect key="testUserSubscriptionCid" id="testUserSubscriptionCid" format="wide" label={t('subscription')} withHeader dropdown dataUrl={`rest/subscriptions-test-user-table/${listCid}`} columns={testUsersColumns} selectionKeyIndex={1} selectionLabelIndex={2} />
);
}
}
if ((mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) && target === Target.CAMPAIGN_ONE) {
const testUsersColumns = [
{data: 1, title: t('email')},
{data: 2, title: t('subscriptionId'), render: data => <code>{data}</code>},
{data: 3, title: t('listId'), render: data => <code>{data}</code>},
{data: 4, title: t('list')},
{data: 5, title: t('listNamespace')}
];
content.push(
<TableSelect key="testUserListAndSubscriptionCid" id="testUserListAndSubscriptionCid" format="wide" label={t('subscription')} withHeader dropdown dataUrl={`rest/campaigns-test-users-table/${this.props.campaign.id}`} columns={testUsersColumns} selectionLabelIndex={1} />
);
}
if ((mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) && target === Target.LIST_ONE) {
const listCid = this.getFormValue('listCid');
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 testUsersColumns = [
{ data: 1, title: t('subscriptionId'), render: data => <code>{data}</code> },
{ data: 2, title: t('email') }
];
content.push(
<TableSelect key="listCid" id="listCid" format="wide" label={t('list')} withHeader dropdown dataUrl={`rest/lists-table`} columns={listsColumns} selectionKeyIndex={2} selectionLabelIndex={1} />
);
if (listCid) {
content.push(
<TableSelect key="testUserSubscriptionCid" id="testUserSubscriptionCid" format="wide" label={t('subscription')} withHeader dropdown dataUrl={`rest/subscriptions-test-user-table/${listCid}`} columns={testUsersColumns} selectionKeyIndex={1} selectionLabelIndex={2} />
);
}
}
if ((mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) && target === Target.LIST_ALL) {
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 segmentsColumns = [
{ data: 1, title: t('name') }
];
content.push(
<TableSelect key="list" id="list" format="wide" label={t('list')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
);
const selectedList = this.getFormValue('list');
content.push(
<div key="segment">
<CheckBox id="useSegmentation" format="wide" text={t('useAParticularSegment')}/>
{selectedList && this.getFormValue('useSegmentation') &&
<TableSelect id="segment" format="wide" withHeader dropdown dataUrl={`rest/segments-table/${selectedList}`} columns={segmentsColumns} selectionLabelIndex={1} />
}
</div>
);
}
if (mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) {
content.push(
<InputField key="subjectPrepend" id="subjectPrepend" format="wide" label={t('Prepend to subject')}/>
);
content.push(
<InputField key="subjectAppend" id="subjectAppend" format="wide" label={t('Append to subject')}/>
);
}
return (
<ModalDialog hidden={!this.props.visible} title={t('sendTestEmail')} onCloseAsync={() => this.hideModal()} buttons={[
{ label: t('send'), className: 'btn-primary', onClickAsync: ::this.performAction },
{ label: t('cancel'), className: 'btn-danger', onClickAsync: ::this.hideModal }
{ label: t('close'), className: 'btn-danger', 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} />
{content}
</Form>
</ModalDialog>
);

View file

@ -18,7 +18,8 @@ export function getCampaignLabels(t) {
[CampaignStatus.PAUSED]: t('paused'),
[CampaignStatus.INACTIVE]: t('inactive'),
[CampaignStatus.ACTIVE]: t('active'),
[CampaignStatus.SENDING]: t('sending')
[CampaignStatus.SENDING]: t('sending'),
[CampaignStatus.PAUSING]: t('Pausing')
};

View file

@ -333,6 +333,13 @@ class InputField extends Component {
const className = owner.addFormValidationClass('form-control', id);
/* This is for debugging purposes when React reports that InputField is uncontrolled
const value = owner.getFormValue(id);
if (value === null || value === undefined) console.log(`Warning: InputField ${id} is ${value}`);
*/
const value = owner.getFormValue(id);
if (value === null || value === undefined) console.log(`Warning: InputField ${id} is ${value}`);
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
<input type={type} value={owner.getFormValue(id)} placeholder={props.placeholder} id={htmlId} className={className} aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}/>
);

View file

@ -85,7 +85,7 @@ export default class CUD extends Component {
}
return filterData(data, ['name', 'description', 'from_email', 'from_email_overridable', 'from_name',
'from_name_overridable', 'reply_to', 'reply_to_overridable', 'subject', 'subject_overridable', 'x_mailer',
'from_name_overridable', 'reply_to', 'reply_to_overridable', 'x_mailer',
'verp_hostname', 'verp_disable_sender_header', 'mailer_type', 'mailer_settings', 'namespace']);
}
@ -103,8 +103,6 @@ export default class CUD extends Component {
from_name_overridable: false,
reply_to: '',
reply_to_overridable: false,
subject: '',
subject_overridable: false,
verpEnabled: false,
verp_hostname: '',
verp_disable_sender_header: false,
@ -233,8 +231,6 @@ export default class CUD extends Component {
<CheckBox id="from_name_overridable" text={t('overridable')} className={sendConfigurationsStyles.overridableCheckbox}/>
<InputField id="reply_to" label={t('defaultReplytoEmail')}/>
<CheckBox id="reply_to_overridable" text={t('overridable')} className={sendConfigurationsStyles.overridableCheckbox}/>
<InputField id="subject" label={t('subject')}/>
<CheckBox id="subject_overridable" text={t('overridable')} className={sendConfigurationsStyles.overridableCheckbox}/>
<InputField id="x_mailer" label={t('xMailer')}/>
</Fieldset>

View file

@ -21,7 +21,7 @@ export function getMailerTypes(t) {
const initVals = mailerTypes[mailerType].initData();
for (const key in initVals) {
if (!mutStateData.hasIn([key])) {
if (!mutStateData.hasIn([key, 'value']) || mutStateData.getIn([key, 'value']) === undefined) {
mutStateData.setIn([key, 'value'], initVals[key]);
}
}

View file

@ -27,7 +27,7 @@ import {getEditForm, getTemplateTypes, getTypeForm} from './helpers';
import axios from '../lib/axios';
import styles from "../lib/styles.scss";
import {getUrl} from "../lib/urls";
import {TestSendModalDialog} from "./TestSendModalDialog";
import {TestSendModalDialog, TestSendModalDialogMode} from "../campaigns/TestSendModalDialog";
import {withComponentMixins} from "../lib/decorator-helpers";
import moment from 'moment';
@ -298,6 +298,7 @@ export default class CUD extends Component {
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
{isEdit &&
<TestSendModalDialog
mode={TestSendModalDialogMode.TEMPLATE}
visible={this.state.showTestSendModal}
onHide={() => this.setState({showTestSendModal: false})}
getDataAsync={this.sendModalGetDataHandler}/>

View file

@ -1,149 +0,0 @@
'use strict';
import React, {Component} from 'react';
import {withTranslation} from '../lib/i18n';
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";
import {withComponentMixins} from "../lib/decorator-helpers";
@withComponentMixins([
withTranslation,
withForm,
withErrorHandling,
withPageHelpers,
requiresAuthenticatedUser
])
export class TestSendModalDialog extends Component {
constructor(props) {
super(props);
this.mailerTypes = getMailerTypes(props.t);
this.initForm({
leaveConfirmation: false
});
}
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('sendingTestEmail'));
const data = await this.props.getDataAsync();
data.listCid = this.getFormValue('list');
data.subscriptionCid = this.getFormValue('testUser');
data.sendConfigurationId = this.getFormValue('sendConfiguration');
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('sendConfigurationHasToBeSelected'))
} else {
state.setIn(['sendConfiguration', 'error'], null);
}
if (!state.getIn(['list', 'value'])) {
state.setIn(['list', 'error'], t('listHasToBeSelected'))
} else {
state.setIn(['list', 'error'], null);
}
if (!state.getIn(['testUser', 'value'])) {
state.setIn(['testUser', 'error'], t('subscriptionHasToBeSelected'))
} else {
state.setIn(['testUser', 'error'], null);
}
}
render() {
const t = this.props.t;
const listId = this.getFormValue('list');
const testUsersColumns = [
{ data: 1, title: t('subscriptionId'), 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('sendTestEmail')} onCloseAsync={() => this.hideModal()} buttons={[
{ label: t('send'), className: 'btn-primary', onClickAsync: ::this.performAction },
{ label: t('cancel'), className: 'btn-danger', onClickAsync: ::this.hideModal }
]}>
<Form stateOwner={this} format="wide">
<TableSelect id="sendConfiguration" format="wide" label={t('sendConfiguration')} 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>
);
}
}