Editing of triggers seems to work.

Some further fixes.
This commit is contained in:
Tomas Bures 2018-08-04 15:00:37 +05:30
parent ffc26a4836
commit 965f30cea7
23 changed files with 855 additions and 377 deletions

13
UPGRADE.md Normal file
View file

@ -0,0 +1,13 @@
## Migration from Mailtrain v1 to Mailtrain v2
The migration should almost happen automatically. There are however the following caveats:
1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`)
and update your configs accordingly.
2. Images uploaded in a template editor (Mosaico, GrapeJS, etc.) need to be manually moved to a new destination (under `client`).
For Mosaico, this means to move folders named by a number from `public/mosaico` to `client/public/mosaico`.
3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/public/mosaico/templates`.
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.

View file

@ -217,34 +217,28 @@ export default class CUD extends Component {
const t = this.props.t;
const isEdit = !!this.props.entity;
for (const key of state.keys()) {
state.setIn([key, 'error'], null);
}
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(['send_configuration', 'value'])) {
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
} else {
state.setIn(['send_configuration', '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);
}
@ -252,12 +246,6 @@ export default class CUD extends Component {
const sourceTypeKey = Number.parseInt(state.getIn(['source', 'value']));
for (const key of state.keys()) {
if (key.startsWith('data_')) {
state.setIn([key, 'error'], null);
}
}
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
if (!state.getIn(['data_sourceTemplate', 'value'])) {
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
@ -292,7 +280,6 @@ export default class CUD extends Component {
}
validateNamespace(t, state);
}
async submitHandler() {
@ -361,8 +348,11 @@ export default class CUD extends Component {
});
if (submitResponse) {
const sourceTypeKey = Number.parseInt(this.getFormValue('source'));
if (this.props.entity) {
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/content`, 'success', t('Campaign saved'));
} else {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
}
@ -494,6 +484,14 @@ export default class CUD extends Component {
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
}
let saveButtonLabel;
if (isEdit) {
saveButtonLabel = t('Save');
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
saveButtonLabel = t('Save and edit content');
} else {
saveButtonLabel = t('Save and edit campaign');
}
return (
<div>
@ -545,7 +543,7 @@ export default class CUD extends Component {
{templateEdit}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={isEdit ? t('Save') : t('Save and edit campaign')}/>
<Button type="submit" className="btn-primary" icon="ok" label={saveButtonLabel}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.entity.id}/delete`}/> }
</ButtonRow>
</Form>

View file

@ -97,6 +97,7 @@ export default class List extends Component {
actions: data => {
const actions = [];
const perms = data[9];
const campaignType = data[3];
const campaignSource = data[6];
if (perms.includes('edit')) {
@ -127,6 +128,13 @@ export default class List extends Component {
});
}
if (campaignType === CampaignType.TRIGGERED && perms.includes('viewTriggers')) {
actions.push({
label: <Icon icon="flash" title={t('Triggers')}/>,
link: `/campaigns/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,

View file

@ -7,7 +7,12 @@ import Content from './Content';
import CampaignsList from './List';
import Share from '../shares/Share';
import Files from "../lib/files";
import {CampaignSource, CampaignType} from "../../../shared/campaigns";
import {
CampaignSource,
CampaignType
} from "../../../shared/campaigns";
import TriggersCUD from './triggers/CUD';
import TriggersList from './triggers/List';
function getMenus(t) {
@ -51,6 +56,32 @@ function getMenus(t) {
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to resulting eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
},
triggers: {
title: t('Triggers'),
link: params => `/campaigns/${params.campaignId}/triggers/`,
visible: resolved => resolved.campaign.type === CampaignType.TRIGGERED && resolved.campaign.permissions.includes('viewTriggers'),
panelRender: props => <TriggersList campaign={props.resolved.campaign} />,
children: {
':triggerId([0-9]+)': {
title: resolved => t('Trigger "{{name}}"', {name: resolved.trigger.name}),
resolve: {
trigger: params => `rest/triggers/${params.campaignId}/${params.triggerId}`,
},
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
panelRender: props => <TriggersCUD action={props.match.params.action} entity={props.resolved.trigger} campaign={props.resolved.campaign} />
}
}
},
create: {
title: t('Create'),
panelRender: props => <TriggersCUD action="create" campaign={props.resolved.campaign} />
}
}
},
share: {
title: t('Share'),
link: params => `/campaigns/${params.campaignId}/share`,

View file

@ -0,0 +1,237 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../../lib/page';
import {
AlignedRow,
Button,
ButtonRow,
CheckBox,
Dropdown,
Form,
FormSendMethod,
InputField,
TableSelect,
TextArea,
withForm
} from '../../lib/form';
import {withErrorHandling} from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/modals";
import {getTriggerTypes} from './helpers';
import {
Entity,
Event
} from '../../../../shared/triggers';
import moment from 'moment';
import {getCampaignTypeLabels} from "../helpers";
@translate()
@withForm
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component {
constructor(props) {
super(props);
this.state = {};
this.campaignTypes = getCampaignTypeLabels(props.t);
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
this.entityLabels = entityLabels;
this.entityOptions = [
{key: Entity.SUBSCRIPTION, label: entityLabels[Entity.SUBSCRIPTION]},
{key: Entity.CAMPAIGN, label: entityLabels[Entity.CAMPAIGN]}
];
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
const CampaignEvent = Event[Entity.CAMPAIGN];
this.eventOptions = {
[Entity.SUBSCRIPTION]: [
{key: SubscriptionEvent.CREATED, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.CREATED]},
{key: SubscriptionEvent.LATEST_OPEN, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.LATEST_OPEN]},
{key: SubscriptionEvent.LATEST_CLICK, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.LATEST_CLICK]}
],
[Entity.CAMPAIGN]: [
{key: CampaignEvent.DELIVERED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.DELIVERED]},
{key: CampaignEvent.OPENED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.OPENED]},
{key: CampaignEvent.CLICKED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.CLICKED]},
{key: CampaignEvent.NOT_OPENED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.NOT_OPENED]},
{key: CampaignEvent.NOT_CLICKED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.NOT_CLICKED]}
]
};
this.initForm();
}
static propTypes = {
action: PropTypes.string.isRequired,
campaign: PropTypes.object,
entity: PropTypes.object
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
data.daysAfter = (Math.round(data.seconds_after / (3600 * 24))).toString();
if (data.entity === Entity.SUBSCRIPTION) {
data.subscriptionEvent = data.event;
} else {
data.subscriptionEvent = Event[Entity.SUBSCRIPTION].CREATED;
}
if (data.entity === Entity.CAMPAIGN) {
data.campaignEvent = data.event;
} else {
data.campaignEvent = Event[Entity.CAMPAIGN].DELIVERED;
}
});
} else {
this.populateFormValues({
name: '',
description: '',
entity: Entity.SUBSCRIPTION,
subscriptionEvent: Event[Entity.SUBSCRIPTION].CREATED,
campaignEvent: Event[Entity.CAMPAIGN].DELIVERED,
daysAfter: '',
enabled: true,
source_campaign: null
});
}
}
localValidateFormValues(state) {
const t = this.props.t;
const entityKey = state.getIn(['entity', 'value']);
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
const daysAfter = state.getIn(['daysAfter', 'value']).trim();
if (daysAfter === '') {
state.setIn(['daysAfter', 'error'], t('Values must not be empty'));
} else if (isNaN(daysAfter) || Number.parseInt(daysAfter) < 0) {
state.setIn(['daysAfter', 'error'], t('Value must be a non-negative number'));
} else {
state.setIn(['daysAfter', 'error'], null);
}
if (entityKey === Entity.CAMPAIGN && !state.getIn(['source_campaign', 'value'])) {
state.setIn(['source_campaign', 'error'], t('Source campaign must not be empty'));
} else {
state.setIn(['source_campaign', 'error'], null);
}
}
async submitHandler() {
const t = this.props.t;
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `rest/triggers/${this.props.campaign.id}`
}
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.seconds_after = Number.parseInt(data.daysAfter) * 3600 * 24;
if (data.entity === Entity.SUBSCRIPTION) {
data.event = data.subscriptionEvent;
} else if (data.entity === Entity.CAMPAIGN) {
data.event = data.campaignEvent;
}
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/campaigns/${this.props.campaign.id}/triggers`, 'success', t('Trigger saved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
}
} catch (error) {
throw error;
}
}
render() {
const t = this.props.t;
const isEdit = !!this.props.entity;
const entityKey = this.getFormValue('entity');
const campaignsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypes[data] },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') }
];
return (
<div>
{isEdit &&
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`}
cudUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`}
listUrl={`/campaigns/${this.props.campaign.id}/triggers`}
deletingMsg={t('Deleting trigger ...')}
deletedMsg={t('Trigger deleted')}/>
}
<Title>{isEdit ? t('Edit Trigger') : t('Create Trigger')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<Dropdown id="entity" label={t('Entity')} options={this.entityOptions} help={t('Select the type of the trigger rule.')}/>
<InputField id="daysAfter" label={t('Trigger fires')}/>
<AlignedRow>days after:</AlignedRow>
{entityKey === Entity.SUBSCRIPTION && <Dropdown id="subscriptionEvent" label={t('Event')} options={this.eventOptions[Entity.SUBSCRIPTION]} help={t('Select the event that triggers sending the campaign.')}/>}
{entityKey === Entity.CAMPAIGN && <Dropdown id="campaignEvent" label={t('Event')} options={this.eventOptions[Entity.CAMPAIGN]} help={t('Select the event that triggers sending the campaign.')}/>}
{entityKey === Entity.CAMPAIGN &&
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${this.props.campaign.list}`} columns={campaignsColumns} selectionLabelIndex={1} />
}
<CheckBox id="enabled" text={t('Enabled')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -0,0 +1,81 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
Toolbar,
withPageHelpers
} from '../../lib/page';
import {withErrorHandling} from '../../lib/error-handling';
import {Table} from '../../lib/table';
import {getTriggerTypes} from './helpers';
import {Icon} from "../../lib/bootstrap-components";
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
super(props);
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
this.entityLabels = entityLabels;
this.eventLabels = eventLabels;
this.state = {};
}
static propTypes = {
campaign: PropTypes.object
}
componentDidMount() {
}
render() {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('List') },
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
{
actions: data => {
const actions = [];
if (this.props.campaign.permissions.includes('manageTriggers')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
});
}
return actions;
}
}
];
return (
<div>
{this.props.campaign.permissions.includes('manageTriggers') &&
<Toolbar>
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
</Toolbar>
}
<Title>{t('Triggers')}</Title>
<Table withHeader dataUrl={`rest/triggers-by-campaign-table/${this.props.campaign.id}`} columns={columns} />
</div>
);
}
}

View file

@ -0,0 +1,35 @@
'use strict';
import {Entity, Event} from '../../../../shared/triggers';
export function getTriggerTypes(t) {
const entityLabels = {
[Entity.SUBSCRIPTION]: t('Subscription'),
[Entity.CAMPAIGN]: t('Campaign')
};
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
const CampaignEvent = Event[Entity.CAMPAIGN];
const eventLabels = {
[Entity.SUBSCRIPTION]: {
[SubscriptionEvent.CREATED]: t('Created'),
[SubscriptionEvent.LATEST_OPEN]: t('Latest open'),
[SubscriptionEvent.LATEST_CLICK]: t('Latest click')
},
[Entity.CAMPAIGN]: {
[CampaignEvent.DELIVERED]: t('Delivered'),
[CampaignEvent.OPENED]: t('Opened'),
[CampaignEvent.CLICKED]: t('Clicked'),
[CampaignEvent.NOT_OPENED]: t('Not opened'),
[CampaignEvent.NOT_CLICKED]: t('Not clicked')
}
};
return {
entityLabels,
eventLabels
};
}

View file

@ -1,8 +1,9 @@
'use strict';
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
import mailtrainConfig from "mailtrainConfig";
let restrictedAccessToken = 'ANONYMOUS';
let restrictedAccessToken = anonymousRestrictedAccessToken;
function setRestrictedAccessToken(token) {
restrictedAccessToken = token;
@ -28,7 +29,7 @@ function getBaseDir() {
if (mailtrainConfig.trusted) {
return mailtrainConfig.trustedUrlBaseDir;
} else {
return mailtrainConfig.sandboxUrlBaseDir + 'ANONYMOUS';
return mailtrainConfig.sandboxUrlBaseDir + anonymousRestrictedAccessToken;
}
}

View file

@ -47,7 +47,7 @@ export default class List extends Component {
data: 1,
title: t('Name'),
actions: data => {
const perms = data[6];
const perms = data[7];
if (perms.includes('viewSubscriptions')) {
return [{label: data[1], link: `/lists/${data[0]}/subscriptions`}];
} else {
@ -62,7 +62,9 @@ export default class List extends Component {
{
actions: data => {
const actions = [];
const perms = data[6];
const triggersCount = data[6];
const perms = data[7];
console.log(data);
if (perms.includes('viewSubscriptions')) {
actions.push({
@ -92,6 +94,13 @@ export default class List extends Component {
});
}
if (triggersCount > 0) {
actions.push({
label: <Icon icon="flash" title={t('Triggers')}/>,
link: `/lists/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,

View file

@ -0,0 +1,75 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import {
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page';
import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table';
import {getTriggerTypes} from '../campaigns/triggers/helpers';
import {Icon} from "../lib/bootstrap-components";
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
super(props);
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
this.entityLabels = entityLabels;
this.eventLabels = eventLabels;
this.state = {};
}
static propTypes = {
list: PropTypes.object
}
componentDidMount() {
}
render() {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Campaign') },
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
{
actions: data => {
const actions = [];
const perms = data[9];
const campaignId = data[8];
if (perms.includes('manageTriggers')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
});
}
return actions;
}
}
];
return (
<div>
<Title>{t('Triggers')}</Title>
<Table withHeader dataUrl={`rest/triggers-by-list-table/${this.props.list.id}`} columns={columns} />
</div>
);
}
}

View file

@ -13,7 +13,7 @@ import SubscriptionsCUD from './subscriptions/CUD';
import SegmentsList from './segments/List';
import SegmentsCUD from './segments/CUD';
import Share from '../shares/Share';
import TriggersList from './TriggersList';
function getMenus(t) {
return {
@ -127,6 +127,11 @@ function getMenus(t) {
}
}
},
triggers: {
title: t('Triggers'),
link: params => `/lists/${params.listId}/triggers`,
panelRender: props => <TriggersList list={props.resolved.list} />
},
share: {
title: t('Share'),
link: params => `/lists/${params.listId}/share`,

View file

@ -94,17 +94,17 @@ export default class CUD extends Component {
localValidateFormValues(state) {
const t = this.props.t;
for (const key of state.keys()) {
state.setIn([key, 'error'], null);
}
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
const typeKey = state.getIn(['type', 'value']);
if (!typeKey) {
state.setIn(['type', 'error'], t('Type must be selected'));
} else {
state.setIn(['type', 'error'], null);
}
validateNamespace(t, state);

View file

@ -112,14 +112,12 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
if (!mosaicoTemplate) {
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
} else {
state.setIn([prefix + 'mosaicoTemplate', 'error'], null);
}
}
};
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates.map(([key, label]) => ({key, label}));
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates);
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates;
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates.map(({key, label}) => ([key, label])));
templateTypes.mosaicoWithFsTemplate = {
typeName: t('Mosaico with predefined templates'),
@ -151,7 +149,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
});
},
initData: () => ({
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0][0],
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0].key,
[prefix + 'mosaicoData']: {}
}),
afterLoad: data => {

View file

@ -1,322 +0,0 @@
# This file is the default config file for Mailtrain. To use a environment specific
# configuration add new file {ENV}.{ext} (eg. "production.toml") to the same folder.
# {ENV} is defined by NODE_ENV environment variable.
#
# Do not modify this file directly, otherwise you might lose your modifications when upgrading
#
# You should only define the options you want to change in your additional config file.
# For example if the only thing you want to change is the port number for the www server
# then your additional config file should look like this:
# # production.toml
# [www]
# port=80
# or if you want to use Javascript instead of TOML then the same file could look like this:
# // production.js
# module.exports = {
# www: {
# port: 80
# }
# };
# Process title visible in monitoring logs and process listing
title="mailtrain"
# Enabled HTML editors
editors=["ckeditor", "codeeditor", "mosaico", "mosaicoWithFsTemplate"]
# Default language to use
language="en"
# Inject custom scripts in subscription/layout.mjml.hbs
# customSubscriptionScripts=["/custom/hello-world.js"]
# If you start out as a root user (eg. if you want to use ports lower than 1000)
# then you can downgrade the user once all services are up and running
#user="mailtrain"
#group="mailtrain"
# If Mailtrain is started as root, "Reports" feature drops the privileges of script generating the report to disallow
# any modifications of Mailtrain code and even prohibits reading the production configuration (which contains the MySQL
# password for read/write operations). The roUser/roGroup determines the user to be used
#roUser="nobody"
#roGroup="nogroup"
[log]
# silly|verbose|info|http|warn|error|silent
level="verbose"
[www]
# HTTP port to listen on
port=3000
# HTTP port to listen on for sandboxed requests
sandboxPort=8081
# HTTP interface to listen on
host="0.0.0.0"
# URL base for trusted urls. It must be absolute (starting with http:// or https://). If Mailtrain is served on
# a non-standard port (e.g. 3000), the URL must also specify the port.
trustedUrlBase="http://localhost:3000"
# URL base for sandbox urls. It must be absolute (starting with http:// or https://) and contain the sandbox port.
sandboxUrlBase="http://localhost:8081"
# Secret for signing the session ID cookie
secret="a cat"
# Session length in seconds when "remember me" is checked
remember=2592000 # 30 days
# logger interface for expressjs morgan
log="dev"
# Is the server behind a proxy? true/false
# Set this to true if you are serving Mailtrain as a virtual domain through Nginx or Apache
proxy=false
# maximum POST body size
postSize="2MB"
[mysql]
host="localhost"
user="mailtrain"
password="mailtrain"
database="mailtrain"
# Some installations, eg. MAMP can use a different port (8889)
# MAMP users should also turn on "Allow network access to MySQL" otherwise MySQL might not be accessible
port=3306
charset="utf8mb4"
# The timezone configured on the MySQL server. This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM
timezone="local"
[redis]
# enable to use Redis session cache or disable if Redis is not installed
enabled=false
host="localhost"
port=6379
db=5
# Uncomment if your Redis installation requires a password
#password=""
[verp]
# Enable to start an MX server that detects bounced messages using VERP
# In most cases you do not want to use it
# Requires root privileges
enabled=false
port=2525
host="0.0.0.0"
# With DMARC, the Return-Path and From address must match the same domain.
# By default we get around this by using the VERP address in the Sender header,
# with the side effect that some email clients diplay an ugly "on behalf of" message.
# You can safely disable this Sender header if you're not using DMARC or your
# VERP hostname is in the same domain as the From address.
# disablesenderheader=true
[ldap]
# enable to use ldap user backend
enabled=false
# method is ldapjs or ldapauth - it chooses the library to be used. If not given, it chooses the one present.
# method="ldapjs"
host="localhost"
port=3002
baseDN="ou=users,dc=company"
filter="(|(username={{username}})(mail={{username}}))"
# Username field in LDAP (uid/cn/username)
uidTag="username"
# nameTag identifies the attribute to be used for user's full name
nameTag="username"
passwordresetlink=""
newUserRole="master"
# Global namespace id
newUserNamespaceId=1
# Use a different user to bind LDAP (final bind DN will be: {{uidTag}}={{bindUser}},{{baseDN}})
bindUser="name@company.net"
bindPassword="mySecretPassword"
[postfixbounce]
# Enable to allow writing Postfix bounce log to Mailtrain listener
# If enabled, tail mail.log to Mailtrain with the following command:
# tail -f -n +0 /var/log/mail.log | nc localhost 5699 -
enabled=false
port=5699
# allow connections from localhost only
host="127.0.0.1"
# extra options for nodemailer
[nodemailer]
#textEncoding="base64"
[queue]
# How many parallel sender processes to spawn
# You can use more than 1 process only if you have Redis enabled
processes=1
[cors]
# Allow subscription widgets to be embedded
# origins=['https://www.example.com']
[mosaico]
# Installed templates
fsTemplates=[["versafix-1", "Versafix One"]]
# Inject custom scripts
# customscripts=["/mosaico/custom/my-mosaico-plugin.js"]
[grapejs]
# Installed templates
templates=[
["demo", "HTML Template"],
["aves", "MJML Template"]
]
[reports]
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
# properly protected.
# Reports rely on custom user defined Javascript snippets defined in the report template. The snippets are run on the
# server when generating a report. As these snippets are stored in the DB, they pose a security risk because they can
# help gaining access to the server if the DB cannot
# be properly protected (e.g. if it is shared with another application with security weaknesses).
# Mailtrain mitigates this problem by running the custom Javascript snippets in a chrooted environment and under a
# DB user that cannot modify the database (see userRO in [mysql] above). However the chrooted environment is available
# only if Mailtrain is started as root. The chrooted environment still does not prevent the custom JS script in
# performing network operations and in generating XSS attacks as part of the report.
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
# then it's safer to switch off the reporting functionality below.
enabled=false
[testserver]
# Starts a vanity server that redirects all mail to /dev/null
# Mostly needed for local development
enabled=false
port=5587
mailboxserverport=3001
host="0.0.0.0"
username="testuser"
password="testpass"
logger=false
[seleniumwebdriver]
browser="phantomjs"
[roles.global.master]
name="Master"
admin=true
description="All permissions"
permissions=["rebuildPermissions", "createJavascriptWithROAccess", "manageBlacklist", "manageSettings"]
rootNamespaceRole="master"
[roles.namespace.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "createCampaign", "manageUsers"]
[roles.namespace.master.children]
sendConfiguration=["viewPublic", "viewPrivate", "edit", "delete", "share", "sendWithoutOverrides", "sendWithAllowedOverrides", "sendWithAnyOverrides"]
list=["view", "edit", "delete", "share", "viewFields", "manageFields", "viewSubscriptions", "manageSubscriptions", "viewSegments", "manageSegments"]
customForm=["view", "edit", "delete", "share"]
campaign=["view", "edit", "delete", "share", "viewFiles", "manageFiles", "viewAttachments", "manageAttachments", "viewTriggers", "manageTriggers", "send", "viewStats"]
template=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
reportTemplate=["view", "edit", "delete", "share", "execute"]
mosaicoTemplate=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "createCampaign", "manageUsers"]
[roles.sendConfiguration.master]
name="Master"
description="All permissions"
permissions=["viewPublic", "viewPrivate", "edit", "delete", "share", "sendWithoutOverrides", "sendWithAllowedOverrides", "sendWithAnyOverrides"]
[roles.list.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "viewFields", "manageFields", "viewSubscriptions", "manageSubscriptions", "viewSegments", "manageSegments"]
[roles.customForm.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share"]
[roles.campaign.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles", "viewAttachments", "manageAttachments", "viewTriggers", "manageTriggers", "send", "viewStats"]
[roles.template.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
[roles.report.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
[roles.reportTemplate.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "execute"]
[roles.mosaicoTemplate.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "viewFiles", "manageFiles"]
[roles.global.editor]
name="Editor"
description="XXX"
permissions=[]
ownNamespaceRole="editor"
[roles.namespace.editor]
name="Editor"
description="XXX"
permissions=["view", "edit", "delete"]
[roles.namespace.editor.children]
sendConfiguration=[]
list=[]
customForm=[]
campaign=[]
template=[]
report=[]
reportTemplate=[]
namespace=["view", "edit", "delete"]
mosaicoTemplate=[]
[roles.sendConfiguration.editor]
name="Editor"
description="XXX"
permissions=[]
[roles.list.editor]
name="Editor"
description="XXX"
permissions=[]
[roles.customForm.editor]
name="Editor"
description="All permissions"
permissions=[]
[roles.campaign.editor]
name="Editor"
description="XXX"
permissions=[]
[roles.template.editor]
name="Editor"
description="XXX"
permissions=[]
[roles.report.editor]
name="Editor"
description="XXX"
permissions=[]
[roles.reportTemplate.editor]
name="Editor"
description="XXX"
permissions=[]
[roles.mosaicoTemplate.editor]
name="Editor"
description="XXX"
permissions=[]

268
config/default.yaml Normal file
View file

@ -0,0 +1,268 @@
# This file is the default config file for Mailtrain. To use a environment specific
# configuration add new file {ENV}.{ext} (eg. production.yaml) to the same folder.
# {ENV} is defined by NODE_ENV environment variable.
#
# Do not modify this file directly, otherwise you might lose your modifications when upgrading
#
# You should only define the options you want to change in your additional config file.
# For example if the only thing you want to change is the port number for the www server
# then your additional config file should look like this:
# # production.yaml
# www:
# port: 80
# Process title visible in monitoring logs and process listing
title: mailtrain
# Enabled HTML editors
editors:
- ckeditor
- codeeditor
- mosaico
- mosaicoWithFsTemplate
# Default language to use
language: en
# Inject custom scripts in subscription/layout.mjml.hbs
# customSubscriptionScripts: [/custom/hello-world.js]
# If you start out as a root user (eg. if you want to use ports lower than 1000)
# then you can downgrade the user once all services are up and running
#user: mailtrain
#group: mailtrain
# If Mailtrain is started as root, Reports feature drops the privileges of script generating the report to disallow
# any modifications of Mailtrain code and even prohibits reading the production configuration (which contains the MySQL
# password for read/write operations). The roUser/roGroup determines the user to be used
#roUser: nobody
#roGroup: nogroup
log:
# silly|verbose|info|http|warn|error|silent
level: verbose
www:
# HTTP port to listen on
port: 3000
# HTTP port to listen on for sandboxed requests
sandboxPort: 8081
# HTTP interface to listen on
host: 0.0.0.0
# URL base for trusted urls. It must be absolute (starting with http:// or https://). If Mailtrain is served on
# a non-standard port (e.g. 3000), the URL must also specify the port.
trustedUrlBase: http://localhost:3000
# URL base for sandbox urls. It must be absolute (starting with http:// or https://) and contain the sandbox port.
sandboxUrlBase: http://localhost:8081
# Secret for signing the session ID cookie
secret: a cat
# Session length in seconds when remember me is checked
remember: 2592000 # 30 days
# logger interface for expressjs morgan
log: dev
# Is the server behind a proxy? true/false
# Set this to true if you are serving Mailtrain as a virtual domain through Nginx or Apache
proxy: false
# maximum POST body size
postSize: 2MB
mysql:
host: localhost
user: mailtrain
password: mailtrain
database: mailtrain
# Some installations, eg. MAMP can use a different port (8889)
# MAMP users should also turn on Allow network access to MySQL otherwise MySQL might not be accessible
port: 3306
charset: utf8mb4
# The timezone configured on the MySQL server. This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM
timezone: local
redis:
# enable to use Redis session cache or disable if Redis is not installed
enabled: false
host: localhost
port: 6379
db: 5
# Uncomment if your Redis installation requires a password
#password:
verp:
# Enable to start an MX server that detects bounced messages using VERP
# In most cases you do not want to use it
# Requires root privileges
enabled: false
port: 2525
host: 0.0.0.0
# With DMARC, the Return-Path and From address must match the same domain.
# By default we get around this by using the VERP address in the Sender header,
# with the side effect that some email clients diplay an ugly on behalf of message.
# You can safely disable this Sender header if you're not using DMARC or your
# VERP hostname is in the same domain as the From address.
# disablesenderheader: true
ldap:
# enable to use ldap user backend
enabled: false
# method is ldapjs or ldapauth - it chooses the library to be used. If not given, it chooses the one present.
# method: ldapjs
host: localhost
port: 3002
baseDN: ou=users,dc=company
filter: (|(username={{username}})(mail={{username}}))
# Username field in LDAP (uid/cn/username)
uidTag: username
# nameTag identifies the attribute to be used for user's full name
nameTag: username
passwordresetlink:
newUserRole: master
# Global namespace id
newUserNamespaceId: 1
# Use a different user to bind LDAP (final bind DN will be: {{uidTag}}: {{bindUser}},{{baseDN}})
bindUser: name@company.net
bindPassword: mySecretPassword
postfixbounce:
# Enable to allow writing Postfix bounce log to Mailtrain listener
# If enabled, tail mail.log to Mailtrain with the following command:
# tail -f -n +0 /var/log/mail.log | nc localhost 5699 -
enabled: false
port: 5699
# allow connections from localhost only
host: 127.0.0.1
# extra options for nodemailer
nodemailer:
#textEncoding: base64
queue:
# How many parallel sender processes to spawn
# You can use more than 1 process only if you have Redis enabled
processes: 1
cors:
# Allow subscription widgets to be embedded
# origins: ['https://www.example.com']
mosaico:
# Installed templates
fsTemplates:
- key: versafix-1
label: Versafix One
# Inject custom scripts
# customscripts:
# - /mosaico/custom/my-mosaico-plugin.js
grapejs:
# Installed templates
templates:
- key: demo
label: HTML Template
- key: aves
label: MJML Template
reports:
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
# properly protected.
# Reports rely on custom user defined Javascript snippets defined in the report template. The snippets are run on the
# server when generating a report. As these snippets are stored in the DB, they pose a security risk because they can
# help gaining access to the server if the DB cannot
# be properly protected (e.g. if it is shared with another application with security weaknesses).
# Mailtrain mitigates this problem by running the custom Javascript snippets in a chrooted environment and under a
# DB user that cannot modify the database (see userRO in [mysql] above). However the chrooted environment is available
# only if Mailtrain is started as root. The chrooted environment still does not prevent the custom JS script in
# performing network operations and in generating XSS attacks as part of the report.
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
# then it's safer to switch off the reporting functionality below.
enabled: false
testserver:
# Starts a vanity server that redirects all mail to /dev/null
# Mostly needed for local development
enabled: false
port: 5587
mailboxserverport: 3001
host: 0.0.0.0
username: testuser
password: testpass
logger: false
seleniumwebdriver:
browser: phantomjs
roles:
global:
master:
name: Master
admin: true
description: All permissions
permissions: [rebuildPermissions, createJavascriptWithROAccess, manageBlacklist, manageSettings]
rootNamespaceRole: master
namespace:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
children:
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments]
customForm: [view, edit, delete, share]
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats]
template: [view, edit, delete, share, viewFiles, manageFiles]
report: [view, edit, delete, share, execute, viewContent, viewOutput]
reportTemplate: [view, edit, delete, share, execute]
mosaicoTemplate: [view, edit, delete, share, viewFiles, manageFiles]
namespace: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
sendConfiguration:
master:
name: Master
description: All permissions
permissions: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
list:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments]
customForm:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share]
campaign:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats]
template:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share, viewFiles, manageFiles]
report:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share, execute, viewContent, viewOutput]
reportTemplate:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share, execute]
mosaicoTemplate:
master:
name: Master
description: All permissions
permissions: [view, edit, delete, share, viewFiles, manageFiles]

View file

@ -2,6 +2,7 @@
const config = require('config');
const urllib = require('url');
const {anonymousRestrictedAccessToken} = require('../shared/urls');
function getTrustedUrlBase() {
return urllib.resolve(config.www.trustedUrlBase, '');
@ -19,7 +20,7 @@ function getSandboxUrl(path, context) {
if (context && context.user && context.user.restrictedAccessToken) {
return urllib.resolve(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''));
} else {
return urllib.resolve(config.www.sandboxUrlBase, 'ANONYMOUS/' + (path || ''));
return urllib.resolve(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''));
}
}

View file

@ -72,6 +72,19 @@ async function listWithContentDTAjax(context, params) {
);
}
async function listOthersByListDTAjax(context, campaignId, listId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
params,
builder => builder.from('campaigns')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.whereNot('campaigns.id', campaignId)
.where('campaigns.list', listId),
['campaigns.id', 'campaigns.name', 'campaigns.description', 'campaigns.type', 'campaigns.created', 'namespaces.name']
);
}
async function getByIdTx(tx, context, id, withPermissions = true, content = Content.ALL) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'view');
let entity = await tx('campaigns').where('id', id).first();
@ -309,8 +322,8 @@ async function enforceSendPermissionTx(tx, context, campaignId) {
const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
await shares.enforceEntityPermissionTx(tx, context, 'send_configuration', campaign.send_configuration, requiredPermission);
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, requiredPermission);
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', campaign.send_configuration, requiredPermission);
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'send');
}
// This is to handle circular dependency with triggers.js
@ -319,6 +332,7 @@ Object.assign(module.exports, {
hash,
listDTAjax,
listWithContentDTAjax,
listOthersByListDTAjax,
getByIdTx,
getById,
create,

View file

@ -10,6 +10,7 @@ const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers');
const fields = require('./fields');
const segments = require('./segments');
const entitySettings = require('../lib/entity-settings');
const UnsubscriptionMode = require('../shared/lists').UnsubscriptionMode;
@ -21,6 +22,8 @@ function hash(entity) {
async function listDTAjax(context, params) {
const campaignEntityType = entitySettings.getEntityType('campaign');
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'list', requiredOperations: ['view'] }],
@ -28,7 +31,16 @@ async function listDTAjax(context, params) {
builder => builder
.from('lists')
.innerJoin('namespaces', 'namespaces.id', 'lists.namespace'),
['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name']
['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name',
{ query: builder =>
builder.from('campaigns')
.whereRaw('campaigns.list = lists.id')
.innerJoin(campaignEntityType.permissionsTable, 'campaigns.id', `${campaignEntityType.permissionsTable}.entity`)
.where(`${campaignEntityType.permissionsTable}.operation`, 'viewTriggers')
.innerJoin('triggers', 'campaigns.id', 'triggers.campaign')
.count()
}
]
);
}

View file

@ -6,10 +6,10 @@ const { enforce, filterObject } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../shared/interoperable-errors');
const shares = require('./shares');
const {EntityVals, ActionVals} = require('../shared/triggers');
const {EntityVals, EventVals, Entity} = require('../shared/triggers');
const campaigns = require('./campaigns');
const allowedKeys = new Set(['name', 'description', 'entity', 'action', 'seconds_after', 'enabled', 'source_campaign']);
const allowedKeys = new Set(['name', 'description', 'entity', 'event', 'seconds_after', 'enabled', 'source_campaign']);
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
@ -34,9 +34,10 @@ async function listByCampaignDTAjax(context, campaignId, params) {
params,
builder => builder
.from('triggers')
.innerJoin('campaigns', 'campaign.id', 'triggers.campaign')
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
.innerJoin('lists', 'lists.id', 'campaigns.list')
.where('triggers.campaign', campaignId),
[ 'triggers.id', 'triggers.name', 'campaign.list', 'triggers.entity', 'triggers.action', 'triggers.source_campaign', 'triggers.enabled' ]
[ 'triggers.id', 'triggers.name', 'triggers.description', 'lists.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled' ]
);
});
}
@ -44,13 +45,13 @@ async function listByCampaignDTAjax(context, campaignId, params) {
async function listByListDTAjax(context, listId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
[{ entityTypeId: 'campaign', requiredOperations: ['viewTriggers'] }],
params,
builder => builder
.from('triggers')
.innerJoin('campaigns', 'campaign.id', 'triggers.campaign')
.where('campaign.list', listId),
[ 'triggers.id', 'triggers.name', 'trigger.campaign', 'triggers.entity', 'triggers.action', 'triggers.source_campaign', 'triggers.enabled' ]
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
.where('campaigns.list', listId),
[ 'triggers.id', 'triggers.name', 'triggers.description', 'campaigns.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled', 'triggers.campaign' ]
);
}
@ -58,7 +59,7 @@ async function _validateAndPreprocess(tx, context, campaignId, entity) {
enforce(Number.isInteger(entity.seconds_after));
enforce(entity.seconds_after >= 0, 'Seconds after must not be negative');
enforce(entity.entity in EntityVals, 'Invalid entity');
enforce(entity.action in ActionVals[entity.entity], 'Invalid action');
enforce(entity.event in EventVals[entity.entity], 'Invalid event');
if (entity.entity === Entity.CAMPAIGN) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.source_campaign, 'view');
@ -94,7 +95,7 @@ async function updateWithConsistencyCheck(context, campaignId, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'manageTriggers');
const existing = await tx('campaign').where({campaign: campaignId, id: entity.id}).first();
const existing = await tx('triggers').where({campaign: campaignId, id: entity.id}).first();
if (!existing) {
throw new interoperableErrors.NotFoundError();
}
@ -138,7 +139,8 @@ Object.assign(module.exports, {
hash,
getById,
listByCampaignDTAjax,
listByListDTAjax, create,
listByListDTAjax,
create,
updateWithConsistencyCheck,
removeTx,
remove,

View file

@ -14,6 +14,11 @@ router.postAsync('/campaigns-with-content-table', passport.loggedIn, async (req,
return res.json(await campaigns.listWithContentDTAjax(req.context, req.body));
});
router.postAsync('/campaigns-others-by-list-table/:campaignId/:listId', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listOthersByListDTAjax(req.context, req.params.campaignId, req.params.listId, req.body));
});
router.getAsync('/campaigns-settings/:campaignId', passport.loggedIn, async (req, res) => {
const campaign = await campaigns.getById(req.context, req.params.campaignId, true, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
campaign.hash = campaigns.hash(campaign, campaigns.Content.WITHOUT_SOURCE_CUSTOM);

View file

@ -7,7 +7,7 @@ const entityTypesAddNamespace = ['list', 'custom_form', 'template', 'campaign',
const shareableEntityTypes = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'namespace', 'send_configuration', 'mosaico_template'];
const { MailerType, getSystemSendConfigurationId } = require('../../../shared/send-configurations');
const { enforce } = require('../../../lib/helpers');
const { EntityVals: TriggerEntityVals, ActionVals: TriggerActionVals } = require('../../../shared/triggers');
const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require('../../../shared/triggers');
const entityTypesWithFiles = {
campaign: {
@ -951,7 +951,7 @@ async function migrateAttachments(knex) {
async function migrateTriggers(knex) {
await knex.schema.table('triggers', table => {
table.renameColumn('rule', 'entity');
table.renameColumn('column', 'action');
table.renameColumn('column', 'event');
table.renameColumn('dest_campaign', 'campaign');
table.renameColumn('seconds', 'seconds_after');
});
@ -964,7 +964,7 @@ async function migrateTriggers(knex) {
enforce(campaign.list === trigger.list, 'The list of trigger and campaign have to be the same.');
enforce(trigger.entity in TriggerEntityVals);
enforce(trigger.action in TriggerActionVals[trigger.entity]);
enforce(trigger.event in TriggerEventVals[trigger.entity]);
}
await knex.schema.table('triggers', table => {

View file

@ -5,7 +5,7 @@ const Entity = {
CAMPAIGN: 'campaign'
};
const Action = {
const Event = {
[Entity.SUBSCRIPTION]: {
CREATED: 'created',
LATEST_OPEN: 'latest_open',
@ -25,7 +25,7 @@ const EntityVals = {
campaign: 'CAMPAIGN'
};
const ActionVals = {
const EventVals = {
[Entity.SUBSCRIPTION]: {
created: 'CREATED',
latest_open: 'LATEST_OPEN',
@ -42,7 +42,7 @@ const ActionVals = {
module.exports = {
Entity,
Action,
Event,
EntityVals,
ActionVals
EventVals
};

7
shared/urls.js Normal file
View file

@ -0,0 +1,7 @@
'use strict';
const anonymousRestrictedAccessToken = 'public';
module.exports = {
anonymousRestrictedAccessToken
};