First attempt on campaign editing. Misses attachments. Untested.

This commit is contained in:
Tomas Bures 2018-07-31 10:04:28 +05:30
parent ee786bc8ad
commit 0e0fb944e3
26 changed files with 1244 additions and 233 deletions

503
client/src/campaigns/CUD.js Normal file
View file

@ -0,0 +1,503 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Trans,
translate
} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page'
import {
ACEEditor,
AlignedRow,
Button,
ButtonRow,
CheckBox,
Dropdown,
Form,
FormSendMethod,
InputField,
StaticField,
TableSelect,
TextArea,
withForm
} from '../lib/form';
import {
withAsyncErrorHandler,
withErrorHandling
} from '../lib/error-handling';
import {
NamespaceSelect,
validateNamespace
} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {
getEditForm,
getTemplateTypes,
getTypeForm
} from '../templates/helpers';
import {ActionLink} from "../lib/bootstrap-components";
import axios from '../lib/axios';
import styles from "../lib/styles.scss";
import {getUrl} from "../lib/urls";
import {CampaignType, CampaignSource} from "../../../shared/campaigns";
import moment from 'moment';
import {getMailerTypes} from "../send-configurations/helpers";
@translate()
@withForm
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component {
constructor(props) {
super(props);
this.templateTypes = getTemplateTypes(props.t, 'data_sourceCustom_');
this.mailerTypes = getMailerTypes(props.t);
this.state = {
showMergeTagReference: false,
elementInFullscreen: false,
};
this.initForm({
onChange: {
send_configuration: ::this.onSendConfigurationChanged
},
onChangeBeforeValidation: {
data_sourceCustom_type: ::this.onCustomTemplateTypeChanged
}
});
}
static propTypes = {
action: PropTypes.string.isRequired,
wizard: PropTypes.string,
entity: PropTypes.object,
type: PropTypes.number.isRequired
}
onCustomTemplateTypeChanged(mutState, key, oldType, type) {
if (type) {
this.templateTypes[type].afterTypeChange(mutState);
}
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
if (data.source === CampaignSource.TEMPLATE || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
data.data_sourceTemplate = data.data.sourceTemplate;
} else {
data.data_sourceTemplate = null;
}
if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
data.data_sourceCustom_type = data.data.source.type;
data.data_sourceCustom_data = data.data.source.data;
data.data_sourceCustom_html = data.data.source.html;
data.data_sourceCustom_text = data.data.source.text;
this.templateTypes[data.type].afterLoad(data);
} else {
data.data_sourceCustom_type = null;
data.data_sourceCustom_data = {};
data.data_sourceCustom_html = '';
data.data_sourceCustom_text = '';
}
if (data.source === CampaignSource.URL) {
data.data_sourceUrl = data.data.sourceUrl;
} else {
data.data_sourceUrl = null;
}
if (data.type === CampaignType.RSS) {
data.data_feedUrl = data.data.feedUrl;
} else {
data.data_feedUrl = '';
}
data.useSegmentation = !!data.segment;
this.fetchSendConfiguration(data.send_configuration);
});
} else {
this.populateFormValues({
type: this.props.type,
name: '',
description: '',
list: null,
segment: null,
useSegmentation: false,
send_configuration: null,
namespace: mailtrainConfig.user.namespace,
from_name_override: '',
from_name_overriden: false,
from_email_override: '',
from_email_overriden: false,
reply_to_override: '',
reply_to_overriden: false,
subject_override: '',
subject_overriden: false,
click_tracking_disabled: false,
open_trackings_disabled: false,
source: CampaignSource.TEMPLATE,
// This is for CampaignSource.TEMPLATE
data_sourceTemplate: null,
// This is for CampaignSource.CUSTOM
data_sourceCustom_type: null,
data_sourceCustom_data: {},
data_sourceCustom_html: '',
data_sourceCustom_text: '',
...this.templateTypes[mailtrainConfig.editors[0]].initData(),
// This is for CampaignSource.URL
data_sourceUrl: '',
// This is for CampaignType.RSS
data_feedUrl: ''
});
}
}
localValidateFormValues(state) {
const t = this.props.t;
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
if (!state.getIn(['list', 'value'])) {
state.setIn(['list', 'error'], t('List must be selected'));
} else {
state.setIn(['list', 'error'], null);
}
if (state.getIn(['useSegmentation', 'value']) && !state.getIn(['segment', 'value'])) {
state.setIn(['segment', 'error'], t('Segment must be selected'));
} else {
state.setIn(['segment', 'error'], null);
}
if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) {
state.setIn(['from_email_override', 'error'], t('"From" email must not be empty'));
} else {
state.setIn(['from_email_override', 'error'], null);
}
const campaignTypeKey = state.getIn(['type', 'value']);
const sourceTypeKey = state.getIn(['source', 'value']);
if (sourceTypeKey === CampaignSource.TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
if (!state.getIn(['data_sourceTemplate', 'value'])) {
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
} else {
state.setIn(['data_sourceTemplate', 'error'], null);
}
} else if (sourceTypeKey === CampaignSource.CUSTOM) {
// The type is used only in create form. In case of CUSTOM_FROM_TEMPLATE, it is determined by the source template, so no need to check it here
const customTemplateTypeKey = state.getIn(['data_sourceCustom_type', 'value']);
if (!customTemplateTypeKey) {
state.setIn(['data_sourceCustom_type', 'error'], t('Type must be selected'));
} else {
state.setIn(['data_sourceCustom_type', 'error'], null);
}
if (customTemplateTypeKey) {
this.templateTypes[customTemplateTypeKey].validate(state);
}
} else if (sourceTypeKey === CampaignSource.URL) {
if (!state.getIn(['data_sourceUrl', 'value'])) {
state.setIn(['data_sourceUrl', 'error'], t('URL must not be empty'));
} else {
state.setIn(['data_sourceUrl', 'error'], null);
}
}
if (campaignTypeKey === CampaignType.RSS) {
if (!state.getIn(['data_feedUrl', 'value'])) {
state.setIn(['data_feedUrl', 'error'], t('RSS feed URL must be given'));
} else {
state.setIn(['data_feedUrl', 'error'], null);
}
}
validateNamespace(t, state);
}
async submitHandler() {
const t = this.props.t;
if (this.props.entity) {
const sourceTypeKey = this.getFormValue('source');
if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE) {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
}
}
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `rest/campaigns/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = 'rest/campaigns'
}
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitResponse = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
if (!data.useSegmentation) {
data.segment = null;
}
delete data.useSegmentation;
data.data = {};
if (data.source === CampaignSource.TEMPLATE || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
data.data.sourceTemplate = data.data_sourceTemplate;
}
if (data.source === CampaignSource.CUSTOM || data.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
this.templateTypes[data.data_sourceCustom_type].beforeSave(data);
data.data.source = {
type: data.data_sourceCustom_type,
data: data.data_sourceCustom_data,
html: data.data_sourceCustom_html,
text: data.data_sourceCustom_text
}
}
if (data.source === CampaignSource.URL) {
data.data.sourceUrl = data.data_sourceUrl;
}
if (data.type === CampaignType.RSS) {
data.data.feedUrl = data.data_feedUrl;
}
for (const key in data) {
if (key.startsWith('data_')) {
delete data[key];
}
}
});
if (submitResponse) {
if (this.props.entity) {
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
} else {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
}
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
}
}
async extractPlainText() {
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
await this.templateTypes[customTemplateTypeKey].exportHTMLEditorData(this);
const html = this.getFormValue('data_sourceCustom_html');
if (!html) {
return;
}
if (this.isFormDisabled()) {
return;
}
this.disableForm();
const response = await axios.post(getUrl('rest/html-to-text', { html }));
this.updateFormValue('data_sourceCustom_text', response.data.text);
this.enableForm();
}
async toggleMergeTagReference() {
this.setState({
showMergeTagReference: !this.state.showMergeTagReference
});
}
async setElementInFullscreen(elementInFullscreen) {
this.setState({
elementInFullscreen
});
}
render() {
const t = this.props.t;
const isEdit = !!this.props.entity;
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
const useSaveAndEditLabel = !isEdit;
let templateEdit = null;
let extraSettings = null;
const sourceTypeKey = this.getFormValue('source');
const campaignTypeKey = this.getFormValue('type');
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
const templatesColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') },
];
templateEdit = <TableSelect id="data_sourceTemplate" label={t('Template')} withHeader dropdown dataUrl='rest/templates-table' columns={templatesColumns} selectionLabelIndex={1} />;
} else if (sourceTypeKey === CampaignSource.CUSTOM || (isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
const customTemplateTypeOptions = [];
for (const key of mailtrainConfig.editors) {
customTemplateTypeOptions.push({key, label: this.templateTypes[key].typeName});
}
// TODO: Toggle HTML preview
const customTemplateTypeKey = this.getFormValue('data_sourceCustom_type');
let customTemplateEditForm = null;
let customTemplateTypeForm = null;
if (customTemplateTypeKey) {
customTemplateTypeForm = getTypeForm(this, customTemplateTypeKey, isEdit);
if (isEdit) {
customTemplateEditForm = getEditForm(this, customTemplateTypeKey);
}
}
templateEdit = <div>
{isEdit
?
<StaticField id="data_sourceCustom_type" className={styles.formDisabled} label={t('Type')}>
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
</StaticField>
:
<Dropdown id="data_sourceCustom_type" label={t('Type')} options={customTemplateTypeOptions}/>
}
{customTemplateTypeForm}
{customTemplateEditForm}
</div>;
} else if (sourceTypeKey === CampaignSource.URL) {
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')}/>
}
if (campaignTypeKey === CampaignType.RSS) {
extraSettings = <InputField id="data_feedUrl" label={t('RSS Feed Url')}/>
}
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') }
];
const sendConfigurationsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') }
];
return (
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
{canDelete &&
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/campaigns/${this.props.entity.id}`}
cudUrl={`/campaigns/${this.props.entity.id}/edit`}
listUrl="/campaigns"
deletingMsg={t('Deleting campaign ...')}
deletedMsg={t('Campaign deleted')}/>
}
<Title>{isEdit ? t('Edit Campaign') : t('Create Campaign')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<TableSelect id="list" label={t('List')} withHeader dropdown dataUrl='rest/lists-table' columns={listsColumns} selectionLabelIndex={1} />
<CheckBox id="useSegmentation" text={t('Use segmentation')}/>
{this.getFormValue('useSegmentation') &&
<TableSelect id="segment" label={t('Segment')} withHeader dropdown dataUrl='rest/segments-table' columns={segmentsColumns} selectionLabelIndex={1} />
}
{extraSettings}
<NamespaceSelect/>
<TableSelect id="send_configuration" label={t('Send configuration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} />
<CheckBox id="from_name_overriden" text={t('Override email "From" name')}/>
{ this.getFormValue('from_name_overriden') && <InputField id="from_name_override" label={t('Email "From" name')}/> }
<CheckBox id="from_email_overriden" text={t('Override email "From" address')}/>
{ this.getFormValue('from_email_overriden') && <InputField id="from_email_override" label={t('Email "From" address')}/> }
<CheckBox id="reply_to_overriden" text={t('Override email "Reply-to" address')}/>
{ this.getFormValue('reply_to_overriden') && <InputField id="reply_to_override" label={t('Email "Reply-to" address')}/> }
<CheckBox id="subject_overriden" text={t('Override email "Subject" line')}/>
{ this.getFormValue('subject_overriden') && <InputField id="subject_override" label={t('Email "Subject" line')}/> }
<CheckBox id="open_trackings_disabled" text={t('Disable opened tracking')}/>
<CheckBox id="click_tracking_disabled" text={t('Disable clicked tracking')}/>
{templateEdit}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={isEdit ? t('Save') : t('Save and edit campaign')}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.entity.id}/delete`}/> }
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -0,0 +1,144 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import {Icon} from '../lib/bootstrap-components';
import {
NavButton,
requiresAuthenticatedUser,
Title,
Toolbar,
withPageHelpers
} from '../lib/page';
import {
withAsyncErrorHandler,
withErrorHandling
} from '../lib/error-handling';
import {Table} from '../lib/table';
import moment from 'moment';
import {
CampaignSource,
CampaignStatus,
CampaignType
} from "../../../shared/campaigns";
import {checkPermissions} from "../lib/permissions";
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
super(props);
const t = props.t;
this.campaignStatuses = {
[CampaignStatus.IDLE]: t('Idle'),
[CampaignStatus.FINISHED]: t('Finished'),
[CampaignStatus.PAUSED]: t('Paused'),
[CampaignStatus.INACTIVE]: t('Inactive'),
[CampaignStatus.ACTIVE]: t('Active')
};
this.campaignTypes = {
[CampaignType.REGULAR]: t('Regular'),
[CampaignType.TRIGGERED]: t('Triggered'),
[CampaignType.RSS]: t('RSS')
};
this.state = {};
}
@withAsyncErrorHandler
async fetchPermissions() {
const result = await checkPermissions({
createCampaign: {
entityTypeId: 'namespace',
requiredOperations: ['createCampaign']
}
});
this.setState({
createPermitted: result.data.createCampaign
});
}
componentDidMount() {
this.fetchPermissions();
}
render() {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypes[data] },
{
data: 4,
title: t('Status'),
render: (data, display, rowData) => {
if (data === CampaignStatus.SCHEDULED) {
const scheduled = rowData[5];
if (scheduled && new Date(scheduled) > new Date()) {
return t('Sending scheduled');
} else {
return t('Sending');
}
} else {
return this.campaignStatuses[data];
}
}
},
{ data: 7, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 8, title: t('Namespace') },
{
actions: data => {
const actions = [];
const perms = data[9];
const campaignSource = data[6];
if (perms.includes('edit')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/campaigns/${data[0]}/edit`
});
}
if (perms.includes('manageFiles') && (campaignSource === CampaignSource.CUSTOM || campaignSource === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
actions.push({
label: <Icon icon="hdd" title={t('Files')}/>,
link: `/campaigns/${data[0]}/files`
});
}
// FIXME: add attachments
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,
link: `/campaigns/${data[0]}/share`
});
}
return actions;
}
}
];
return (
<div>
<Toolbar>
{this.state.createPermitted &&
<NavButton linkTo="/campaigns/create" className="btn-primary" icon="plus" label={t('Create Campaign')}/>
}
</Toolbar>
<Title>{t('Campaigns')}</Title>
<Table withHeader dataUrl="rest/campaigns-table" columns={columns} />
</div>
);
}
}

View file

@ -0,0 +1,66 @@
'use strict';
import React from 'react';
import CampaignsCUD from './CUD';
import CampaignsList from './List';
import Share from '../shares/Share';
import Files from "../lib/files";
import {CampaignSource, CampaignType} from "../../../shared/campaigns";
function getMenus(t) {
return {
'campaigns': {
title: t('Campaigns'),
link: '/campaigns',
panelComponent: CampaignsList,
children: {
':campaignId([0-9]+)': {
title: resolved => t('Campaign "{{name}}"', {name: resolved.campaign.name}),
resolve: {
campaign: params => `rest/campaigns/${params.campaignId}`
},
link: params => `/campaigns/${params.campaignId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/campaigns/${params.campaignId}/edit`,
visible: resolved => resolved.campaign.permissions.includes('edit'),
panelRender: props => <CampaignsCUD action={props.match.params.action} entity={props.resolved.campaign} />
},
files: {
title: t('Files'),
link: params => `/campaigns/${params.campaignId}/files`,
visible: resolved => resolved.campaign.permissions.includes('edit') && (resolved.campaign.source === CampaignSource.CUSTOM || resolved.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE),
panelRender: props => <Files title={t('Files')} entity={props.resolved.campaign} entityTypeId="campaign" />
},
// FIXME: add attachments
share: {
title: t('Share'),
link: params => `/campaigns/${params.campaignId}/share`,
visible: resolved => resolved.campaign.permissions.includes('share'),
panelRender: props => <Share title={t('Share')} entity={props.resolved.campaign} entityTypeId="campaign" />
}
}
},
'create-regular': {
title: t('Create Regular Campaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.REGULAR} />
},
'create-rss': {
title: t('Create RSS Campaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.RSS} />
},
'create-triggered': {
title: t('Create Triggered Campaign'),
panelRender: props => <CampaignsCUD action="create" type={CampaignType.TRIGGERED} />
}
}
}
};
}
export default {
getMenus
}

View file

@ -151,6 +151,8 @@ export default class List extends Component {
dataUrl += '/' + this.props.segmentId;
}
// FIXME - presents segments in a data table as in campaign edit
return (
<div>
<Toolbar>

View file

@ -15,6 +15,7 @@ import blacklist from './blacklist/root';
import lists from './lists/root';
import namespaces from './namespaces/root';
import reports from './reports/root';
import campaigns from './campaigns/root';
import templates from './templates/root';
import users from './users/root';
import sendConfigurations from './send-configurations/root';
@ -45,7 +46,7 @@ class Root extends Component {
const t = props.t;
const self = this;
const topLevelMenuKeys = ['lists', 'templates', 'reports'];
const topLevelMenuKeys = ['lists', 'templates', 'campaigns', 'reports'];
class MainMenu extends Component {
render() {
@ -123,7 +124,8 @@ class Root extends Component {
...blacklist.getMenus(t),
...account.getMenus(t),
...settings.getMenus(t),
...sendConfigurations.getMenus(t)
...sendConfigurations.getMenus(t),
...campaigns.getMenus(t)
}
}
};

View file

@ -17,7 +17,7 @@ function getMenus(t) {
':sendConfigurationId([0-9]+)': {
title: resolved => t('Template "{{name}}"', {name: resolved.sendConfiguration.name}),
resolve: {
sendConfiguration: params => `rest/send-configurations/${params.sendConfigurationId}`
sendConfiguration: params => `rest/send-configurations-private/${params.sendConfigurationId}`
},
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
navs: {

View file

@ -2,10 +2,7 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Trans,
translate
} from 'react-i18next';
import {translate} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
@ -13,8 +10,6 @@ import {
withPageHelpers
} from '../lib/page'
import {
ACEEditor,
AlignedRow,
Button,
ButtonRow,
Dropdown,
@ -32,8 +27,11 @@ import {
} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import mailtrainConfig from 'mailtrainConfig';
import {getTemplateTypes} from './helpers';
import {ActionLink} from "../lib/bootstrap-components";
import {
getEditForm,
getTemplateTypes,
getTypeForm
} from './helpers';
import axios from '../lib/axios';
import styles from "../lib/styles.scss";
import {getUrl} from "../lib/urls";
@ -158,7 +156,6 @@ export default class CUD extends Component {
const html = this.getFormValue('html');
if (!html) {
alert('Missing HTML content');
return;
}
@ -203,101 +200,15 @@ export default class CUD extends Component {
let editForm = null;
if (isEdit && typeKey) {
editForm = <div>
<AlignedRow>
<Button className="btn-default" onClickAsync={::this.toggleMergeTagReference} label={t('Merge tag reference')}/>
{this.state.showMergeTagReference &&
<div style={{marginTop: '15px'}}>
<Trans><p>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>[TAG_NAME]</code> or <code>[TAG_NAME/fallback]</code> where <code>fallback</code> is an optional text value used when <code>TAG_NAME</code> is empty.</p></Trans>
<Trans><p>You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.</p></Trans>
<table className="table table-bordered table-condensed table-striped">
<thead>
<tr>
<th>
<Trans>Merge tag</Trans>
</th>
<th>
<Trans>Description</Trans>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
[LINK_UNSUBSCRIBE]
</th>
<td>
<Trans>URL that points to the unsubscribe page</Trans>
</td>
</tr>
<tr>
<th scope="row">
[LINK_PREFERENCES]
</th>
<td>
<Trans>URL that points to the preferences page of the subscriber</Trans>
</td>
</tr>
<tr>
<th scope="row">
[LINK_BROWSER]
</th>
<td>
<Trans>URL to preview the message in a browser</Trans>
</td>
</tr>
<tr>
<th scope="row">
[EMAIL]
</th>
<td>
<Trans>Email address</Trans>
</td>
</tr>
<tr>
<th scope="row">
[SUBSCRIPTION_ID]
</th>
<td>
<Trans>Unique ID that identifies the recipient</Trans>
</td>
</tr>
<tr>
<th scope="row">
[LIST_ID]
</th>
<td>
<Trans>Unique ID that identifies the list used for this campaign</Trans>
</td>
</tr>
<tr>
<th scope="row">
[CAMPAIGN_ID]
</th>
<td>
<Trans>Unique ID that identifies current campaign</Trans>
</td>
</tr>
</tbody>
</table>
</div>}
</AlignedRow>
{this.templateTypes[typeKey].getHTMLEditor(this)}
<ACEEditor id="text" height="400px" mode="text" label={t('Template content (plain text)')} help={<Trans>To extract the text from HTML click <ActionLink onClickAsync={::this.extractPlainText}>here</ActionLink>. Please note that your existing plaintext in the field above will be overwritten. This feature uses the <a href="http://premailer.dialect.ca/api">Premailer API</a>, a third party service. Their Terms of Service and Privacy Policy apply.</Trans>}/>
</div>
editForm = getEditForm(this, typeKey);
}
let typeForm = null;
if (typeKey) {
typeForm = <div>
{this.templateTypes[typeKey].getTypeForm(this, isEdit)}
</div>;
typeForm = getTypeForm(this, typeKey, isEdit);
}
return (
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
{canDelete &&

View file

@ -1,14 +1,22 @@
'use strict';
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { Icon } from '../lib/bootstrap-components';
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
import { Table } from '../lib/table';
import axios from '../lib/axios';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import {Icon} from '../lib/bootstrap-components';
import {
NavButton,
requiresAuthenticatedUser,
Title,
Toolbar,
withPageHelpers
} from '../lib/page';
import {
withAsyncErrorHandler,
withErrorHandling
} from '../lib/error-handling';
import {Table} from '../lib/table';
import moment from 'moment';
import { getTemplateTypes } from './helpers';
import {getTemplateTypes} from './helpers';
import {checkPermissions} from "../lib/permissions";
@translate()

View file

@ -17,19 +17,20 @@ import {
} from "../lib/mosaico";
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
import {getTrustedUrl, getSandboxUrl} from "../lib/urls";
import {getSandboxUrl} from "../lib/urls";
import mailtrainConfig from 'mailtrainConfig';
export function getTemplateTypes(t) {
export function getTemplateTypes(t, prefix = '') {
// The prefix is used to to enable use within other forms (i.e. campaign form)
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]);
if (!mutState.hasIn([prefix + key])) {
mutState.setIn([prefix + key, 'value'], initVals[key]);
}
}
}
@ -55,43 +56,43 @@ export function getTemplateTypes(t) {
templateTypes.mosaico = {
typeName: t('Mosaico'),
getTypeForm: (owner, isEdit) =>
<TableSelect id="mosaicoTemplate" label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<MosaicoEditor
ref={node => owner.editorNode = node}
entity={owner.props.entity}
initialModel={owner.getFormValue('mosaicoData').model}
initialMetadata={owner.getFormValue('mosaicoData').metadata}
templatePath={getSandboxUrl(`mosaico/templates/${owner.getFormValue('mosaicoTemplate')}/index.html`)}
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
templatePath={getSandboxUrl(`mosaico/templates/${owner.getFormValue(prefix + 'mosaicoTemplate')}/index.html`)}
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('mosaicoData', {
owner.updateFormValue(prefix + 'html', html);
owner.updateFormValue(prefix + 'mosaicoData', {
metadata,
model
});
},
initData: () => ({
mosaicoTemplate: '',
mosaicoData: {}
[prefix + 'mosaicoTemplate']: '',
[prefix + 'mosaicoData']: {}
}),
afterLoad: data => {
data.mosaicoTemplate = data.data.mosaicoTemplate;
data.mosaicoData = {
metadata: data.data.metadata,
model: data.data.model
data[prefix + 'mosaicoTemplate'] = data[prefix + 'data'].mosaicoTemplate;
data[prefix + 'mosaicoData'] = {
metadata: data[prefix + 'data'].metadata,
model: data[prefix + 'data'].model
};
},
beforeSave: data => {
data.data = {
mosaicoTemplate: data.mosaicoTemplate,
metadata: data.mosaicoData.metadata,
model: data.mosaicoData.model
data[prefix + 'data'] = {
mosaicoTemplate: data[prefix + 'mosaicoTemplate'],
metadata: data[prefix + 'mosaicoData'].metadata,
model: data[prefix + 'mosaicoData'].model
};
clearBeforeSave(data);
},
@ -99,11 +100,11 @@ export function getTemplateTypes(t) {
initFieldsIfMissing(mutState, 'mosaico');
},
validate: state => {
const mosaicoTemplate = state.getIn(['mosaicoTemplate', 'value']);
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
if (!mosaicoTemplate) {
state.setIn(['mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
} else {
state.setIn(['mosaicoTemplate', 'error'], null);
state.setIn([prefix + 'mosaicoTemplate', 'error'], null);
}
}
};
@ -113,23 +114,23 @@ export function getTemplateTypes(t) {
templateTypes.mosaicoWithFsTemplate = {
typeName: t('Mosaico with predefined templates'),
getTypeForm: (owner, isEdit) =>
<Dropdown id="mosaicoFsTemplate" label={t('Mosaico Template')} options={mosaicoFsTemplatesOptions}/>,
<Dropdown id={prefix + 'mosaicoFsTemplate'} label={t('Mosaico Template')} options={mosaicoFsTemplatesOptions}/>,
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}
templatePath={getSandboxUrl(`public/mosaico/templates/${owner.getFormValue('mosaicoFsTemplate')}/index.html`)}
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
templatePath={getSandboxUrl(`public/mosaico/templates/${owner.getFormValue(prefix + 'mosaicoFsTemplate')}/index.html`)}
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('mosaicoData', {
owner.updateFormValue(prefix + 'html', html);
owner.updateFormValue(prefix + 'mosaicoData', {
metadata,
model
});
@ -139,17 +140,17 @@ export function getTemplateTypes(t) {
mosaicoData: {}
}),
afterLoad: data => {
data.mosaicoFsTemplate = data.data.mosaicoFsTemplate;
data.mosaicoData = {
metadata: data.data.metadata,
model: data.data.model
data['mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate;
data[prefix + 'mosaicoData'] = {
metadata: data[prefix + 'data'].metadata,
model: data[prefix + 'data'].model
};
},
beforeSave: data => {
data.data = {
mosaicoFsTemplate: data.mosaicoFsTemplate,
metadata: data.mosaicoData.metadata,
model: data.mosaicoData.model
data[prefix + 'data'] = {
mosaicoFsTemplate: data[prefix + 'mosaicoFsTemplate'],
metadata: data[prefix + 'mosaicoData'].metadata,
model: data[prefix + 'mosaicoData'].model
};
clearBeforeSave(data);
},
@ -176,7 +177,7 @@ export function getTemplateTypes(t) {
templateTypes.ckeditor = {
typeName: t('CKEditor'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <CKEditor id="html" height="600px" label={t('Template content (HTML)')}/>,
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" label={t('Template content (HTML)')}/>,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
@ -190,7 +191,7 @@ export function getTemplateTypes(t) {
templateTypes.codeeditor = {
typeName: t('Code Editor'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <ACEEditor id="html" height="600px" mode="html" label={t('Template content (HTML)')}/>,
getHTMLEditor: owner => <ACEEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
@ -215,4 +216,101 @@ export function getTemplateTypes(t) {
};
return templateTypes;
}
}
export function getEditForm(owner, typeKey, prefix = '') {
const t = owner.props.t;
return <div>
<AlignedRow>
<Button className="btn-default" onClickAsync={::owner.toggleMergeTagReference} label={t('Merge tag reference')}/>
{owner.state.showMergeTagReference &&
<div style={{marginTop: '15px'}}>
<Trans><p>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>[TAG_NAME]</code> or <code>[TAG_NAME/fallback]</code> where <code>fallback</code> is an optional text value used when <code>TAG_NAME</code> is empty.</p></Trans>
<Trans><p>You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.</p></Trans>
<table className="table table-bordered table-condensed table-striped">
<thead>
<tr>
<th>
<Trans>Merge tag</Trans>
</th>
<th>
<Trans>Description</Trans>
</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">
[LINK_UNSUBSCRIBE]
</th>
<td>
<Trans>URL that points to the unsubscribe page</Trans>
</td>
</tr>
<tr>
<th scope="row">
[LINK_PREFERENCES]
</th>
<td>
<Trans>URL that points to the preferences page of the subscriber</Trans>
</td>
</tr>
<tr>
<th scope="row">
[LINK_BROWSER]
</th>
<td>
<Trans>URL to preview the message in a browser</Trans>
</td>
</tr>
<tr>
<th scope="row">
[EMAIL]
</th>
<td>
<Trans>Email address</Trans>
</td>
</tr>
<tr>
<th scope="row">
[SUBSCRIPTION_ID]
</th>
<td>
<Trans>Unique ID that identifies the recipient</Trans>
</td>
</tr>
<tr>
<th scope="row">
[LIST_ID]
</th>
<td>
<Trans>Unique ID that identifies the list used for this campaign</Trans>
</td>
</tr>
<tr>
<th scope="row">
[CAMPAIGN_ID]
</th>
<td>
<Trans>Unique ID that identifies current campaign</Trans>
</td>
</tr>
</tbody>
</table>
</div>}
</AlignedRow>
{owner.templateTypes[typeKey].getHTMLEditor(owner)}
<ACEEditor id={prefix + 'text'} height="400px" mode="text" label={t('Template content (plain text)')} help={<Trans>To extract the text from HTML click <ActionLink onClickAsync={::owner.extractPlainText}>here</ActionLink>. Please note that your existing plaintext in the field above will be overwritten. This feature uses the <a href="http://premailer.dialect.ca/api">Premailer API</a>, a third party service. Their Terms of Service and Privacy Policy apply.</Trans>}/>
</div>;
}
export function getTypeForm(owner, typeKey, isEdit) {
return <div>
{owner.templateTypes[typeKey].getTypeForm(this, isEdit)}
</div>;
}