First attempt on campaign editing. Misses attachments. Untested.
This commit is contained in:
parent
ee786bc8ad
commit
0e0fb944e3
26 changed files with 1244 additions and 233 deletions
503
client/src/campaigns/CUD.js
Normal file
503
client/src/campaigns/CUD.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
144
client/src/campaigns/List.js
Normal file
144
client/src/campaigns/List.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
client/src/campaigns/root.js
Normal file
66
client/src/campaigns/root.js
Normal 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
|
||||||
|
}
|
|
@ -151,6 +151,8 @@ export default class List extends Component {
|
||||||
dataUrl += '/' + this.props.segmentId;
|
dataUrl += '/' + this.props.segmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// FIXME - presents segments in a data table as in campaign edit
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import blacklist from './blacklist/root';
|
||||||
import lists from './lists/root';
|
import lists from './lists/root';
|
||||||
import namespaces from './namespaces/root';
|
import namespaces from './namespaces/root';
|
||||||
import reports from './reports/root';
|
import reports from './reports/root';
|
||||||
|
import campaigns from './campaigns/root';
|
||||||
import templates from './templates/root';
|
import templates from './templates/root';
|
||||||
import users from './users/root';
|
import users from './users/root';
|
||||||
import sendConfigurations from './send-configurations/root';
|
import sendConfigurations from './send-configurations/root';
|
||||||
|
@ -45,7 +46,7 @@ class Root extends Component {
|
||||||
const t = props.t;
|
const t = props.t;
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const topLevelMenuKeys = ['lists', 'templates', 'reports'];
|
const topLevelMenuKeys = ['lists', 'templates', 'campaigns', 'reports'];
|
||||||
|
|
||||||
class MainMenu extends Component {
|
class MainMenu extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
@ -123,7 +124,8 @@ class Root extends Component {
|
||||||
...blacklist.getMenus(t),
|
...blacklist.getMenus(t),
|
||||||
...account.getMenus(t),
|
...account.getMenus(t),
|
||||||
...settings.getMenus(t),
|
...settings.getMenus(t),
|
||||||
...sendConfigurations.getMenus(t)
|
...sendConfigurations.getMenus(t),
|
||||||
|
...campaigns.getMenus(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ function getMenus(t) {
|
||||||
':sendConfigurationId([0-9]+)': {
|
':sendConfigurationId([0-9]+)': {
|
||||||
title: resolved => t('Template "{{name}}"', {name: resolved.sendConfiguration.name}),
|
title: resolved => t('Template "{{name}}"', {name: resolved.sendConfiguration.name}),
|
||||||
resolve: {
|
resolve: {
|
||||||
sendConfiguration: params => `rest/send-configurations/${params.sendConfigurationId}`
|
sendConfiguration: params => `rest/send-configurations-private/${params.sendConfigurationId}`
|
||||||
},
|
},
|
||||||
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
|
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
|
||||||
navs: {
|
navs: {
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {translate} from 'react-i18next';
|
||||||
Trans,
|
|
||||||
translate
|
|
||||||
} from 'react-i18next';
|
|
||||||
import {
|
import {
|
||||||
NavButton,
|
NavButton,
|
||||||
requiresAuthenticatedUser,
|
requiresAuthenticatedUser,
|
||||||
|
@ -13,8 +10,6 @@ import {
|
||||||
withPageHelpers
|
withPageHelpers
|
||||||
} from '../lib/page'
|
} from '../lib/page'
|
||||||
import {
|
import {
|
||||||
ACEEditor,
|
|
||||||
AlignedRow,
|
|
||||||
Button,
|
Button,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
@ -32,8 +27,11 @@ import {
|
||||||
} from '../lib/namespace';
|
} from '../lib/namespace';
|
||||||
import {DeleteModalDialog} from "../lib/modals";
|
import {DeleteModalDialog} from "../lib/modals";
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
import mailtrainConfig from 'mailtrainConfig';
|
||||||
import {getTemplateTypes} from './helpers';
|
import {
|
||||||
import {ActionLink} from "../lib/bootstrap-components";
|
getEditForm,
|
||||||
|
getTemplateTypes,
|
||||||
|
getTypeForm
|
||||||
|
} from './helpers';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import styles from "../lib/styles.scss";
|
import styles from "../lib/styles.scss";
|
||||||
import {getUrl} from "../lib/urls";
|
import {getUrl} from "../lib/urls";
|
||||||
|
@ -158,7 +156,6 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
const html = this.getFormValue('html');
|
const html = this.getFormValue('html');
|
||||||
if (!html) {
|
if (!html) {
|
||||||
alert('Missing HTML content');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,101 +200,15 @@ export default class CUD extends Component {
|
||||||
|
|
||||||
let editForm = null;
|
let editForm = null;
|
||||||
if (isEdit && typeKey) {
|
if (isEdit && typeKey) {
|
||||||
editForm = <div>
|
editForm = getEditForm(this, typeKey);
|
||||||
<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>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let typeForm = null;
|
let typeForm = null;
|
||||||
if (typeKey) {
|
if (typeKey) {
|
||||||
typeForm = <div>
|
typeForm = getTypeForm(this, typeKey, isEdit);
|
||||||
{this.templateTypes[typeKey].getTypeForm(this, isEdit)}
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
<div className={this.state.elementInFullscreen ? styles.withElementInFullscreen : ''}>
|
||||||
{canDelete &&
|
{canDelete &&
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, {Component} from 'react';
|
||||||
import { translate } from 'react-i18next';
|
import {translate} from 'react-i18next';
|
||||||
import { Icon } from '../lib/bootstrap-components';
|
import {Icon} from '../lib/bootstrap-components';
|
||||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton } from '../lib/page';
|
import {
|
||||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
NavButton,
|
||||||
import { Table } from '../lib/table';
|
requiresAuthenticatedUser,
|
||||||
import axios from '../lib/axios';
|
Title,
|
||||||
|
Toolbar,
|
||||||
|
withPageHelpers
|
||||||
|
} from '../lib/page';
|
||||||
|
import {
|
||||||
|
withAsyncErrorHandler,
|
||||||
|
withErrorHandling
|
||||||
|
} from '../lib/error-handling';
|
||||||
|
import {Table} from '../lib/table';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { getTemplateTypes } from './helpers';
|
import {getTemplateTypes} from './helpers';
|
||||||
import {checkPermissions} from "../lib/permissions";
|
import {checkPermissions} from "../lib/permissions";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
|
|
@ -17,19 +17,20 @@ import {
|
||||||
} from "../lib/mosaico";
|
} from "../lib/mosaico";
|
||||||
|
|
||||||
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
||||||
import {getTrustedUrl, getSandboxUrl} from "../lib/urls";
|
import {getSandboxUrl} from "../lib/urls";
|
||||||
import mailtrainConfig from 'mailtrainConfig';
|
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 = {};
|
const templateTypes = {};
|
||||||
|
|
||||||
function initFieldsIfMissing(mutState, templateType) {
|
function initFieldsIfMissing(mutState, templateType) {
|
||||||
const initVals = templateTypes[templateType].initData();
|
const initVals = templateTypes[templateType].initData();
|
||||||
|
|
||||||
for (const key in initVals) {
|
for (const key in initVals) {
|
||||||
if (!mutState.hasIn([key])) {
|
if (!mutState.hasIn([prefix + key])) {
|
||||||
mutState.setIn([key, 'value'], initVals[key]);
|
mutState.setIn([prefix + key, 'value'], initVals[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,43 +56,43 @@ export function getTemplateTypes(t) {
|
||||||
templateTypes.mosaico = {
|
templateTypes.mosaico = {
|
||||||
typeName: t('Mosaico'),
|
typeName: t('Mosaico'),
|
||||||
getTypeForm: (owner, isEdit) =>
|
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 =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content (HTML)')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<MosaicoEditor
|
<MosaicoEditor
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
initialModel={owner.getFormValue('mosaicoData').model}
|
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||||
initialMetadata={owner.getFormValue('mosaicoData').metadata}
|
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
|
||||||
templatePath={getSandboxUrl(`mosaico/templates/${owner.getFormValue('mosaicoTemplate')}/index.html`)}
|
templatePath={getSandboxUrl(`mosaico/templates/${owner.getFormValue(prefix + 'mosaicoTemplate')}/index.html`)}
|
||||||
entityTypeId={ResourceType.TEMPLATE}
|
entityTypeId={ResourceType.TEMPLATE}
|
||||||
title={t('Mosaico Template Designer')}
|
title={t('Mosaico Template Designer')}
|
||||||
onFullscreenAsync={::owner.setElementInFullscreen}/>
|
onFullscreenAsync={::owner.setElementInFullscreen}/>
|
||||||
</AlignedRow>,
|
</AlignedRow>,
|
||||||
exportHTMLEditorData: async owner => {
|
exportHTMLEditorData: async owner => {
|
||||||
const {html, metadata, model} = await owner.editorNode.exportState();
|
const {html, metadata, model} = await owner.editorNode.exportState();
|
||||||
owner.updateFormValue('html', html);
|
owner.updateFormValue(prefix + 'html', html);
|
||||||
owner.updateFormValue('mosaicoData', {
|
owner.updateFormValue(prefix + 'mosaicoData', {
|
||||||
metadata,
|
metadata,
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
initData: () => ({
|
initData: () => ({
|
||||||
mosaicoTemplate: '',
|
[prefix + 'mosaicoTemplate']: '',
|
||||||
mosaicoData: {}
|
[prefix + 'mosaicoData']: {}
|
||||||
}),
|
}),
|
||||||
afterLoad: data => {
|
afterLoad: data => {
|
||||||
data.mosaicoTemplate = data.data.mosaicoTemplate;
|
data[prefix + 'mosaicoTemplate'] = data[prefix + 'data'].mosaicoTemplate;
|
||||||
data.mosaicoData = {
|
data[prefix + 'mosaicoData'] = {
|
||||||
metadata: data.data.metadata,
|
metadata: data[prefix + 'data'].metadata,
|
||||||
model: data.data.model
|
model: data[prefix + 'data'].model
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeSave: data => {
|
beforeSave: data => {
|
||||||
data.data = {
|
data[prefix + 'data'] = {
|
||||||
mosaicoTemplate: data.mosaicoTemplate,
|
mosaicoTemplate: data[prefix + 'mosaicoTemplate'],
|
||||||
metadata: data.mosaicoData.metadata,
|
metadata: data[prefix + 'mosaicoData'].metadata,
|
||||||
model: data.mosaicoData.model
|
model: data[prefix + 'mosaicoData'].model
|
||||||
};
|
};
|
||||||
clearBeforeSave(data);
|
clearBeforeSave(data);
|
||||||
},
|
},
|
||||||
|
@ -99,11 +100,11 @@ export function getTemplateTypes(t) {
|
||||||
initFieldsIfMissing(mutState, 'mosaico');
|
initFieldsIfMissing(mutState, 'mosaico');
|
||||||
},
|
},
|
||||||
validate: state => {
|
validate: state => {
|
||||||
const mosaicoTemplate = state.getIn(['mosaicoTemplate', 'value']);
|
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
|
||||||
if (!mosaicoTemplate) {
|
if (!mosaicoTemplate) {
|
||||||
state.setIn(['mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
|
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
|
||||||
} else {
|
} else {
|
||||||
state.setIn(['mosaicoTemplate', 'error'], null);
|
state.setIn([prefix + 'mosaicoTemplate', 'error'], null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -113,23 +114,23 @@ export function getTemplateTypes(t) {
|
||||||
templateTypes.mosaicoWithFsTemplate = {
|
templateTypes.mosaicoWithFsTemplate = {
|
||||||
typeName: t('Mosaico with predefined templates'),
|
typeName: t('Mosaico with predefined templates'),
|
||||||
getTypeForm: (owner, isEdit) =>
|
getTypeForm: (owner, isEdit) =>
|
||||||
<Dropdown id="mosaicoFsTemplate" label={t('Mosaico Template')} options={mosaicoFsTemplatesOptions}/>,
|
<Dropdown id={prefix + 'mosaicoFsTemplate'} label={t('Mosaico Template')} options={mosaicoFsTemplatesOptions}/>,
|
||||||
getHTMLEditor: owner =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content (HTML)')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<MosaicoEditor
|
<MosaicoEditor
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
initialModel={owner.getFormValue('mosaicoData').model}
|
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||||
initialMetadata={owner.getFormValue('mosaicoData').metadata}
|
initialMetadata={owner.getFormValue(prefix + 'mosaicoData').metadata}
|
||||||
templatePath={getSandboxUrl(`public/mosaico/templates/${owner.getFormValue('mosaicoFsTemplate')}/index.html`)}
|
templatePath={getSandboxUrl(`public/mosaico/templates/${owner.getFormValue(prefix + 'mosaicoFsTemplate')}/index.html`)}
|
||||||
entityTypeId={ResourceType.TEMPLATE}
|
entityTypeId={ResourceType.TEMPLATE}
|
||||||
title={t('Mosaico Template Designer')}
|
title={t('Mosaico Template Designer')}
|
||||||
onFullscreenAsync={::owner.setElementInFullscreen}/>
|
onFullscreenAsync={::owner.setElementInFullscreen}/>
|
||||||
</AlignedRow>,
|
</AlignedRow>,
|
||||||
exportHTMLEditorData: async owner => {
|
exportHTMLEditorData: async owner => {
|
||||||
const {html, metadata, model} = await owner.editorNode.exportState();
|
const {html, metadata, model} = await owner.editorNode.exportState();
|
||||||
owner.updateFormValue('html', html);
|
owner.updateFormValue(prefix + 'html', html);
|
||||||
owner.updateFormValue('mosaicoData', {
|
owner.updateFormValue(prefix + 'mosaicoData', {
|
||||||
metadata,
|
metadata,
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
|
@ -139,17 +140,17 @@ export function getTemplateTypes(t) {
|
||||||
mosaicoData: {}
|
mosaicoData: {}
|
||||||
}),
|
}),
|
||||||
afterLoad: data => {
|
afterLoad: data => {
|
||||||
data.mosaicoFsTemplate = data.data.mosaicoFsTemplate;
|
data['mosaicoFsTemplate'] = data[prefix + 'data'].mosaicoFsTemplate;
|
||||||
data.mosaicoData = {
|
data[prefix + 'mosaicoData'] = {
|
||||||
metadata: data.data.metadata,
|
metadata: data[prefix + 'data'].metadata,
|
||||||
model: data.data.model
|
model: data[prefix + 'data'].model
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeSave: data => {
|
beforeSave: data => {
|
||||||
data.data = {
|
data[prefix + 'data'] = {
|
||||||
mosaicoFsTemplate: data.mosaicoFsTemplate,
|
mosaicoFsTemplate: data[prefix + 'mosaicoFsTemplate'],
|
||||||
metadata: data.mosaicoData.metadata,
|
metadata: data[prefix + 'mosaicoData'].metadata,
|
||||||
model: data.mosaicoData.model
|
model: data[prefix + 'mosaicoData'].model
|
||||||
};
|
};
|
||||||
clearBeforeSave(data);
|
clearBeforeSave(data);
|
||||||
},
|
},
|
||||||
|
@ -176,7 +177,7 @@ export function getTemplateTypes(t) {
|
||||||
templateTypes.ckeditor = {
|
templateTypes.ckeditor = {
|
||||||
typeName: t('CKEditor'),
|
typeName: t('CKEditor'),
|
||||||
getTypeForm: (owner, isEdit) => null,
|
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 => {},
|
exportHTMLEditorData: async owner => {},
|
||||||
initData: () => ({}),
|
initData: () => ({}),
|
||||||
afterLoad: data => {},
|
afterLoad: data => {},
|
||||||
|
@ -190,7 +191,7 @@ export function getTemplateTypes(t) {
|
||||||
templateTypes.codeeditor = {
|
templateTypes.codeeditor = {
|
||||||
typeName: t('Code Editor'),
|
typeName: t('Code Editor'),
|
||||||
getTypeForm: (owner, isEdit) => null,
|
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 => {},
|
exportHTMLEditorData: async owner => {},
|
||||||
initData: () => ({}),
|
initData: () => ({}),
|
||||||
afterLoad: data => {},
|
afterLoad: data => {},
|
||||||
|
@ -215,4 +216,101 @@ export function getTemplateTypes(t) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return templateTypes;
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -202,28 +202,28 @@ rootNamespaceRole="master"
|
||||||
[roles.namespace.master]
|
[roles.namespace.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "manageUsers"]
|
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "createCampaign", "manageUsers"]
|
||||||
|
|
||||||
[roles.namespace.master.children]
|
[roles.namespace.master.children]
|
||||||
sendConfiguration=["view", "edit", "delete", "share", "send", "overrideAllowed", "overrideAll"]
|
sendConfiguration=["viewPublic", "viewPrivate", "edit", "delete", "share", "sendWithoutOverrides", "sendWithAllowedOverrides", "sendWithAnyOverrides"]
|
||||||
list=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
list=["view", "edit", "delete", "share", "viewFields", "manageFields", "viewSubscriptions", "manageSubscriptions", "viewSegments", "manageSegments"]
|
||||||
customForm=["view", "edit", "delete", "share"]
|
customForm=["view", "edit", "delete", "share"]
|
||||||
campaign=["view", "edit", "delete", "share", "manageFiles"]
|
campaign=["view", "edit", "delete", "share", "viewFiles", "manageFiles", "manageAttachments", "send", "viewStats"]
|
||||||
template=["view", "edit", "delete", "share", "manageFiles"]
|
template=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
||||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||||
mosaicoTemplate=["view", "edit", "delete", "share", "manageFiles"]
|
mosaicoTemplate=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
||||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "manageUsers"]
|
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "createCampaign", "manageUsers"]
|
||||||
|
|
||||||
[roles.sendConfiguration.master]
|
[roles.sendConfiguration.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "send", "overrideAllowed", "overrideAll"]
|
permissions=["viewPublic", "viewPrivate", "edit", "delete", "share", "sendWithoutOverrides", "sendWithAllowedOverrides", "sendWithAnyOverrides"]
|
||||||
|
|
||||||
[roles.list.master]
|
[roles.list.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
permissions=["view", "edit", "delete", "share", "viewFields", "manageFields", "viewSubscriptions", "manageSubscriptions", "viewSegments", "manageSegments"]
|
||||||
|
|
||||||
[roles.customForm.master]
|
[roles.customForm.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
|
@ -233,12 +233,12 @@ permissions=["view", "edit", "delete", "share"]
|
||||||
[roles.campaign.master]
|
[roles.campaign.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles", "manageAttachments", "send", "viewStats"]
|
||||||
|
|
||||||
[roles.template.master]
|
[roles.template.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
||||||
|
|
||||||
[roles.report.master]
|
[roles.report.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
|
@ -253,7 +253,8 @@ permissions=["view", "edit", "delete", "share", "execute"]
|
||||||
[roles.mosaicoTemplate.master]
|
[roles.mosaicoTemplate.master]
|
||||||
name="Master"
|
name="Master"
|
||||||
description="All permissions"
|
description="All permissions"
|
||||||
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[roles.global.editor]
|
[roles.global.editor]
|
||||||
|
@ -317,3 +318,4 @@ permissions=[]
|
||||||
name="Editor"
|
name="Editor"
|
||||||
description="XXX"
|
description="XXX"
|
||||||
permissions=[]
|
permissions=[]
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ async function getOrCreateMailer(sendConfigurationId) {
|
||||||
if (!sendConfiguration) {
|
if (!sendConfiguration) {
|
||||||
sendConfiguration = await sendConfigurations.getSystemSendConfiguration();
|
sendConfiguration = await sendConfigurations.getSystemSendConfiguration();
|
||||||
} else {
|
} else {
|
||||||
sendConfiguration = await sendConfigurations.getById(contextHelpers.getAdminContext(), sendConfigurationId, false);
|
sendConfiguration = await sendConfigurations.getById(contextHelpers.getAdminContext(), sendConfigurationId, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = transports.get(sendConfiguration.id) || await _createTransport(sendConfiguration);
|
const transport = transports.get(sendConfiguration.id) || await _createTransport(sendConfiguration);
|
||||||
|
|
|
@ -3,18 +3,28 @@
|
||||||
const knex = require('../lib/knex');
|
const knex = require('../lib/knex');
|
||||||
const dtHelpers = require('../lib/dt-helpers');
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
const interoperableErrors = require('../shared/interoperable-errors');
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
const shortid = require('shortid');
|
||||||
const shares = require('./shares');
|
const shares = require('./shares');
|
||||||
|
const files = require('./files');
|
||||||
|
const { CampaignSource, CampaignType} = require('../shared/campaigns');
|
||||||
|
const segments = require('./segments');
|
||||||
|
|
||||||
async function listDTAjax(params) {
|
const allowedKeysCommon = ['name', 'description', 'list', 'segment', 'namespace',
|
||||||
|
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override',
|
||||||
|
'source', 'data', 'click_tracking_disabled', 'open_tracking_disabled'];
|
||||||
|
|
||||||
|
const allowedKeysCreate = new Set(['type', ...allowedKeysCommon]);
|
||||||
|
const allowedKeysUpdate = new Set([...allowedKeysCommon]);
|
||||||
|
|
||||||
|
async function listDTAjax(context, params) {
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
context,
|
context,
|
||||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||||
params,
|
params,
|
||||||
builder => builder.from('campaigns')
|
builder => builder.from('campaigns')
|
||||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'),
|
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'),
|
||||||
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.status', 'campaigns.created']
|
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getById(context, id) {
|
async function getById(context, id) {
|
||||||
|
@ -26,6 +36,122 @@ async function getById(context, id) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _validateAndPreprocess(tx, context, entity, isCreate) {
|
||||||
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
|
|
||||||
|
if (isCreate) {
|
||||||
|
enforce(entity.type === CampaignType.REGULAR && entity.type === CampaignType.RSS && entity.type === CampaignType.TRIGGERED, 'Unknown campaign type');
|
||||||
|
}
|
||||||
|
|
||||||
|
enforce(entity.source >= CampaignSource.MIN && entity.source <= CampaignSource.MAX, 'Unknown campaign source');
|
||||||
|
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.list, 'view');
|
||||||
|
|
||||||
|
if (entity.segment) {
|
||||||
|
// Check that the segment under the list exists
|
||||||
|
await segments.getByIdTx(tx, context, entity.list, entity.segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.source === CampaignSource.TEMPLATE || (isCreate && entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'template', entity.data.sourceTemplate, 'view');
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.data = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(context, entity) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
|
||||||
|
|
||||||
|
let copyFilesFromTemplateId;
|
||||||
|
if (entity.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||||
|
copyFilesFromTemplateId = entity.data.sourceTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _validateAndPreprocess(tx, context, entity, true);
|
||||||
|
|
||||||
|
const filteredEntity = filterObject(entity, allowedKeysCreate);
|
||||||
|
filteredEntity.cid = shortid.generate();
|
||||||
|
|
||||||
|
const ids = await tx('campaigns').insert(filteredEntity);
|
||||||
|
const id = ids[0];
|
||||||
|
|
||||||
|
await knex.schema.raw('CREATE TABLE `campaign__' + id + '` (\n' +
|
||||||
|
' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
|
||||||
|
' `list` int(10) unsigned NOT NULL,\n' +
|
||||||
|
' `segment` int(10) unsigned NOT NULL,\n' +
|
||||||
|
' `subscription` int(10) unsigned NOT NULL,\n' +
|
||||||
|
' `status` tinyint(4) unsigned NOT NULL DEFAULT \'0\',\n' +
|
||||||
|
' `response` varchar(255) DEFAULT NULL,\n' +
|
||||||
|
' `response_id` varchar(255) CHARACTER SET ascii DEFAULT NULL,\n' +
|
||||||
|
' `updated` timestamp NULL DEFAULT NULL,\n' +
|
||||||
|
' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
|
||||||
|
' PRIMARY KEY (`id`),\n' +
|
||||||
|
' UNIQUE KEY `list` (`list`,`segment`,`subscription`),\n' +
|
||||||
|
' KEY `created` (`created`),\n' +
|
||||||
|
' KEY `response_id` (`response_id`),\n' +
|
||||||
|
' KEY `status_index` (`status`),\n' +
|
||||||
|
' KEY `subscription_index` (`subscription`)\n' +
|
||||||
|
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n');
|
||||||
|
|
||||||
|
await knex.schema.raw('CREATE TABLE `campaign__tracker' + id + '` (\n' +
|
||||||
|
' `list` int(10) unsigned NOT NULL,\n' +
|
||||||
|
' `subscriber` int(10) unsigned NOT NULL,\n' +
|
||||||
|
' `link` int(10) NOT NULL,\n' +
|
||||||
|
' `ip` varchar(100) CHARACTER SET ascii DEFAULT NULL,\n' +
|
||||||
|
' `device_type` varchar(50) DEFAULT NULL,\n' +
|
||||||
|
' `country` varchar(2) CHARACTER SET ascii DEFAULT NULL,\n' +
|
||||||
|
' `count` int(11) unsigned NOT NULL DEFAULT \'1\',\n' +
|
||||||
|
' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
|
||||||
|
' PRIMARY KEY (`list`,`subscriber`,`link`),\n' +
|
||||||
|
' KEY `created_index` (`created`)\n' +
|
||||||
|
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n');
|
||||||
|
|
||||||
|
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
||||||
|
|
||||||
|
if (copyFilesFromTemplateId) {
|
||||||
|
files.copyAllTx(tx, context, 'template', copyFilesFromTemplateId, 'campaign', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateWithConsistencyCheck(context, entity) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.id, 'edit');
|
||||||
|
|
||||||
|
const existing = await tx('campaigns').where('id', entity.id).first();
|
||||||
|
if (!existing) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingHash = hash(existing);
|
||||||
|
if (existingHash !== entity.originalHash) {
|
||||||
|
throw new interoperableErrors.ChangedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _validateAndPreprocess(tx, context, entity, false);
|
||||||
|
|
||||||
|
await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
|
||||||
|
|
||||||
|
await tx('campaigns').where('id', entity.id).update(filterObject(entity, allowedKeysUpdate));
|
||||||
|
|
||||||
|
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(context, id) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
|
||||||
|
|
||||||
|
await tx('campaigns').where('id', id).del();
|
||||||
|
await knex.schema.dropTableIfExists('campaign__' + id);
|
||||||
|
await knex.schema.dropTableIfExists('campaign_tracker__' + id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
getById
|
getById
|
||||||
|
|
|
@ -200,7 +200,7 @@ function hash(entity) {
|
||||||
|
|
||||||
async function getById(context, listId, id) {
|
async function getById(context, listId, id) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewFields');
|
||||||
|
|
||||||
const entity = await tx('custom_fields').where({list: listId, id}).first();
|
const entity = await tx('custom_fields').where({list: listId, id}).first();
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@ async function listTx(tx, listId) {
|
||||||
|
|
||||||
async function list(context, listId) {
|
async function list(context, listId) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageFields', 'manageSegments', 'manageSubscriptions']);
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewFields']);
|
||||||
return await listTx(tx, listId);
|
return await listTx(tx, listId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -286,7 +286,7 @@ async function listByOrderListTx(tx, listId, extraColumns = []) {
|
||||||
|
|
||||||
async function listDTAjax(context, listId, params) {
|
async function listDTAjax(context, listId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewFields');
|
||||||
|
|
||||||
return await dtHelpers.ajaxListTx(
|
return await dtHelpers.ajaxListTx(
|
||||||
tx,
|
tx,
|
||||||
|
@ -330,7 +330,7 @@ async function listDTAjax(context, listId, params) {
|
||||||
|
|
||||||
async function listGroupedDTAjax(context, listId, params) {
|
async function listGroupedDTAjax(context, listId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewFields');
|
||||||
|
|
||||||
return await dtHelpers.ajaxListTx(
|
return await dtHelpers.ajaxListTx(
|
||||||
tx,
|
tx,
|
||||||
|
|
|
@ -32,7 +32,7 @@ function getFilesTable(type) {
|
||||||
|
|
||||||
async function listDTAjax(context, type, entityId, params) {
|
async function listDTAjax(context, type, entityId, params) {
|
||||||
enforceTypePermitted(type);
|
enforceTypePermitted(type);
|
||||||
await shares.enforceEntityPermission(context, type, entityId, 'manageFiles');
|
await shares.enforceEntityPermission(context, type, entityId, 'viewFiles');
|
||||||
return await dtHelpers.ajaxList(
|
return await dtHelpers.ajaxList(
|
||||||
params,
|
params,
|
||||||
builder => builder.from(getFilesTable(type)).where({entity: entityId}),
|
builder => builder.from(getFilesTable(type)).where({entity: entityId}),
|
||||||
|
@ -41,8 +41,9 @@ async function listDTAjax(context, type, entityId, params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function list(context, type, entityId) {
|
async function list(context, type, entityId) {
|
||||||
|
enforceTypePermitted(type);
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermission(context, type, entityId, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'viewFiles');
|
||||||
return await tx(getFilesTable(type)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
return await tx(getFilesTable(type)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -51,7 +52,7 @@ async function getFileById(context, type, id) {
|
||||||
enforceTypePermitted(type);
|
enforceTypePermitted(type);
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
const file = await tx(getFilesTable(type)).where('id', id).first();
|
const file = await tx(getFilesTable(type)).where('id', id).first();
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'viewFiles');
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,7 +70,7 @@ async function getFileById(context, type, id) {
|
||||||
async function _getFileBy(context, type, entityId, key, value) {
|
async function _getFileBy(context, type, entityId, key, value) {
|
||||||
enforceTypePermitted(type);
|
enforceTypePermitted(type);
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'viewFiles');
|
||||||
const file = await tx(getFilesTable(type)).where({entity: entityId, [key]: value}).first();
|
const file = await tx(getFilesTable(type)).where({entity: entityId, [key]: value}).first();
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
@ -212,6 +213,8 @@ async function createFiles(context, type, entityId, files, getUrl = null, dontRe
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFile(context, type, id) {
|
async function removeFile(context, type, id) {
|
||||||
|
enforceTypePermitted(type);
|
||||||
|
|
||||||
const file = await knex.transaction(async tx => {
|
const file = await knex.transaction(async tx => {
|
||||||
const file = await tx(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
|
const file = await tx(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
|
||||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
||||||
|
@ -223,6 +226,27 @@ async function removeFile(context, type, id) {
|
||||||
await fs.removeAsync(filePath);
|
await fs.removeAsync(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyAllTx(tx, context, fromType, fromEntityId, toType, toEntityId) {
|
||||||
|
enforceTypePermitted(fromType);
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, fromType, fromEntityId, 'viewFiles');
|
||||||
|
|
||||||
|
enforceTypePermitted(toType);
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, toType, toEntityId, 'manageFiles');
|
||||||
|
|
||||||
|
const rows = await tx(getFilesTable(fromType)).where({entity: fromEntityId});
|
||||||
|
for (const row of rows) {
|
||||||
|
const fromFilePath = getFilePath(fromType, fromEntityId, row.filename);
|
||||||
|
const toFilePath = getFilePath(toType, toEntityId, row.filename);
|
||||||
|
await fs.copyAsync(fromFilePath, toFilePath, {});
|
||||||
|
|
||||||
|
delete row.id;
|
||||||
|
row.entity = toEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx(getFilesTable(toType)).insert(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
filesDir,
|
filesDir,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
|
@ -234,5 +258,6 @@ module.exports = {
|
||||||
createFiles,
|
createFiles,
|
||||||
removeFile,
|
removeFile,
|
||||||
getFileUrl,
|
getFileUrl,
|
||||||
getFilePath
|
getFilePath,
|
||||||
|
copyAllTx
|
||||||
};
|
};
|
|
@ -66,12 +66,16 @@ async function getByCid(context, cid) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _validateAndPreprocess(tx, entity) {
|
||||||
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
|
enforce(entity.unsubscription_mode >= UnsubscriptionMode.MIN && entity.unsubscription_mode <= UnsubscriptionMode.MAX, 'Unknown unsubscription mode');
|
||||||
|
}
|
||||||
|
|
||||||
async function create(context, entity) {
|
async function create(context, entity) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createList');
|
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createList');
|
||||||
|
|
||||||
await namespaceHelpers.validateEntity(tx, entity);
|
await _validateAndPreprocess(tx, entity);
|
||||||
enforce(entity.unsubscription_mode >= 0 && entity.unsubscription_mode < UnsubscriptionMode.MAX, 'Unknown unsubscription mode');
|
|
||||||
|
|
||||||
const filteredEntity = filterObject(entity, allowedKeys);
|
const filteredEntity = filterObject(entity, allowedKeys);
|
||||||
filteredEntity.cid = shortid.generate();
|
filteredEntity.cid = shortid.generate();
|
||||||
|
@ -79,9 +83,34 @@ async function create(context, entity) {
|
||||||
const ids = await tx('lists').insert(filteredEntity);
|
const ids = await tx('lists').insert(filteredEntity);
|
||||||
const id = ids[0];
|
const id = ids[0];
|
||||||
|
|
||||||
await knex.schema.raw('CREATE TABLE `subscription__' + id + '` LIKE subscription');
|
await knex.schema.raw('CREATE TABLE `subscription__' + id + '` (\n' +
|
||||||
|
' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
|
||||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: id });
|
' `cid` varchar(255) CHARACTER SET ascii NOT NULL,\n' +
|
||||||
|
' `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT \'\',\n' +
|
||||||
|
' `opt_in_ip` varchar(100) DEFAULT NULL,\n' +
|
||||||
|
' `opt_in_country` varchar(2) DEFAULT NULL,\n' +
|
||||||
|
' `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,\n' +
|
||||||
|
' `imported` int(11) unsigned DEFAULT NULL,\n' +
|
||||||
|
' `status` tinyint(4) unsigned NOT NULL DEFAULT \'1\',\n' +
|
||||||
|
' `is_test` tinyint(4) unsigned NOT NULL DEFAULT \'0\',\n' +
|
||||||
|
' `status_change` timestamp NULL DEFAULT NULL,\n' +
|
||||||
|
' `latest_open` timestamp NULL DEFAULT NULL,\n' +
|
||||||
|
' `latest_click` timestamp NULL DEFAULT NULL,\n' +
|
||||||
|
' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
|
||||||
|
' `first_name` varchar(255) DEFAULT NULL,\n' +
|
||||||
|
' `last_name` varchar(255) DEFAULT NULL,\n' +
|
||||||
|
' PRIMARY KEY (`id`),\n' +
|
||||||
|
' UNIQUE KEY `email` (`email`),\n' +
|
||||||
|
' UNIQUE KEY `cid` (`cid`),\n' +
|
||||||
|
' KEY `status` (`status`),\n' +
|
||||||
|
' KEY `first_name` (`first_name`(191)),\n' +
|
||||||
|
' KEY `last_name` (`last_name`(191)),\n' +
|
||||||
|
' KEY `subscriber_tz` (`tz`),\n' +
|
||||||
|
' KEY `is_test` (`is_test`),\n' +
|
||||||
|
' KEY `latest_open` (`latest_open`),\n' +
|
||||||
|
' KEY `latest_click` (`latest_click`),\n' +
|
||||||
|
' KEY `created` (`created`)\n' +
|
||||||
|
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n');
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
});
|
});
|
||||||
|
@ -101,9 +130,9 @@ async function updateWithConsistencyCheck(context, entity) {
|
||||||
throw new interoperableErrors.ChangedError();
|
throw new interoperableErrors.ChangedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
await namespaceHelpers.validateEntity(tx, entity);
|
await _validateAndPreprocess(tx, entity);
|
||||||
|
|
||||||
await namespaceHelpers.validateMove(context, entity, existing, 'list', 'createList', 'delete');
|
await namespaceHelpers.validateMove(context, entity, existing, 'list', 'createList', 'delete');
|
||||||
enforce(entity.unsubscription_mode >= 0 && entity.unsubscription_mode < UnsubscriptionMode.MAX, 'Unknown unsubscription mode');
|
|
||||||
|
|
||||||
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||||
|
|
||||||
|
|
|
@ -214,7 +214,7 @@ function hash(entity) {
|
||||||
|
|
||||||
async function listDTAjax(context, listId, params) {
|
async function listDTAjax(context, listId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSegments');
|
||||||
|
|
||||||
return await dtHelpers.ajaxListTx(
|
return await dtHelpers.ajaxListTx(
|
||||||
tx,
|
tx,
|
||||||
|
@ -229,18 +229,27 @@ async function listDTAjax(context, listId, params) {
|
||||||
|
|
||||||
async function listIdName(context, listId) {
|
async function listIdName(context, listId) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageSegments']);
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSegments']);
|
||||||
|
|
||||||
return await tx('segments').select(['id', 'name']).where('list', listId).orderBy('name', 'asc');
|
return await tx('segments').select(['id', 'name']).where('list', listId).orderBy('name', 'asc');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getByIdTx(tx, context, listId, id) {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSegments');
|
||||||
|
const entity = await tx('segments').where({id, list: listId}).first();
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.settings = JSON.parse(entity.settings);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
async function getById(context, listId, id) {
|
async function getById(context, listId, id) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
return getByIdTx(tx, context, listId, id);
|
||||||
const entity = await tx('segments').where({id, list: listId}).first();
|
|
||||||
entity.settings = JSON.parse(entity.settings);
|
|
||||||
return entity;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,6 +330,8 @@ async function updateWithConsistencyCheck(context, listId, entity) {
|
||||||
async function removeTx(tx, context, listId, id) {
|
async function removeTx(tx, context, listId, id) {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSegments');
|
||||||
|
|
||||||
|
// FIXME - check dependencies: campaigns
|
||||||
|
|
||||||
// The listId "where" is here to prevent deleting segment of a list for which a user does not have permission
|
// The listId "where" is here to prevent deleting segment of a list for which a user does not have permission
|
||||||
await tx('segments').where({list: listId, id}).del();
|
await tx('segments').where({list: listId, id}).del();
|
||||||
}
|
}
|
||||||
|
@ -402,6 +413,7 @@ Object.assign(module.exports, {
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
listIdName,
|
listIdName,
|
||||||
getById,
|
getById,
|
||||||
|
getByIdTx,
|
||||||
create,
|
create,
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
remove,
|
remove,
|
||||||
|
|
|
@ -21,7 +21,7 @@ function hash(entity) {
|
||||||
async function listDTAjax(context, params) {
|
async function listDTAjax(context, params) {
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
context,
|
context,
|
||||||
[{ entityTypeId: 'sendConfiguration', requiredOperations: ['view'] }],
|
[{ entityTypeId: 'sendConfiguration', requiredOperations: ['viewPublic'] }],
|
||||||
params,
|
params,
|
||||||
builder => builder
|
builder => builder
|
||||||
.from('send_configurations')
|
.from('send_configurations')
|
||||||
|
@ -30,11 +30,20 @@ async function listDTAjax(context, params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getById(context, id, withPermissions = true) {
|
async function getById(context, id, withPermissions = true, withPrivateData = true) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', id, 'view');
|
let entity;
|
||||||
const entity = await tx('send_configurations').where('id', id).first();
|
|
||||||
entity.mailer_settings = JSON.parse(entity.mailer_settings);
|
if (withPrivateData) {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', id, 'viewPrivate');
|
||||||
|
entity = await tx('send_configurations').where('id', id).first();
|
||||||
|
entity.mailer_settings = JSON.parse(entity.mailer_settings);
|
||||||
|
} else {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', id, 'viewPublic');
|
||||||
|
entity = await tx('send_configurations').where('id', id).select(
|
||||||
|
['id', 'name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'subject', 'subject_overridable']
|
||||||
|
).first();
|
||||||
|
}
|
||||||
|
|
||||||
// note that permissions are optional as as this methods may be used with synthetic admin context
|
// note that permissions are optional as as this methods may be used with synthetic admin context
|
||||||
if (withPermissions) {
|
if (withPermissions) {
|
||||||
|
|
|
@ -415,7 +415,7 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((isCreate && !(meta && meta.update)) || 'status' in entity) {
|
if ((isCreate && !(meta && meta.update)) || 'status' in entity) {
|
||||||
enforce(entity.status >= 0 && entity.status < SubscriptionStatus.MAX, 'Invalid status');
|
enforce(entity.status >= SubscriptionStatus.MIN && entity.status <= SubscriptionStatus.MAX, 'Invalid status');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in groupedFieldsMap) {
|
for (const key in groupedFieldsMap) {
|
||||||
|
|
|
@ -40,6 +40,10 @@ async function listDTAjax(context, params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _validateAndPreprocess(tx, entity) {
|
async function _validateAndPreprocess(tx, entity) {
|
||||||
|
await namespaceHelpers.validateEntity(tx, entity);
|
||||||
|
|
||||||
|
// We don't check contents of the "data" because it is processed solely on the client. The client generates the HTML code we use when sending out campaigns.
|
||||||
|
|
||||||
entity.data = JSON.stringify(entity.data);
|
entity.data = JSON.stringify(entity.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,8 +53,6 @@ async function create(context, entity) {
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, entity);
|
await _validateAndPreprocess(tx, entity);
|
||||||
|
|
||||||
await namespaceHelpers.validateEntity(tx, entity);
|
|
||||||
|
|
||||||
const ids = await tx('templates').insert(filterObject(entity, allowedKeys));
|
const ids = await tx('templates').insert(filterObject(entity, allowedKeys));
|
||||||
const id = ids[0];
|
const id = ids[0];
|
||||||
|
|
||||||
|
@ -78,7 +80,6 @@ async function updateWithConsistencyCheck(context, entity) {
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, entity);
|
await _validateAndPreprocess(tx, entity);
|
||||||
|
|
||||||
await namespaceHelpers.validateEntity(tx, entity);
|
|
||||||
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');
|
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');
|
||||||
|
|
||||||
await tx('templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
await tx('templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||||
|
|
|
@ -10,5 +10,28 @@ router.postAsync('/campaigns-table', passport.loggedIn, async (req, res) => {
|
||||||
return res.json(await campaigns.listDTAjax(req.context, req.body));
|
return res.json(await campaigns.listDTAjax(req.context, req.body));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.getAsync('/campaings/:campaignId', passport.loggedIn, async (req, res) => {
|
||||||
|
const campaign = await campaigns.getById(req.context, req.params.campaignId);
|
||||||
|
campaign.hash = campaigns.hash(campaign);
|
||||||
|
return res.json(campaign);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/campaigns', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
return res.json(await campaigns.create(req.context, req.body));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.putAsync('/campaigns/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
const entity = req.body;
|
||||||
|
entity.id = parseInt(req.params.campaignId);
|
||||||
|
|
||||||
|
await campaigns.updateWithConsistencyCheck(req.context, entity);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.deleteAsync('/campaigns/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
await campaigns.remove(req.context, req.params.campaignId);
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
|
@ -6,8 +6,14 @@ const sendConfigurations = require('../../models/send-configurations');
|
||||||
const router = require('../../lib/router-async').create();
|
const router = require('../../lib/router-async').create();
|
||||||
|
|
||||||
|
|
||||||
router.getAsync('/send-configurations/:sendConfigurationId', passport.loggedIn, async (req, res) => {
|
router.getAsync('/send-configurations-private/:sendConfigurationId', passport.loggedIn, async (req, res) => {
|
||||||
const sendConfiguration = await sendConfigurations.getById(req.context, req.params.sendConfigurationId);
|
const sendConfiguration = await sendConfigurations.getById(req.context, req.params.sendConfigurationId, true, true);
|
||||||
|
sendConfiguration.hash = sendConfigurations.hash(sendConfiguration);
|
||||||
|
return res.json(sendConfiguration);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.getAsync('/send-configurations-public/:sendConfigurationId', passport.loggedIn, async (req, res) => {
|
||||||
|
const sendConfiguration = await sendConfigurations.getById(req.context, req.params.sendConfigurationId, true, false);
|
||||||
sendConfiguration.hash = sendConfigurations.hash(sendConfiguration);
|
sendConfiguration.hash = sendConfigurations.hash(sendConfiguration);
|
||||||
return res.json(sendConfiguration);
|
return res.json(sendConfiguration);
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,7 +44,7 @@ OK | namespace | int(10) unsigned | NO | MUL | NULL
|
||||||
New columns:
|
New columns:
|
||||||
+-------------------------+---------------------+------+-----+-------------------+----------------+
|
+-------------------------+---------------------+------+-----+-------------------+----------------+
|
||||||
| data | longtext | NO | | NULL | |
|
| data | longtext | NO | | NULL | |
|
||||||
| source_type | int(10) unsigned | NO | | | |
|
| source | int(10) unsigned | NO | | | |
|
||||||
| send_configuration | int(10) unsigned | NO | | | |
|
| send_configuration | int(10) unsigned | NO | | | |
|
||||||
+-------------------------+---------------------+------+-----+-------------------+----------------+
|
+-------------------------+---------------------+------+-----+-------------------+----------------+
|
||||||
|
|
||||||
|
@ -55,38 +55,13 @@ scheduled - used only for campaign type NORMAL
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { getSystemSendConfigurationId } = require('../../../shared/send-configurations');
|
const { getSystemSendConfigurationId } = require('../../../shared/send-configurations');
|
||||||
|
const { CampaignSource, CampaignType} = require('../../../shared/campaigns');
|
||||||
const CampaignSource = {
|
|
||||||
TEMPLATE: 1,
|
|
||||||
CUSTOM: 2,
|
|
||||||
URL: 3,
|
|
||||||
RSS: 4
|
|
||||||
};
|
|
||||||
|
|
||||||
const CampaignType = {
|
|
||||||
NORMAL: 1,
|
|
||||||
RSS: 2,
|
|
||||||
RSS_ENTRY: 3,
|
|
||||||
TRIGGERED: 4
|
|
||||||
};
|
|
||||||
|
|
||||||
const CampaignStatus = {
|
|
||||||
// For campaign types: NORMAL, RSS_ENTRY
|
|
||||||
IDLE: 1,
|
|
||||||
SCHEDULED: 2,
|
|
||||||
FINISHED: 3,
|
|
||||||
PAUSED: 4,
|
|
||||||
|
|
||||||
// For campaign types: RSS, TRIGGERED
|
|
||||||
INACTIVE: 5,
|
|
||||||
ACTIVE: 6
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
|
|
||||||
await knex.schema.table('campaigns', table => {
|
await knex.schema.table('campaigns', table => {
|
||||||
table.text('data', 'longtext');
|
table.text('data', 'longtext');
|
||||||
table.integer('source_type').unsigned().notNullable();
|
table.integer('source').unsigned().notNullable();
|
||||||
|
|
||||||
// Add a default values, such that the new column has some valid non-null value
|
// Add a default values, such that the new column has some valid non-null value
|
||||||
table.integer('send_configuration').unsigned().notNullable().references(`send_configurations.id`).defaultTo(getSystemSendConfigurationId());
|
table.integer('send_configuration').unsigned().notNullable().references(`send_configurations.id`).defaultTo(getSystemSendConfigurationId());
|
||||||
|
@ -97,7 +72,7 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
for (const campaign of campaigns) {
|
for (const campaign of campaigns) {
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
if (campaign.type === CampaignType.NORMAL || campaign.type === CampaignType.RSS_ENTRY || campaign.type === CampaignType.NORMAL || campaign.type === CampaignType.TRIGGERED) {
|
if (campaign.type === CampaignType.REGULAR || campaign.type === CampaignType.RSS_ENTRY || campaign.type === CampaignType.REGULAR || campaign.type === CampaignType.TRIGGERED) {
|
||||||
if (campaign.template) {
|
if (campaign.template) {
|
||||||
let editorType = campaign.editor_name;
|
let editorType = campaign.editor_name;
|
||||||
const editorData = JSON.parse(campaign.editor_data || '{}');
|
const editorData = JSON.parse(campaign.editor_data || '{}');
|
||||||
|
@ -106,21 +81,26 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
editorType = 'ckeditor';
|
editorType = 'ckeditor';
|
||||||
}
|
}
|
||||||
|
|
||||||
campaign.source_type = CampaignSource.CUSTOM;
|
campaign.source = CampaignSource.CUSTOM_FROM_TEMPLATE;
|
||||||
data.source = {
|
data.sourceCustom = {
|
||||||
type: editorType,
|
type: editorType,
|
||||||
data: editorData,
|
data: editorData,
|
||||||
html: campaign.html,
|
html: campaign.html,
|
||||||
text: campaign.text,
|
text: campaign.text,
|
||||||
htmlPrepared: campaign.html_prepared
|
htmlPrepared: campaign.html_prepared
|
||||||
};
|
};
|
||||||
|
|
||||||
|
data.sourceTemplate = campaign.template;
|
||||||
|
|
||||||
|
// For source === CampaignSource.TEMPLATE, the data is as follows:
|
||||||
|
// data.sourceTemplate = <template id>
|
||||||
} else {
|
} else {
|
||||||
campaign.source_type = CampaignSource.URL;
|
campaign.source = CampaignSource.URL;
|
||||||
data.sourceUrl = campaign.source_url;
|
data.sourceUrl = campaign.source_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (campaign.type === CampaignType.RSS) {
|
} else if (campaign.type === CampaignType.RSS) {
|
||||||
campaign.source_type = CampaignSource.RSS;
|
campaign.source = CampaignSource.RSS;
|
||||||
data.feedUrl = campaign.source_url;
|
data.feedUrl = campaign.source_url;
|
||||||
|
|
||||||
data.checkStatus = campaign.checkStatus;
|
data.checkStatus = campaign.checkStatus;
|
||||||
|
@ -150,6 +130,8 @@ exports.up = (knex, Promise) => (async() => {
|
||||||
table.integer('send_configuration').unsigned().notNullable().alter();
|
table.integer('send_configuration').unsigned().notNullable().alter();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists('campaign');
|
||||||
|
await knex.schema.dropTableIfExists('campaign_tracker');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
exports.up = (knex, Promise) => (async() => {
|
||||||
|
await knex.schema.dropTableIfExists('subscription');
|
||||||
|
})();
|
||||||
|
|
||||||
|
exports.down = (knex, Promise) => (async() => {
|
||||||
|
})();
|
47
shared/campaigns.js
Normal file
47
shared/campaigns.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const CampaignSource = {
|
||||||
|
MIN: 1,
|
||||||
|
|
||||||
|
TEMPLATE: 1,
|
||||||
|
CUSTOM: 2,
|
||||||
|
CUSTOM_FROM_TEMPLATE: 3,
|
||||||
|
URL: 4,
|
||||||
|
RSS: 5,
|
||||||
|
|
||||||
|
MAX: 6
|
||||||
|
};
|
||||||
|
|
||||||
|
const CampaignType = {
|
||||||
|
MIN: 1,
|
||||||
|
|
||||||
|
REGULAR: 1,
|
||||||
|
RSS: 2,
|
||||||
|
RSS_ENTRY: 3,
|
||||||
|
TRIGGERED: 4,
|
||||||
|
|
||||||
|
MAX: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
const CampaignStatus = {
|
||||||
|
MIN: 1,
|
||||||
|
|
||||||
|
// For campaign types: NORMAL, RSS_ENTRY
|
||||||
|
IDLE: 1,
|
||||||
|
SCHEDULED: 2,
|
||||||
|
FINISHED: 3,
|
||||||
|
PAUSED: 4,
|
||||||
|
|
||||||
|
// For campaign types: RSS, TRIGGERED
|
||||||
|
INACTIVE: 5,
|
||||||
|
ACTIVE: 6,
|
||||||
|
|
||||||
|
MAA: 6
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CampaignSource,
|
||||||
|
CampaignType,
|
||||||
|
CampaignStatus
|
||||||
|
};
|
|
@ -1,20 +1,26 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const UnsubscriptionMode = {
|
const UnsubscriptionMode = {
|
||||||
|
MIN: 0,
|
||||||
|
|
||||||
ONE_STEP: 0,
|
ONE_STEP: 0,
|
||||||
ONE_STEP_WITH_FORM: 1,
|
ONE_STEP_WITH_FORM: 1,
|
||||||
TWO_STEP: 2,
|
TWO_STEP: 2,
|
||||||
TWO_STEP_WITH_FORM: 3,
|
TWO_STEP_WITH_FORM: 3,
|
||||||
MANUAL: 4,
|
MANUAL: 4,
|
||||||
MAX: 5
|
|
||||||
|
MAX: 4
|
||||||
};
|
};
|
||||||
|
|
||||||
const SubscriptionStatus = {
|
const SubscriptionStatus = {
|
||||||
|
MIN: 0,
|
||||||
|
|
||||||
SUBSCRIBED: 1,
|
SUBSCRIBED: 1,
|
||||||
UNSUBSCRIBED: 2,
|
UNSUBSCRIBED: 2,
|
||||||
BOUNCED: 3,
|
BOUNCED: 3,
|
||||||
COMPLAINED: 4,
|
COMPLAINED: 4,
|
||||||
MAX: 5
|
|
||||||
|
MAX: 4
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFieldKey(field) {
|
function getFieldKey(field) {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const ReportState = {
|
const ReportState = {
|
||||||
|
MIN: 0,
|
||||||
|
|
||||||
SCHEDULED: 0,
|
SCHEDULED: 0,
|
||||||
PROCESSING: 1,
|
PROCESSING: 1,
|
||||||
FINISHED: 2,
|
FINISHED: 2,
|
||||||
FAILED: 3,
|
FAILED: 3,
|
||||||
MAX: 4
|
|
||||||
|
MAX: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue