Line endings fixed so that we don't have CRLF in Git. Better now than later.

This commit is contained in:
Tomas Bures 2019-03-27 09:49:29 +01:00
parent 2fe7f82be3
commit d482d214d9
69 changed files with 6405 additions and 6405 deletions

42
TODO.md
View file

@ -1,21 +1,21 @@
### Front page
- Some dashboard
### Campaigns
- List of sent RSS campaigns (?)
### Pull requests
- Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591
- Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b
- Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13
- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
### API
- Add API extensions
### GDPR
- Refuse editing subscriptions which have been anonymized
- Add field to subscriptions which says till when the consent has been given
- Provide a link (and merge tag) that will update the consent date to now
- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds)
### Front page
- Some dashboard
### Campaigns
- List of sent RSS campaigns (?)
### Pull requests
- Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591
- Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b
- Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13
- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
### API
- Add API extensions
### GDPR
- Refuse editing subscriptions which have been anonymized
- Add field to subscriptions which says till when the consent has been given
- Provide a link (and merge tag) that will update the consent date to now
- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds)

View file

@ -1,21 +1,21 @@
## Migration from Mailtrain v1 to Mailtrain v2
The migration should happen almost 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, Grapesjs, 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/static/mosaico`.
3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/static/mosaico/templates`.
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be
part of the URL - e.g. webhooks/zone-mta/sender-config/system.
6. If there are lists that contain birthday or date fields that were created before
commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead
of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME.
Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth.
## Migration from Mailtrain v1 to Mailtrain v2
The migration should happen almost 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, Grapesjs, 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/static/mosaico`.
3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/static/mosaico/templates`.
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be
part of the URL - e.g. webhooks/zone-mta/sender-config/system.
6. If there are lists that contain birthday or date fields that were created before
commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead
of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME.
Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth.

2
client/.gitignore vendored
View file

@ -1 +1 @@
/dist
/dist

View file

@ -1,9 +1,9 @@
.api {
:global .card h4 {
margin-top: 0px;
}
h4 {
margin-top: 45px;
}
.api {
:global .card h4 {
margin-top: 0px;
}
h4 {
margin-top: 45px;
}
}

View file

@ -1,34 +1,34 @@
'use strict';
import {
CampaignStatus,
CampaignType
} from "../../../shared/campaigns";
export function getCampaignLabels(t) {
const campaignTypeLabels = {
[CampaignType.REGULAR]: t('regular'),
[CampaignType.TRIGGERED]: t('triggered'),
[CampaignType.RSS]: t('rss')
};
const campaignStatusLabels = {
[CampaignStatus.IDLE]: t('idle'),
[CampaignStatus.SCHEDULED]: t('scheduled'),
[CampaignStatus.PAUSED]: t('paused'),
[CampaignStatus.FINISHED]: t('finished'),
[CampaignStatus.PAUSED]: t('paused'),
[CampaignStatus.INACTIVE]: t('inactive'),
[CampaignStatus.ACTIVE]: t('active'),
[CampaignStatus.SENDING]: t('sending')
};
return {
campaignStatusLabels,
campaignTypeLabels
};
}
'use strict';
import {
CampaignStatus,
CampaignType
} from "../../../shared/campaigns";
export function getCampaignLabels(t) {
const campaignTypeLabels = {
[CampaignType.REGULAR]: t('regular'),
[CampaignType.TRIGGERED]: t('triggered'),
[CampaignType.RSS]: t('rss')
};
const campaignStatusLabels = {
[CampaignStatus.IDLE]: t('idle'),
[CampaignStatus.SCHEDULED]: t('scheduled'),
[CampaignStatus.PAUSED]: t('paused'),
[CampaignStatus.FINISHED]: t('finished'),
[CampaignStatus.PAUSED]: t('paused'),
[CampaignStatus.INACTIVE]: t('inactive'),
[CampaignStatus.ACTIVE]: t('active'),
[CampaignStatus.SENDING]: t('sending')
};
return {
campaignStatusLabels,
campaignTypeLabels
};
}

View file

@ -1,35 +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('latestOpen'),
[SubscriptionEvent.LATEST_CLICK]: t('latestClick')
},
[Entity.CAMPAIGN]: {
[CampaignEvent.DELIVERED]: t('delivered'),
[CampaignEvent.OPENED]: t('opened'),
[CampaignEvent.CLICKED]: t('clicked'),
[CampaignEvent.NOT_OPENED]: t('notOpened'),
[CampaignEvent.NOT_CLICKED]: t('notClicked')
}
};
return {
entityLabels,
eventLabels
};
}
'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('latestOpen'),
[SubscriptionEvent.LATEST_CLICK]: t('latestClick')
},
[Entity.CAMPAIGN]: {
[CampaignEvent.DELIVERED]: t('delivered'),
[CampaignEvent.OPENED]: t('opened'),
[CampaignEvent.CLICKED]: t('clicked'),
[CampaignEvent.NOT_OPENED]: t('notOpened'),
[CampaignEvent.NOT_CLICKED]: t('notClicked')
}
};
return {
entityLabels,
eventLabels
};
}

View file

@ -1,32 +1,32 @@
'use strict';
import csrfToken from 'csrfToken';
import axios from 'axios';
import interoperableErrors from '../../../shared/interoperable-errors';
const axiosInst = axios.create({
headers: {
'X-CSRF-TOKEN': csrfToken
}
});
const axiosWrapper = {
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
};
const HTTPMethod = {
GET: axiosWrapper.get,
PUT: axiosWrapper.put,
POST: axiosWrapper.post,
DELETE: axiosWrapper.delete
};
axiosWrapper.method = (method, ...args) => method(...args);
export default axiosWrapper;
export {
HTTPMethod
'use strict';
import csrfToken from 'csrfToken';
import axios from 'axios';
import interoperableErrors from '../../../shared/interoperable-errors';
const axiosInst = axios.create({
headers: {
'X-CSRF-TOKEN': csrfToken
}
});
const axiosWrapper = {
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
};
const HTTPMethod = {
GET: axiosWrapper.get,
PUT: axiosWrapper.put,
POST: axiosWrapper.post,
DELETE: axiosWrapper.delete
};
axiosWrapper.method = (method, ...args) => method(...args);
export default axiosWrapper;
export {
HTTPMethod
}

View file

@ -1,331 +1,331 @@
'use strict';
import React, {Component} from 'react';
import {withTranslation} from './i18n';
import PropTypes
from 'prop-types';
import {
withAsyncErrorHandler,
withErrorHandling
} from './error-handling';
import {withComponentMixins} from "./decorator-helpers";
@withComponentMixins([
withTranslation,
withErrorHandling
])
export class DismissibleAlert extends Component {
static propTypes = {
severity: PropTypes.string.isRequired,
onCloseAsync: PropTypes.func
}
@withAsyncErrorHandler
onClose() {
if (this.props.onCloseAsync) {
this.props.onCloseAsync();
}
}
render() {
const t = this.props.t;
return (
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">&times;</span></button>
{this.props.children}
</div>
)
}
}
export class Icon extends Component {
static propTypes = {
icon: PropTypes.string.isRequired,
family: PropTypes.string,
title: PropTypes.string,
className: PropTypes.string
}
static defaultProps = {
family: 'fas'
}
render() {
const props = this.props;
if (props.family === 'fas' || props.family === 'far') {
return <i className={`${props.family} fa-${props.icon} ${props.className || ''}`} title={props.title}></i>;
} else {
console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`)
return null;
}
}
}
@withComponentMixins([
withErrorHandling
])
export class Button extends Component {
static propTypes = {
onClickAsync: PropTypes.func,
label: PropTypes.string,
icon: PropTypes.string,
iconTitle: PropTypes.string,
className: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string,
disabled: PropTypes.bool
}
@withAsyncErrorHandler
async onClick(evt) {
if (this.props.onClickAsync) {
evt.preventDefault();
await this.props.onClickAsync(evt);
}
}
render() {
const props = this.props;
let className = 'btn';
if (props.className) {
className = className + ' ' + props.className;
}
let type = props.type || 'button';
let icon;
if (props.icon) {
icon = <Icon icon={props.icon} title={props.iconTitle}/>
}
let iconSpacer;
if (props.icon && props.label) {
iconSpacer = ' ';
}
return (
<button type={type} className={className} onClick={::this.onClick} title={this.props.title} disabled={this.props.disabled}>{icon}{iconSpacer}{props.label}</button>
);
}
}
export class ButtonDropdown extends Component {
static propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
className: PropTypes.string,
buttonClassName: PropTypes.string,
menuClassName: PropTypes.string
}
render() {
const props = this.props;
const className = 'dropdown' + (props.className ? ' ' + props.className : '');
const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : '');
const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
return (
<div className="dropdown" className={className}>
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{props.label}
</button>
<ul className={menuClassName}>
{props.children}
</ul>
</div>
);
}
}
@withComponentMixins([
withErrorHandling
])
export class ActionLink extends Component {
static propTypes = {
onClickAsync: PropTypes.func,
className: PropTypes.string,
href: PropTypes.string
}
@withAsyncErrorHandler
async onClick(evt) {
if (this.props.onClickAsync) {
evt.preventDefault();
evt.stopPropagation();
await this.props.onClickAsync(evt);
}
}
render() {
const props = this.props;
return (
<a href={props.href || ''} className={props.className} onClick={::this.onClick}>{props.children}</a>
);
}
}
export class DropdownActionLink extends Component {
static propTypes = {
onClickAsync: PropTypes.func,
className: PropTypes.string,
disabled: PropTypes.bool
}
render() {
const props = this.props;
let clsName = "dropdown-item ";
if (props.disabled) {
clsName += "disabled ";
}
clsName += props.className;
return (
<ActionLink className={clsName} onClickAsync={props.onClickAsync}>{props.children}</ActionLink>
);
}
}
export class DropdownDivider extends Component {
static propTypes = {
className: PropTypes.string
}
render() {
const props = this.props;
let className = 'dropdown-divider';
if (props.className) {
className = className + ' ' + props.className;
}
return (
<div className={className}/>
);
}
}
@withComponentMixins([
withTranslation,
withErrorHandling
])
export class ModalDialog extends Component {
constructor(props) {
super(props);
const t = props.t;
this.state = {
buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ]
};
}
static propTypes = {
title: PropTypes.string,
onCloseAsync: PropTypes.func,
onButtonClickAsync: PropTypes.func,
buttons: PropTypes.array,
hidden: PropTypes.bool,
className: PropTypes.string
}
/*
this.props.hidden - this is the desired state of the modal
this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not
*/
componentDidMount() {
const jqModal = jQuery(this.domModal);
jqModal.on('shown.bs.modal', () => jqModal.focus());
jqModal.on('hide.bs.modal', ::this.onHide);
this.hidden = this.props.hidden;
jqModal.modal({
show: !this.props.hidden
});
}
componentDidUpdate() {
if (this.props.hidden != this.hidden) {
const jqModal = jQuery(this.domModal);
this.hidden = this.props.hidden;
jqModal.modal(this.props.hidden ? 'hide' : 'show');
}
}
componentWillUnmount() {
// We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too.
jQuery('.modal-backdrop').remove();
}
onHide(evt) {
// Hide event is emited is both when hidden through user action or through API. We have to let the API
// calls through, otherwise the modal would never hide. The user actions, which change the desired state,
// are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to
// hide the modal or not.
if (!this.props.hidden) {
// noinspection JSIgnoredPromiseFromCall
this.onClose();
evt.preventDefault();
}
}
@withAsyncErrorHandler
async onClose() {
if (this.props.onCloseAsync) {
await this.props.onCloseAsync();
}
}
async onButtonClick(idx) {
const buttonSpec = this.state.buttons[idx];
if (buttonSpec.onClickAsync) {
await buttonSpec.onClickAsync(idx);
}
}
render() {
const props = this.props;
const t = props.t;
const buttons = [];
for (let idx = 0; idx < this.state.buttons.length; idx++) {
const buttonSpec = this.state.buttons[idx];
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={async () => this.onButtonClick(idx)} />
buttons.push(button);
}
return (
<div
ref={(domElem) => { this.domModal = domElem; }}
className={'modal fade' + (props.className ? ' ' + props.className : '')}
tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">{this.props.title}</h4>
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">&times;</span></button>
</div>
<div className="modal-body">{this.props.children}</div>
<div className="modal-footer">
{buttons}
</div>
</div>
</div>
</div>
);
}
}
'use strict';
import React, {Component} from 'react';
import {withTranslation} from './i18n';
import PropTypes
from 'prop-types';
import {
withAsyncErrorHandler,
withErrorHandling
} from './error-handling';
import {withComponentMixins} from "./decorator-helpers";
@withComponentMixins([
withTranslation,
withErrorHandling
])
export class DismissibleAlert extends Component {
static propTypes = {
severity: PropTypes.string.isRequired,
onCloseAsync: PropTypes.func
}
@withAsyncErrorHandler
onClose() {
if (this.props.onCloseAsync) {
this.props.onCloseAsync();
}
}
render() {
const t = this.props.t;
return (
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">&times;</span></button>
{this.props.children}
</div>
)
}
}
export class Icon extends Component {
static propTypes = {
icon: PropTypes.string.isRequired,
family: PropTypes.string,
title: PropTypes.string,
className: PropTypes.string
}
static defaultProps = {
family: 'fas'
}
render() {
const props = this.props;
if (props.family === 'fas' || props.family === 'far') {
return <i className={`${props.family} fa-${props.icon} ${props.className || ''}`} title={props.title}></i>;
} else {
console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`)
return null;
}
}
}
@withComponentMixins([
withErrorHandling
])
export class Button extends Component {
static propTypes = {
onClickAsync: PropTypes.func,
label: PropTypes.string,
icon: PropTypes.string,
iconTitle: PropTypes.string,
className: PropTypes.string,
title: PropTypes.string,
type: PropTypes.string,
disabled: PropTypes.bool
}
@withAsyncErrorHandler
async onClick(evt) {
if (this.props.onClickAsync) {
evt.preventDefault();
await this.props.onClickAsync(evt);
}
}
render() {
const props = this.props;
let className = 'btn';
if (props.className) {
className = className + ' ' + props.className;
}
let type = props.type || 'button';
let icon;
if (props.icon) {
icon = <Icon icon={props.icon} title={props.iconTitle}/>
}
let iconSpacer;
if (props.icon && props.label) {
iconSpacer = ' ';
}
return (
<button type={type} className={className} onClick={::this.onClick} title={this.props.title} disabled={this.props.disabled}>{icon}{iconSpacer}{props.label}</button>
);
}
}
export class ButtonDropdown extends Component {
static propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
className: PropTypes.string,
buttonClassName: PropTypes.string,
menuClassName: PropTypes.string
}
render() {
const props = this.props;
const className = 'dropdown' + (props.className ? ' ' + props.className : '');
const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : '');
const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
return (
<div className="dropdown" className={className}>
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{props.label}
</button>
<ul className={menuClassName}>
{props.children}
</ul>
</div>
);
}
}
@withComponentMixins([
withErrorHandling
])
export class ActionLink extends Component {
static propTypes = {
onClickAsync: PropTypes.func,
className: PropTypes.string,
href: PropTypes.string
}
@withAsyncErrorHandler
async onClick(evt) {
if (this.props.onClickAsync) {
evt.preventDefault();
evt.stopPropagation();
await this.props.onClickAsync(evt);
}
}
render() {
const props = this.props;
return (
<a href={props.href || ''} className={props.className} onClick={::this.onClick}>{props.children}</a>
);
}
}
export class DropdownActionLink extends Component {
static propTypes = {
onClickAsync: PropTypes.func,
className: PropTypes.string,
disabled: PropTypes.bool
}
render() {
const props = this.props;
let clsName = "dropdown-item ";
if (props.disabled) {
clsName += "disabled ";
}
clsName += props.className;
return (
<ActionLink className={clsName} onClickAsync={props.onClickAsync}>{props.children}</ActionLink>
);
}
}
export class DropdownDivider extends Component {
static propTypes = {
className: PropTypes.string
}
render() {
const props = this.props;
let className = 'dropdown-divider';
if (props.className) {
className = className + ' ' + props.className;
}
return (
<div className={className}/>
);
}
}
@withComponentMixins([
withTranslation,
withErrorHandling
])
export class ModalDialog extends Component {
constructor(props) {
super(props);
const t = props.t;
this.state = {
buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ]
};
}
static propTypes = {
title: PropTypes.string,
onCloseAsync: PropTypes.func,
onButtonClickAsync: PropTypes.func,
buttons: PropTypes.array,
hidden: PropTypes.bool,
className: PropTypes.string
}
/*
this.props.hidden - this is the desired state of the modal
this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not
*/
componentDidMount() {
const jqModal = jQuery(this.domModal);
jqModal.on('shown.bs.modal', () => jqModal.focus());
jqModal.on('hide.bs.modal', ::this.onHide);
this.hidden = this.props.hidden;
jqModal.modal({
show: !this.props.hidden
});
}
componentDidUpdate() {
if (this.props.hidden != this.hidden) {
const jqModal = jQuery(this.domModal);
this.hidden = this.props.hidden;
jqModal.modal(this.props.hidden ? 'hide' : 'show');
}
}
componentWillUnmount() {
// We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too.
jQuery('.modal-backdrop').remove();
}
onHide(evt) {
// Hide event is emited is both when hidden through user action or through API. We have to let the API
// calls through, otherwise the modal would never hide. The user actions, which change the desired state,
// are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to
// hide the modal or not.
if (!this.props.hidden) {
// noinspection JSIgnoredPromiseFromCall
this.onClose();
evt.preventDefault();
}
}
@withAsyncErrorHandler
async onClose() {
if (this.props.onCloseAsync) {
await this.props.onCloseAsync();
}
}
async onButtonClick(idx) {
const buttonSpec = this.state.buttons[idx];
if (buttonSpec.onClickAsync) {
await buttonSpec.onClickAsync(idx);
}
}
render() {
const props = this.props;
const t = props.t;
const buttons = [];
for (let idx = 0; idx < this.state.buttons.length; idx++) {
const buttonSpec = this.state.buttons[idx];
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={async () => this.onButtonClick(idx)} />
buttons.push(button);
}
return (
<div
ref={(domElem) => { this.domModal = domElem; }}
className={'modal fade' + (props.className ? ' ' + props.className : '')}
tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">{this.props.title}</h4>
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">&times;</span></button>
</div>
<div className="modal-body">{this.props.children}</div>
<div className="modal-footer">
{buttons}
</div>
</div>
</div>
</div>
);
}
}

View file

@ -1,128 +1,128 @@
'use strict';
import React from "react";
export function createComponentMixin(contexts, deps, decoratorFn) {
return {
contexts,
deps,
decoratorFn
};
}
export function withComponentMixins(mixins, delegateFuns) {
const mixinsClosure = new Set();
for (const mixin of mixins) {
mixinsClosure.add(mixin);
for (const dep of mixin.deps) {
mixinsClosure.add(dep);
}
}
const contexts = new Map();
for (const mixin of mixinsClosure.values()) {
for (const ctx of mixin.contexts) {
contexts.set(ctx.propName, ctx.context);
}
}
return TargetClass => {
const ctors = [];
const mixinDelegateFuns = [];
if (delegateFuns) {
mixinDelegateFuns.push(...delegateFuns);
}
function TargetClassWithCtors(props) {
if (!new.target) {
throw new TypeError();
}
const self = Reflect.construct(TargetClass, [props], new.target);
for (const ctor of ctors) {
ctor(self, props);
}
return self;
}
TargetClassWithCtors.prototype = TargetClass.prototype;
for (const attr in TargetClass) {
TargetClassWithCtors[attr] = TargetClass[attr];
}
class ComponentMixinsInner extends React.Component {
render() {
const props = {
...this.props,
ref: this.props._decoratorInnerInstanceRefFn
};
delete props._decoratorInnerInstanceRefFn;
return (
<TargetClassWithCtors {...props}/>
);
}
}
let DecoratedInner = ComponentMixinsInner;
for (const mixin of mixinsClosure.values()) {
const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors);
if (res.cls) {
DecoratedInner = res.cls;
}
if (res.ctor) {
ctors.push(res.ctor);
}
if (res.delegateFuns) {
mixinDelegateFuns.push(...res.delegateFuns);
}
}
class ComponentMixinsOuter extends React.Component {
render() {
let innerFn = parentProps => {
const props = {
...parentProps,
_decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node
};
return <DecoratedInner {...props}/>
}
for (const [propName, Context] of contexts.entries()) {
const existingInnerFn = innerFn;
innerFn = parentProps => (
<Context.Consumer>
{
value => existingInnerFn({
...parentProps,
[propName]: value
})
}
</Context.Consumer>
);
}
return innerFn(this.props);
}
}
for (const fun of mixinDelegateFuns) {
ComponentMixinsOuter.prototype[fun] = function (...args) {
return this._decoratorInnerInstance[fun](...args);
}
}
return ComponentMixinsOuter;
};
}
'use strict';
import React from "react";
export function createComponentMixin(contexts, deps, decoratorFn) {
return {
contexts,
deps,
decoratorFn
};
}
export function withComponentMixins(mixins, delegateFuns) {
const mixinsClosure = new Set();
for (const mixin of mixins) {
mixinsClosure.add(mixin);
for (const dep of mixin.deps) {
mixinsClosure.add(dep);
}
}
const contexts = new Map();
for (const mixin of mixinsClosure.values()) {
for (const ctx of mixin.contexts) {
contexts.set(ctx.propName, ctx.context);
}
}
return TargetClass => {
const ctors = [];
const mixinDelegateFuns = [];
if (delegateFuns) {
mixinDelegateFuns.push(...delegateFuns);
}
function TargetClassWithCtors(props) {
if (!new.target) {
throw new TypeError();
}
const self = Reflect.construct(TargetClass, [props], new.target);
for (const ctor of ctors) {
ctor(self, props);
}
return self;
}
TargetClassWithCtors.prototype = TargetClass.prototype;
for (const attr in TargetClass) {
TargetClassWithCtors[attr] = TargetClass[attr];
}
class ComponentMixinsInner extends React.Component {
render() {
const props = {
...this.props,
ref: this.props._decoratorInnerInstanceRefFn
};
delete props._decoratorInnerInstanceRefFn;
return (
<TargetClassWithCtors {...props}/>
);
}
}
let DecoratedInner = ComponentMixinsInner;
for (const mixin of mixinsClosure.values()) {
const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors);
if (res.cls) {
DecoratedInner = res.cls;
}
if (res.ctor) {
ctors.push(res.ctor);
}
if (res.delegateFuns) {
mixinDelegateFuns.push(...res.delegateFuns);
}
}
class ComponentMixinsOuter extends React.Component {
render() {
let innerFn = parentProps => {
const props = {
...parentProps,
_decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node
};
return <DecoratedInner {...props}/>
}
for (const [propName, Context] of contexts.entries()) {
const existingInnerFn = innerFn;
innerFn = parentProps => (
<Context.Consumer>
{
value => existingInnerFn({
...parentProps,
[propName]: value
})
}
</Context.Consumer>
);
}
return innerFn(this.props);
}
}
for (const fun of mixinDelegateFuns) {
ComponentMixinsOuter.prototype[fun] = function (...args) {
return this._decoratorInnerInstance[fun](...args);
}
}
return ComponentMixinsOuter;
};
}

View file

@ -1,76 +1,76 @@
'use strict';
import React from "react";
import PropTypes from 'prop-types';
import {createComponentMixin} from "./decorator-helpers";
function handleError(that, error) {
let errorHandled;
if (that.errorHandler) {
errorHandled = that.errorHandler(error);
}
if (!errorHandled && that.props.parentErrorHandler) {
errorHandled = handleError(that.props.parentErrorHandler, error);
}
if (!errorHandled) {
throw error;
}
return errorHandled;
}
export const ParentErrorHandlerContext = React.createContext(null);
export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => {
/* Example of use:
this.getFormValuesFromURL(....).catch(error => this.handleError(error));
It's equivalent to:
@withAsyncErrorHandler
async loadFormValues() {
await this.getFormValuesFromURL(...);
}
*/
const originalRender = InnerClass.prototype.render;
InnerClass.prototype.render = function() {
return (
<ParentErrorHandlerContext.Provider value={this}>
{originalRender.apply(this)}
</ParentErrorHandlerContext.Provider>
);
}
InnerClass.prototype.handleError = function(error) {
handleError(this, error);
};
return {};
});
export function withAsyncErrorHandler(target, name, descriptor) {
let fn = descriptor.value;
descriptor.value = async function () {
try {
await fn.apply(this, arguments)
} catch (error) {
handleError(this, error);
}
};
return descriptor;
}
export function wrapWithAsyncErrorHandler(self, fn) {
return async function () {
try {
await fn.apply(this, arguments)
} catch (error) {
handleError(self, error);
}
};
}
'use strict';
import React from "react";
import PropTypes from 'prop-types';
import {createComponentMixin} from "./decorator-helpers";
function handleError(that, error) {
let errorHandled;
if (that.errorHandler) {
errorHandled = that.errorHandler(error);
}
if (!errorHandled && that.props.parentErrorHandler) {
errorHandled = handleError(that.props.parentErrorHandler, error);
}
if (!errorHandled) {
throw error;
}
return errorHandled;
}
export const ParentErrorHandlerContext = React.createContext(null);
export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => {
/* Example of use:
this.getFormValuesFromURL(....).catch(error => this.handleError(error));
It's equivalent to:
@withAsyncErrorHandler
async loadFormValues() {
await this.getFormValuesFromURL(...);
}
*/
const originalRender = InnerClass.prototype.render;
InnerClass.prototype.render = function() {
return (
<ParentErrorHandlerContext.Provider value={this}>
{originalRender.apply(this)}
</ParentErrorHandlerContext.Provider>
);
}
InnerClass.prototype.handleError = function(error) {
handleError(this, error);
};
return {};
});
export function withAsyncErrorHandler(target, name, descriptor) {
let fn = descriptor.value;
descriptor.value = async function () {
try {
await fn.apply(this, arguments)
} catch (error) {
handleError(this, error);
}
};
return descriptor;
}
export function wrapWithAsyncErrorHandler(self, fn) {
return async function () {
try {
await fn.apply(this, arguments)
} catch (error) {
handleError(self, error);
}
};
}

View file

@ -1,8 +1,8 @@
'use strict';
import ellipsize from "ellipsize";
export function ellipsizeBreadcrumbLabel(label) {
return ellipsize(label, 40)
'use strict';
import ellipsize from "ellipsize";
export function ellipsizeBreadcrumbLabel(label) {
return ellipsize(label, 40)
}

View file

@ -1,75 +1,75 @@
'use strict';
import React, {Component} from 'react';
import i18n
from 'i18next';
import {withNamespaces} from "react-i18next";
import LanguageDetector
from 'i18next-browser-languagedetector';
import mailtrainConfig
from 'mailtrainConfig';
import {convertToFake, getLang} from '../../../shared/langs';
import {createComponentMixin} from "./decorator-helpers";
import lang_en_US_common from "../../../locales/en-US/common";
import lang_es_ES_common from "../../../locales/es-ES/common";
const resourcesCommon = {
'en-US': lang_en_US_common,
'es-ES': lang_es_ES_common,
'fk-FK': convertToFake(lang_en_US_common)
};
const resources = {};
for (const lng of mailtrainConfig.enabledLanguages) {
const langDesc = getLang(lng);
resources[langDesc.longCode] = {
common: resourcesCommon[langDesc.longCode]
};
}
i18n
.use(LanguageDetector)
.init({
resources,
fallbackLng: mailtrainConfig.defaultLanguage,
defaultNS: 'common',
interpolation: {
escapeValue: false // not needed for react
},
react: {
wait: true
},
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
lookupQuerystring: 'locale',
lookupCookie: 'i18nextLng',
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage', 'cookie']
},
whitelist: mailtrainConfig.enabledLanguages,
load: 'currentOnly',
debug: false
});
export default i18n;
export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => {
return {
cls: withNamespaces()(TargetClass)
};
});
export function tMark(key) {
return key;
}
'use strict';
import React, {Component} from 'react';
import i18n
from 'i18next';
import {withNamespaces} from "react-i18next";
import LanguageDetector
from 'i18next-browser-languagedetector';
import mailtrainConfig
from 'mailtrainConfig';
import {convertToFake, getLang} from '../../../shared/langs';
import {createComponentMixin} from "./decorator-helpers";
import lang_en_US_common from "../../../locales/en-US/common";
import lang_es_ES_common from "../../../locales/es-ES/common";
const resourcesCommon = {
'en-US': lang_en_US_common,
'es-ES': lang_es_ES_common,
'fk-FK': convertToFake(lang_en_US_common)
};
const resources = {};
for (const lng of mailtrainConfig.enabledLanguages) {
const langDesc = getLang(lng);
resources[langDesc.longCode] = {
common: resourcesCommon[langDesc.longCode]
};
}
i18n
.use(LanguageDetector)
.init({
resources,
fallbackLng: mailtrainConfig.defaultLanguage,
defaultNS: 'common',
interpolation: {
escapeValue: false // not needed for react
},
react: {
wait: true
},
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
lookupQuerystring: 'locale',
lookupCookie: 'i18nextLng',
lookupLocalStorage: 'i18nextLng',
caches: ['localStorage', 'cookie']
},
whitelist: mailtrainConfig.enabledLanguages,
load: 'currentOnly',
debug: false
});
export default i18n;
export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => {
return {
cls: withNamespaces()(TargetClass)
};
});
export function tMark(key) {
return key;
}

View file

@ -1,33 +1,33 @@
'use strict';
import React, {Component} from 'react';
import {withTranslation} from './i18n';
import {TreeTableSelect} from './form';
import {withComponentMixins} from "./decorator-helpers";
@withComponentMixins([
withTranslation
])
class NamespaceSelect extends Component {
render() {
const t = this.props.t;
return (
<TreeTableSelect id="namespace" label={t('namespace')} dataUrl="rest/namespaces-tree"/>
);
}
}
function validateNamespace(t, state) {
if (!state.getIn(['namespace', 'value'])) {
state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
} else {
state.setIn(['namespace', 'error'], null);
}
}
export {
NamespaceSelect,
validateNamespace
'use strict';
import React, {Component} from 'react';
import {withTranslation} from './i18n';
import {TreeTableSelect} from './form';
import {withComponentMixins} from "./decorator-helpers";
@withComponentMixins([
withTranslation
])
class NamespaceSelect extends Component {
render() {
const t = this.props.t;
return (
<TreeTableSelect id="namespace" label={t('namespace')} dataUrl="rest/namespaces-tree"/>
);
}
}
function validateNamespace(t, state) {
if (!state.getIn(['namespace', 'value'])) {
state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
} else {
state.setIn(['namespace', 'error'], null);
}
}
export {
NamespaceSelect,
validateNamespace
};

View file

@ -1,146 +1,146 @@
'use strict';
import React
from "react";
import {withRouter} from "react-router";
import {withErrorHandling} from "./error-handling";
import axios
from "../lib/axios";
import {getUrl} from "./urls";
import {createComponentMixin} from "./decorator-helpers";
export function needsResolve(route, nextRoute, match, nextMatch) {
const resolve = route.resolve;
const nextResolve = nextRoute.resolve;
// This compares whether two objects have the same content and returns TRUE if they don't
if (Object.keys(resolve).length === Object.keys(nextResolve).length) {
for (const key in nextResolve) {
if (!(key in resolve) ||
resolve[key](match.params) !== nextResolve[key](nextMatch.params)) {
return true;
}
}
} else {
return true;
}
return false;
}
export async function resolve(route, match) {
const keys = Object.keys(route.resolve);
const promises = keys.map(key => {
const url = route.resolve[key](match.params);
if (url) {
return axios.get(getUrl(url));
} else {
return Promise.resolve({data: null});
}
});
const resolvedArr = await Promise.all(promises);
const resolved = {};
for (let idx = 0; idx < keys.length; idx++) {
resolved[keys[idx]] = resolvedArr[idx].data;
}
return resolved;
}
export function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
let routes = [];
for (let routeKey in structure) {
const entry = structure[routeKey];
let path = urlPrefix + routeKey;
let pathWithParams = path;
if (entry.extraParams) {
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
}
let entryResolve;
if (entry.resolve) {
entryResolve = Object.assign({}, resolve, entry.resolve);
} else {
entryResolve = resolve;
}
let navKeys;
const entryNavs = [];
if (entry.navs) {
navKeys = Object.keys(entry.navs);
for (const navKey of navKeys) {
const nav = entry.navs[navKey];
entryNavs.push({
title: nav.title,
visible: nav.visible,
link: nav.link,
externalLink: nav.externalLink
});
}
}
const route = {
path: (pathWithParams === '' ? '/' : pathWithParams),
panelComponent: entry.panelComponent,
panelRender: entry.panelRender,
primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
title: entry.title,
link: entry.link,
panelInFullScreen: entry.panelInFullScreen,
insideIframe: entry.insideIframe,
resolve: entryResolve,
parents,
navs: [...navs, ...entryNavs]
};
routes.push(route);
const childrenParents = [...parents, route];
if (entry.navs) {
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
const navKey = navKeys[navKeyIdx];
const nav = entry.navs[navKey];
const childNavs = [...entryNavs];
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
}
}
if (entry.children) {
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
}
}
return routes;
}
export const SectionContentContext = React.createContext(null);
export const withPageHelpers = createComponentMixin([{context: SectionContentContext, propName: 'sectionContent'}], [withErrorHandling], (TargetClass, InnerClass) => {
InnerClass.prototype.setFlashMessage = function(severity, text) {
return this.props.sectionContent.setFlashMessage(severity, text);
};
InnerClass.prototype.navigateTo = function(path) {
return this.props.sectionContent.navigateTo(path);
}
InnerClass.prototype.navigateBack = function() {
return this.props.sectionContent.navigateBack();
}
InnerClass.prototype.navigateToWithFlashMessage = function(path, severity, text) {
return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text);
}
return {};
});
'use strict';
import React
from "react";
import {withRouter} from "react-router";
import {withErrorHandling} from "./error-handling";
import axios
from "../lib/axios";
import {getUrl} from "./urls";
import {createComponentMixin} from "./decorator-helpers";
export function needsResolve(route, nextRoute, match, nextMatch) {
const resolve = route.resolve;
const nextResolve = nextRoute.resolve;
// This compares whether two objects have the same content and returns TRUE if they don't
if (Object.keys(resolve).length === Object.keys(nextResolve).length) {
for (const key in nextResolve) {
if (!(key in resolve) ||
resolve[key](match.params) !== nextResolve[key](nextMatch.params)) {
return true;
}
}
} else {
return true;
}
return false;
}
export async function resolve(route, match) {
const keys = Object.keys(route.resolve);
const promises = keys.map(key => {
const url = route.resolve[key](match.params);
if (url) {
return axios.get(getUrl(url));
} else {
return Promise.resolve({data: null});
}
});
const resolvedArr = await Promise.all(promises);
const resolved = {};
for (let idx = 0; idx < keys.length; idx++) {
resolved[keys[idx]] = resolvedArr[idx].data;
}
return resolved;
}
export function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
let routes = [];
for (let routeKey in structure) {
const entry = structure[routeKey];
let path = urlPrefix + routeKey;
let pathWithParams = path;
if (entry.extraParams) {
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
}
let entryResolve;
if (entry.resolve) {
entryResolve = Object.assign({}, resolve, entry.resolve);
} else {
entryResolve = resolve;
}
let navKeys;
const entryNavs = [];
if (entry.navs) {
navKeys = Object.keys(entry.navs);
for (const navKey of navKeys) {
const nav = entry.navs[navKey];
entryNavs.push({
title: nav.title,
visible: nav.visible,
link: nav.link,
externalLink: nav.externalLink
});
}
}
const route = {
path: (pathWithParams === '' ? '/' : pathWithParams),
panelComponent: entry.panelComponent,
panelRender: entry.panelRender,
primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
title: entry.title,
link: entry.link,
panelInFullScreen: entry.panelInFullScreen,
insideIframe: entry.insideIframe,
resolve: entryResolve,
parents,
navs: [...navs, ...entryNavs]
};
routes.push(route);
const childrenParents = [...parents, route];
if (entry.navs) {
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
const navKey = navKeys[navKeyIdx];
const nav = entry.navs[navKey];
const childNavs = [...entryNavs];
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
}
}
if (entry.children) {
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
}
}
return routes;
}
export const SectionContentContext = React.createContext(null);
export const withPageHelpers = createComponentMixin([{context: SectionContentContext, propName: 'sectionContent'}], [withErrorHandling], (TargetClass, InnerClass) => {
InnerClass.prototype.setFlashMessage = function(severity, text) {
return this.props.sectionContent.setFlashMessage(severity, text);
};
InnerClass.prototype.navigateTo = function(path) {
return this.props.sectionContent.navigateTo(path);
}
InnerClass.prototype.navigateBack = function() {
return this.props.sectionContent.navigateBack();
}
InnerClass.prototype.navigateToWithFlashMessage = function(path, severity, text) {
return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text);
}
return {};
});

View file

@ -1,12 +1,12 @@
'use strict';
import {getUrl} from "./urls";
import axios from "./axios";
async function checkPermissions(request) {
return await axios.post(getUrl('rest/permissions-check'), request);
}
export {
checkPermissions
'use strict';
import {getUrl} from "./urls";
import axios from "./axios";
async function checkPermissions(request) {
return await axios.post(getUrl('rest/permissions-check'), request);
}
export {
checkPermissions
}

View file

@ -1,5 +1,5 @@
'use strict';
import {getUrl} from "./urls";
__webpack_public_path__ = getUrl('client/');
'use strict';
import {getUrl} from "./urls";
__webpack_public_path__ = getUrl('client/');

View file

@ -1,90 +1,90 @@
$navbarHeight: 34px;
$editorNormalHeight: 800px !default;
.editor {
.host {
@if $editorNormalHeight {
height: $editorNormalHeight;
}
}
}
.editorFullscreen {
position: fixed;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 1000;
background: white;
margin-top: $navbarHeight;
.navbar {
margin-top: -$navbarHeight;
}
.host {
height: 100%;
}
}
.navbar {
background: #f86c6b;
width: 100%;
height: $navbarHeight;
display: flex;
justify-content: space-between;
}
.navbarLeft {
.logo {
display: inline-block;
height: $navbarHeight;
padding: 5px 0 5px 10px;
filter: brightness(0) invert(1);
}
.title {
display: inline-block;
padding: 5px 0 5px 10px;
font-size: 18px;
font-weight: bold;
float: left;
color: white;
height: $navbarHeight;
}
}
.navbarRight {
.btn, .btnDisabled {
display: inline-block;
padding: 0px 15px;
line-height: $navbarHeight;
text-align: center;
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
cursor: pointer;
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
color: white;
}
}
.btn:hover {
background-color: #c05454;
text-decoration: none;
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
color: white;
}
}
.btnDisabled {
cursor: default;
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
color: #621d1d;
}
}
}
$navbarHeight: 34px;
$editorNormalHeight: 800px !default;
.editor {
.host {
@if $editorNormalHeight {
height: $editorNormalHeight;
}
}
}
.editorFullscreen {
position: fixed;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 1000;
background: white;
margin-top: $navbarHeight;
.navbar {
margin-top: -$navbarHeight;
}
.host {
height: 100%;
}
}
.navbar {
background: #f86c6b;
width: 100%;
height: $navbarHeight;
display: flex;
justify-content: space-between;
}
.navbarLeft {
.logo {
display: inline-block;
height: $navbarHeight;
padding: 5px 0 5px 10px;
filter: brightness(0) invert(1);
}
.title {
display: inline-block;
padding: 5px 0 5px 10px;
font-size: 18px;
font-weight: bold;
float: left;
color: white;
height: $navbarHeight;
}
}
.navbarRight {
.btn, .btnDisabled {
display: inline-block;
padding: 0px 15px;
line-height: $navbarHeight;
text-align: center;
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
cursor: pointer;
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
color: white;
}
}
.btn:hover {
background-color: #c05454;
text-decoration: none;
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
color: white;
}
}
.btnDisabled {
cursor: default;
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
color: #621d1d;
}
}
}

View file

@ -1,7 +1,7 @@
$editorNormalHeight: false;
@import "sandbox-common";
.sandbox {
height: 100%;
overflow: hidden;
$editorNormalHeight: false;
@import "sandbox-common";
.sandbox {
height: 100%;
overflow: hidden;
}

View file

@ -1,35 +1,35 @@
@import "sandbox-common";
.sandbox {
}
.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
position: absolute;
height: 100%;
}
.aceEditorWithPreview {
border-right: #e8e8e8 solid 2px;
width: 50%;
}
.aceEditorWithoutPreview {
width: 100%;
}
.preview {
border-left: #e8e8e8 solid 2px;
width: 50%;
left: 50%;
overflow: hidden;
iframe {
width: 100%;
height: 100%;
border: 0px none;
body {
margin: 0px;
}
}
@import "sandbox-common";
.sandbox {
}
.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
position: absolute;
height: 100%;
}
.aceEditorWithPreview {
border-right: #e8e8e8 solid 2px;
width: 50%;
}
.aceEditorWithoutPreview {
width: 100%;
}
.preview {
border-left: #e8e8e8 solid 2px;
width: 50%;
left: 50%;
overflow: hidden;
iframe {
width: 100%;
height: 100%;
border: 0px none;
body {
margin: 0px;
}
}
}

View file

@ -1,18 +1,18 @@
@import "sandbox-common";
:global .grapesjs-body {
margin: 0px;
}
:global .gjs-editor-cont {
position: absolute;
}
:global .gjs-devices-c .gjs-devices {
padding-right: 15px;
}
:global .gjs-pn-devices-c, :global .gjs-pn-views {
padding: 4px;
}
@import "sandbox-common";
:global .grapesjs-body {
margin: 0px;
}
:global .gjs-editor-cont {
position: absolute;
}
:global .gjs-devices-c .gjs-devices {
padding-right: 15px;
}
:global .gjs-pn-devices-c, :global .gjs-pn-views {
padding: 4px;
}

View file

@ -1,8 +1,8 @@
@import "sandbox-common";
:global .mo-standalone {
top: 0px;
bottom: 0px;
width: 100%;
position: absolute;
}
@import "sandbox-common";
:global .mo-standalone {
top: 0px;
bottom: 0px;
width: 100%;
position: absolute;
}

View file

@ -1,92 +1,92 @@
@import "../scss/variables.scss";
:global {
.mt-treetable-container .fancytree-container {
border: none;
}
.mt-treetable-container span.fancytree-expander {
color: #333333;
}
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active:hover span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title:hover {
background-color: transparent;
}
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title {
cursor: default;
}
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover {
border-color: transparent;
}
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-expander,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container>tbody>tr.fancytree-active>td {
outline: 0px none;
color: #333333;
}
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
color: inherit;
}
.mt-treetable-container {
padding-top: 9px;
padding-bottom: 9px;
}
.mt-treetable-container>table.fancytree-ext-table {
margin-bottom: 0px;
}
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
border-top: 0px none;
}
.mt-treetable-container .mt-treetable-title {
min-width: 150px;
}
.form-group .mt-treetable-container {
border: $input-border-width solid $input-border-color;
border-radius: $input-border-radius;
padding-top: $input-padding-y;
padding-bottom: $input-padding-y;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
.form-group .mt-treetable-container.is-valid {
border-color: $form-feedback-valid-color;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
}
.form-group .mt-treetable-container.is-invalid {
border-color: $form-feedback-invalid-color;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
}
.mt-treetable-container .table td {
padding-top: 3px;
padding-bottom: 3px;
}
}
@import "../scss/variables.scss";
:global {
.mt-treetable-container .fancytree-container {
border: none;
}
.mt-treetable-container span.fancytree-expander {
color: #333333;
}
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active:hover span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title:hover {
background-color: transparent;
}
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title {
cursor: default;
}
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover {
border-color: transparent;
}
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-expander,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
.mt-treetable-container.mt-treetable-inactivable .fancytree-container>tbody>tr.fancytree-active>td {
outline: 0px none;
color: #333333;
}
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
color: inherit;
}
.mt-treetable-container {
padding-top: 9px;
padding-bottom: 9px;
}
.mt-treetable-container>table.fancytree-ext-table {
margin-bottom: 0px;
}
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
border-top: 0px none;
}
.mt-treetable-container .mt-treetable-title {
min-width: 150px;
}
.form-group .mt-treetable-container {
border: $input-border-width solid $input-border-color;
border-radius: $input-border-radius;
padding-top: $input-padding-y;
padding-bottom: $input-padding-y;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
}
.form-group .mt-treetable-container.is-valid {
border-color: $form-feedback-valid-color;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
}
.form-group .mt-treetable-container.is-invalid {
border-color: $form-feedback-invalid-color;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
}
.mt-treetable-container .table td {
padding-top: 3px;
padding-bottom: 3px;
}
}

View file

@ -1,60 +1,60 @@
'use strict';
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
import {AppType} from '../../../shared/app';
import mailtrainConfig from "mailtrainConfig";
import i18n from './i18n';
let restrictedAccessToken = anonymousRestrictedAccessToken;
function setRestrictedAccessToken(token) {
restrictedAccessToken = token;
}
function getTrustedUrl(path) {
return mailtrainConfig.trustedUrlBase + (path || '');
}
function getSandboxUrl(path, customRestrictedAccessToken) {
const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken;
return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || '');
}
function getPublicUrl(path, opts) {
const url = new URL(path || '', mailtrainConfig.publicUrlBase);
if (opts && opts.withLocale) {
url.searchParams.append('locale', i18n.language);
}
return url.toString();
}
function getUrl(path) {
if (mailtrainConfig.appType === AppType.TRUSTED) {
return getTrustedUrl(path);
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
return getSandboxUrl(path);
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
return getPublicUrl(path);
}
}
function getBaseDir() {
if (mailtrainConfig.appType === AppType.TRUSTED) {
return mailtrainConfig.trustedUrlBaseDir;
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
return mailtrainConfig.sandboxUrlBaseDir + restrictedAccessToken;
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
return mailtrainConfig.publicUrlBaseDir;
}
}
export {
getTrustedUrl,
getSandboxUrl,
getPublicUrl,
getUrl,
getBaseDir,
setRestrictedAccessToken
'use strict';
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
import {AppType} from '../../../shared/app';
import mailtrainConfig from "mailtrainConfig";
import i18n from './i18n';
let restrictedAccessToken = anonymousRestrictedAccessToken;
function setRestrictedAccessToken(token) {
restrictedAccessToken = token;
}
function getTrustedUrl(path) {
return mailtrainConfig.trustedUrlBase + (path || '');
}
function getSandboxUrl(path, customRestrictedAccessToken) {
const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken;
return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || '');
}
function getPublicUrl(path, opts) {
const url = new URL(path || '', mailtrainConfig.publicUrlBase);
if (opts && opts.withLocale) {
url.searchParams.append('locale', i18n.language);
}
return url.toString();
}
function getUrl(path) {
if (mailtrainConfig.appType === AppType.TRUSTED) {
return getTrustedUrl(path);
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
return getSandboxUrl(path);
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
return getPublicUrl(path);
}
}
function getBaseDir() {
if (mailtrainConfig.appType === AppType.TRUSTED) {
return mailtrainConfig.trustedUrlBaseDir;
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
return mailtrainConfig.sandboxUrlBaseDir + restrictedAccessToken;
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
return mailtrainConfig.publicUrlBaseDir;
}
}
export {
getTrustedUrl,
getSandboxUrl,
getPublicUrl,
getUrl,
getBaseDir,
setRestrictedAccessToken
}

View file

@ -1,55 +1,55 @@
'use strict';
import React from 'react';
import {Fieldset, InputField} from "../../lib/form";
export function getFieldTypes(t) {
const fieldTypes = {
text: {
label: t('text'),
},
website: {
label: t('website'),
},
longtext: {
label: t('multilineText'),
},
gpg: {
label: t('gpgPublicKey'),
},
number: {
label: t('number'),
},
'checkbox-grouped': {
label: t('checkboxesFromOptionFields'),
},
'radio-grouped': {
label: t('radioButtonsFromOptionFields')
},
'dropdown-grouped': {
label: t('dropDownFromOptionFields')
},
'radio-enum': {
label: t('radioButtonsEnumerated')
},
'dropdown-enum': {
label: t('dropDownEnumerated')
},
'date': {
label: t('date')
},
'birthday': {
label: t('birthday')
},
json: {
label: t('jsonValueForCustomRendering')
},
option: {
label: t('option')
}
};
return fieldTypes;
}
'use strict';
import React from 'react';
import {Fieldset, InputField} from "../../lib/form";
export function getFieldTypes(t) {
const fieldTypes = {
text: {
label: t('text'),
},
website: {
label: t('website'),
},
longtext: {
label: t('multilineText'),
},
gpg: {
label: t('gpgPublicKey'),
},
number: {
label: t('number'),
},
'checkbox-grouped': {
label: t('checkboxesFromOptionFields'),
},
'radio-grouped': {
label: t('radioButtonsFromOptionFields')
},
'dropdown-grouped': {
label: t('dropDownFromOptionFields')
},
'radio-enum': {
label: t('radioButtonsEnumerated')
},
'dropdown-enum': {
label: t('dropDownEnumerated')
},
'date': {
label: t('date')
},
'birthday': {
label: t('birthday')
},
json: {
label: t('jsonValueForCustomRendering')
},
option: {
label: t('option')
}
};
return fieldTypes;
}

View file

@ -1,11 +1,11 @@
$editorNormalHeight: 400px;
@import "../../lib/sandbox-common";
.editor {
margin-bottom: 15px;
}
.host {
border: none;
width: 100%;
}
$editorNormalHeight: 400px;
@import "../../lib/sandbox-common";
.editor {
margin-bottom: 15px;
}
.host {
border: none;
width: 100%;
}

View file

@ -1,46 +1,46 @@
'use strict';
import React from 'react';
import {ImportSource, MappingType, ImportStatus, RunStatus} from '../../../../shared/imports';
export function getImportLabels(t) {
const importSourceLabels = {
[ImportSource.CSV_FILE]: t('csvFile'),
[ImportSource.LIST]: t('list'),
};
const importStatusLabels = {
[ImportStatus.PREP_SCHEDULED]: t('created'),
[ImportStatus.PREP_RUNNING]: t('preparing'),
[ImportStatus.PREP_STOPPING]: t('stopping'),
[ImportStatus.PREP_FINISHED]: t('ready'),
[ImportStatus.PREP_FAILED]: t('preparationFailed'),
[ImportStatus.RUN_SCHEDULED]: t('scheduled'),
[ImportStatus.RUN_RUNNING]: t('running'),
[ImportStatus.RUN_STOPPING]: t('stopping'),
[ImportStatus.RUN_FINISHED]: t('finished'),
[ImportStatus.RUN_FAILED]: t('failed')
};
const runStatusLabels = {
[RunStatus.SCHEDULED]: t('starting'),
[RunStatus.RUNNING]: t('running'),
[RunStatus.STOPPING]: t('stopping'),
[RunStatus.FINISHED]: t('finished'),
[RunStatus.FAILED]: t('failed')
};
const mappingTypeLabels = {
[MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
[MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
}
return {
importStatusLabels,
mappingTypeLabels,
importSourceLabels,
runStatusLabels
};
}
'use strict';
import React from 'react';
import {ImportSource, MappingType, ImportStatus, RunStatus} from '../../../../shared/imports';
export function getImportLabels(t) {
const importSourceLabels = {
[ImportSource.CSV_FILE]: t('csvFile'),
[ImportSource.LIST]: t('list'),
};
const importStatusLabels = {
[ImportStatus.PREP_SCHEDULED]: t('created'),
[ImportStatus.PREP_RUNNING]: t('preparing'),
[ImportStatus.PREP_STOPPING]: t('stopping'),
[ImportStatus.PREP_FINISHED]: t('ready'),
[ImportStatus.PREP_FAILED]: t('preparationFailed'),
[ImportStatus.RUN_SCHEDULED]: t('scheduled'),
[ImportStatus.RUN_RUNNING]: t('running'),
[ImportStatus.RUN_STOPPING]: t('stopping'),
[ImportStatus.RUN_FINISHED]: t('finished'),
[ImportStatus.RUN_FAILED]: t('failed')
};
const runStatusLabels = {
[RunStatus.SCHEDULED]: t('starting'),
[RunStatus.RUNNING]: t('running'),
[RunStatus.STOPPING]: t('stopping'),
[RunStatus.FINISHED]: t('finished'),
[RunStatus.FAILED]: t('failed')
};
const mappingTypeLabels = {
[MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
[MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
}
return {
importStatusLabels,
mappingTypeLabels,
importSourceLabels,
runStatusLabels
};
}

View file

@ -1,152 +1,152 @@
$desktopMinWidth: 768px;
$mobileLeftPaneResidualWidth: 0px;
$mobileAnimationStartPosition: 100px;
$desktopLeftPaneResidualWidth: 200px;
$desktopAnimationStartPosition: 300px;
@mixin optionsHidden {
transform: translateX($mobileAnimationStartPosition);
@media (min-width: $desktopMinWidth) {
transform: translateX($desktopAnimationStartPosition);
}
}
@mixin optionsVisible {
transform: translateX($mobileLeftPaneResidualWidth);
@media (min-width: $desktopMinWidth) {
transform: translateX($desktopLeftPaneResidualWidth);
}
}
.toolbar {
text-align: right;
}
.ruleActionLink {
padding-right: 5px;
}
.rulePane {
position: relative;
width: 100%;
overflow: hidden;
.leftPane {
display: inline-block;
width: 100%;
margin-right: -100%;
.leftPaneInner {
.ruleTree {
background: #fbfbfb;
border: #cfcfcf 1px solid;
border-radius: 4px;
padding: 10px 0px;
margin-top: 15px;
margin-bottom: 30px;
// Without this, the placeholders when rearranging the tree are not shown
position: relative;
z-index: 0;
}
}
.leftPaneOverlay {
display: none;
position: absolute;
left: 0px;
top: 0px;
height: 100%;
z-index: 1;
width: $mobileLeftPaneResidualWidth;
@media (min-width: $desktopMinWidth) {
width: $desktopLeftPaneResidualWidth;
}
}
.paneDivider {
display: block;
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
background: url('./divider.png') repeat-y;
@include optionsHidden;
padding-left: 50px;
z-index: 1;
opacity: 0;
visibility: hidden;
.paneDividerSolidBackground {
position: absolute;
width: 100%;
height: 100%;
background: white;
}
}
}
.rightPane {
display: inline-block;
width: 100%;
vertical-align: top;
z-index: 2;
position: relative;
@include optionsHidden;
opacity: 0;
visibility: hidden;
.rightPaneInner {
margin-right: $mobileLeftPaneResidualWidth;
@media (min-width: $desktopMinWidth) {
margin-right: $desktopLeftPaneResidualWidth;
}
.ruleOptions {
margin-left: 60px;
}
}
}
&.ruleOptionsVisible {
.leftPaneOverlay {
display: block;
}
.paneDivider {
transition: transform 300ms ease-out, opacity 100ms ease-out;
opacity: 1;
visibility: visible;
@include optionsVisible;
}
.rightPane {
transition: transform 300ms ease-out, opacity 100ms ease-out;
opacity: 1;
visibility: visible;
@include optionsVisible;
}
}
&.ruleOptionsHidden {
.paneDivider {
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
}
.rightPane {
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
}
}
}
$desktopMinWidth: 768px;
$mobileLeftPaneResidualWidth: 0px;
$mobileAnimationStartPosition: 100px;
$desktopLeftPaneResidualWidth: 200px;
$desktopAnimationStartPosition: 300px;
@mixin optionsHidden {
transform: translateX($mobileAnimationStartPosition);
@media (min-width: $desktopMinWidth) {
transform: translateX($desktopAnimationStartPosition);
}
}
@mixin optionsVisible {
transform: translateX($mobileLeftPaneResidualWidth);
@media (min-width: $desktopMinWidth) {
transform: translateX($desktopLeftPaneResidualWidth);
}
}
.toolbar {
text-align: right;
}
.ruleActionLink {
padding-right: 5px;
}
.rulePane {
position: relative;
width: 100%;
overflow: hidden;
.leftPane {
display: inline-block;
width: 100%;
margin-right: -100%;
.leftPaneInner {
.ruleTree {
background: #fbfbfb;
border: #cfcfcf 1px solid;
border-radius: 4px;
padding: 10px 0px;
margin-top: 15px;
margin-bottom: 30px;
// Without this, the placeholders when rearranging the tree are not shown
position: relative;
z-index: 0;
}
}
.leftPaneOverlay {
display: none;
position: absolute;
left: 0px;
top: 0px;
height: 100%;
z-index: 1;
width: $mobileLeftPaneResidualWidth;
@media (min-width: $desktopMinWidth) {
width: $desktopLeftPaneResidualWidth;
}
}
.paneDivider {
display: block;
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
background: url('./divider.png') repeat-y;
@include optionsHidden;
padding-left: 50px;
z-index: 1;
opacity: 0;
visibility: hidden;
.paneDividerSolidBackground {
position: absolute;
width: 100%;
height: 100%;
background: white;
}
}
}
.rightPane {
display: inline-block;
width: 100%;
vertical-align: top;
z-index: 2;
position: relative;
@include optionsHidden;
opacity: 0;
visibility: hidden;
.rightPaneInner {
margin-right: $mobileLeftPaneResidualWidth;
@media (min-width: $desktopMinWidth) {
margin-right: $desktopLeftPaneResidualWidth;
}
.ruleOptions {
margin-left: 60px;
}
}
}
&.ruleOptionsVisible {
.leftPaneOverlay {
display: block;
}
.paneDivider {
transition: transform 300ms ease-out, opacity 100ms ease-out;
opacity: 1;
visibility: visible;
@include optionsVisible;
}
.rightPane {
transition: transform 300ms ease-out, opacity 100ms ease-out;
opacity: 1;
visibility: visible;
@include optionsVisible;
}
}
&.ruleOptionsHidden {
.paneDivider {
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
}
.rightPane {
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
}
}
}

View file

@ -1,459 +1,459 @@
'use strict';
import React from 'react';
import {DatePicker, Dropdown, InputField} from "../../lib/form";
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../../shared/date';
import { tMark } from "../../lib/i18n";
export function getRuleHelpers(t, fields) {
const ruleHelpers = {};
ruleHelpers.compositeRuleTypes = {
all: {
dropdownLabel: t('allRulesMustMatch'),
treeLabel: rule => t('allRulesMustMatch')
},
some: {
dropdownLabel: t('atLeastOneRuleMustMatch'),
treeLabel: rule => t('atLeastOneRuleMustMatch')
},
none: {
dropdownLabel: t('noRuleMayMatch'),
treeLabel: rule => t('noRuleMayMatch')
}
};
ruleHelpers.primitiveRuleTypes = {};
ruleHelpers.primitiveRuleTypes.text = {
eq: {
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('matchWithSqlLike'),
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('matchWithRegularExpressions'),
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('alphabeticallyBefore'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('alphabeticallyBeforeOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('alphabeticallyAfter'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('alphabeticallyAfterOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.website = {
eq: {
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('matchWithSqlLike'),
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('matchWithRegularExpressions'),
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.number = {
eq: {
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('lessThan'),
treeLabel: rule => t('valueInColumnColNameIsLessThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('lessThanOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsLessThanOrEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('greaterThan'),
treeLabel: rule => t('valueInColumnColNameIsGreaterThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('greaterThanOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsGreaterThanOrEqual', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
// FXIME - the localization here is still wrong
function getRelativeDateTreeLabel(rule, variants) {
if (rule.value === 0) {
return t(variants[0], {colName: ruleHelpers.getColumnName(rule.column)})
} else if (rule.value > 0) {
return t(variants[1], {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
} else {
return t(variants[2], {colName: ruleHelpers.getColumnName(rule.column), value: -rule.value});
}
}
ruleHelpers.primitiveRuleTypes.date = {
eq: {
dropdownLabel: t('on'),
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
lt: {
dropdownLabel: t('before'),
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
le: {
dropdownLabel: t('beforeOrOn'),
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
gt: {
dropdownLabel: t('after'),
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
ge: {
dropdownLabel: t('afterOrOn'),
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
eqTodayPlusDays: {
dropdownLabel: t('onXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsTheCurrentDate'), tMark('dateInColumnColNameIsTheValuethDayAfter'), tMark('dateInColumnColNameIsTheValuethDayBefore')]),
},
ltTodayPlusDays: {
dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeTheCurrent'), tMark('dateInColumnColNameIsBeforeTheValuethDay'), tMark('dateInColumnColNameIsBeforeTheValuethDay-1')]),
},
leTodayPlusDays: {
dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeOrOnThe'), tMark('dateInColumnColNameIsBeforeOrOnThe-1'), tMark('dateInColumnColNameIsBeforeOrOnThe-2')]),
},
gtTodayPlusDays: {
dropdownLabel: t('afterXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterTheCurrentDate'), tMark('dateInColumnColNameIsAfterTheValuethDay'), tMark('dateInColumnColNameIsAfterTheValuethDay-1')]),
},
geTodayPlusDays: {
dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterOrOnTheCurrent'), tMark('dateInColumnColNameIsAfterOrOnTheValueth'), tMark('dateInColumnColNameIsAfterOrOnTheValueth-1')]),
}
};
ruleHelpers.primitiveRuleTypes.birthday = {
eq: {
dropdownLabel: t('on'),
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
lt: {
dropdownLabel: t('before'),
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
le: {
dropdownLabel: t('beforeOrOn'),
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
gt: {
dropdownLabel: t('after'),
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
ge: {
dropdownLabel: t('afterOrOn'),
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
}
};
ruleHelpers.primitiveRuleTypes.option = {
isTrue: {
dropdownLabel: t('isSelected'),
treeLabel: rule => t('valueInColumnColNameIsSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
},
isFalse: {
dropdownLabel: t('isNotSelected'),
treeLabel: rule => t('valueInColumnColNameIsNotSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
}
};
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
eq: {
dropdownLabel: t('keyEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIsEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('keyMatchWithSqlLike'),
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('keyMatchWithRegularExpressions'),
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('keyAlphabeticallyBefore'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('keyAlphabeticallyBeforeOrEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('keyAlphabeticallyAfter'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('keyAlphabeticallyAfterOrEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
const stringValueSettings = allowEmpty => ({
getForm: () => <InputField id="value" label={t('value')} />,
getFormData: rule => ({
value: rule.value
}),
assignRuleSettings: (rule, getter) => {
rule.value = getter('value');
},
validate: state => {
if (!allowEmpty && !state.getIn(['value', 'value'])) {
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
} else {
state.setIn(['value', 'error'], null);
}
}
});
const numberValueSettings = {
getForm: () => <InputField id="value" label={t('value')} />,
getFormData: rule => ({
value: rule.value.toString()
}),
assignRuleSettings: (rule, getter) => {
rule.value = parseInt(getter('value'));
},
validate: state => {
const value = state.getIn(['value', 'value']).trim();
if (value === '') {
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
} else if (isNaN(value)) {
state.setIn(['value', 'error'], t('valueMustBeANumber'));
} else {
state.setIn(['value', 'error'], null);
}
}
};
const birthdayValueSettings = {
getForm: () => <DatePicker id="birthday" label={t('date')} birthday />,
getFormData: rule => ({
birthday: formatBirthday(DateFormat.INTL, rule.value)
}),
assignRuleSettings: (rule, getter) => {
rule.value = parseBirthday(DateFormat.INTL, getter('birthday')).toISOString();
},
validate: state => {
const value = state.getIn(['birthday', 'value']);
const date = parseBirthday(DateFormat.INTL, value);
if (!value) {
state.setIn(['birthday', 'error'], t('dateMustNotBeEmpty'));
} else if (!date) {
state.setIn(['birthday', 'error'], t('dateIsInvalid'));
} else {
state.setIn(['birthday', 'error'], null);
}
}
};
const dateValueSettings = {
getForm: () => <DatePicker id="date" label={t('date')} />,
getFormData: rule => ({
date: formatDate(DateFormat.INTL, rule.value)
}),
assignRuleSettings: (rule, getter) => {
rule.value = parseDate(DateFormat.INTL, getter('date')).toISOString();
},
validate: state => {
const value = state.getIn(['date', 'value']);
const date = parseDate(DateFormat.INTL, value);
if (!value) {
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
} else if (!date) {
state.setIn(['date', 'error'], t('dateIsInvalid'));
} else {
state.setIn(['date', 'error'], null);
}
}
};
const dateRelativeValueSettings = {
getForm: () =>
<div>
<InputField id="daysValue" label={t('numberOfDays')}/>
<Dropdown id="direction" label={t('beforeAfter')} options={[
{ key: 'before', label: t('beforeCurrentDate') },
{ key: 'after', label: t('afterCurrentDate') }
]}/>
</div>,
getFormData: rule => ({
daysValue: Math.abs(rule.value).toString(),
direction: rule.value >= 0 ? 'after' : 'before'
}),
assignRuleSettings: (rule, getter) => {
const direction = getter('direction');
rule.value = parseInt(getter('daysValue')) * (direction === 'before' ? -1 : 1);
},
validate: state => {
const value = state.getIn(['daysValue', 'value']);
if (!value) {
state.setIn(['daysValue', 'error'], t('numberOfDaysMustNotBeEmpty'));
} else if (isNaN(value)) {
state.setIn(['daysValue', 'error'], t('numberOfDaysMustBeANumber'));
} else {
state.setIn(['daysValue', 'error'], null);
}
}
};
const optionValueSettings = {
getForm: () => null,
getFormData: rule => ({}),
assignRuleSettings: (rule, getter) => {},
validate: state => {}
};
function assignSettingsToRuleTypes(ruleTypes, keys, settings) {
for (const key of keys) {
Object.assign(ruleTypes[key], settings);
}
}
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, ['eq', 'lt', 'le', 'gt', 'ge'], numberValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'], dateRelativeValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, ['isTrue', 'isFalse'], optionValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
ruleHelpers.primitiveRuleTypesFormDataDefaults = {
value: '',
date: '',
daysValue: '',
birthday: '',
direction: 'before'
};
ruleHelpers.getCompositeRuleTypeOptions = () => {
const order = ['all', 'some', 'none'];
return order.map(key => ({ key, label: ruleHelpers.compositeRuleTypes[key].dropdownLabel }));
};
ruleHelpers.getPrimitiveRuleTypeOptions = columnType => {
const order = {
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
website: ['eq', 'like', 're'],
number: ['eq', 'lt', 'le', 'gt', 'ge'],
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
option: ['isTrue', 'isFalse'],
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
};
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
};
const predefColumns = [
{
column: 'email',
name: t('emailAddress-1'),
type: 'text',
key: 'EMAIL'
},
{
column: 'opt_in_country',
name: t('signupCountry'),
type: 'text'
},
{
column: 'created',
name: t('signUpDate'),
type: 'date'
},
{
column: 'latest_open',
name: t('latestOpen'),
type: 'date'
},
{
column: 'latest_click',
name: t('latestClick'),
type: 'date'
},
{
column: 'is_test',
name: t('Test user'),
type: 'option'
}
];
ruleHelpers.fields = [
...predefColumns,
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
];
ruleHelpers.fieldsByColumn = {};
for (const fld of ruleHelpers.fields) {
ruleHelpers.fieldsByColumn[fld.column] = fld;
}
ruleHelpers.getColumnType = column => {
const field = ruleHelpers.fieldsByColumn[column];
if (field) {
return field.type;
}
};
ruleHelpers.getColumnName = column => {
const field = ruleHelpers.fieldsByColumn[column];
if (field) {
return field.name;
}
};
ruleHelpers.getRuleTypeSettings = rule => {
if (ruleHelpers.isCompositeRuleType(rule.type)) {
return ruleHelpers.compositeRuleTypes[rule.type];
} else {
const colType = ruleHelpers.getColumnType(rule.column);
if (colType) {
if (rule.type in ruleHelpers.primitiveRuleTypes[colType]) {
return ruleHelpers.primitiveRuleTypes[colType][rule.type];
}
}
}
};
ruleHelpers.isCompositeRuleType = ruleType => ruleType in ruleHelpers.compositeRuleTypes;
return ruleHelpers;
}
'use strict';
import React from 'react';
import {DatePicker, Dropdown, InputField} from "../../lib/form";
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../../shared/date';
import { tMark } from "../../lib/i18n";
export function getRuleHelpers(t, fields) {
const ruleHelpers = {};
ruleHelpers.compositeRuleTypes = {
all: {
dropdownLabel: t('allRulesMustMatch'),
treeLabel: rule => t('allRulesMustMatch')
},
some: {
dropdownLabel: t('atLeastOneRuleMustMatch'),
treeLabel: rule => t('atLeastOneRuleMustMatch')
},
none: {
dropdownLabel: t('noRuleMayMatch'),
treeLabel: rule => t('noRuleMayMatch')
}
};
ruleHelpers.primitiveRuleTypes = {};
ruleHelpers.primitiveRuleTypes.text = {
eq: {
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('matchWithSqlLike'),
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('matchWithRegularExpressions'),
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('alphabeticallyBefore'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('alphabeticallyBeforeOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('alphabeticallyAfter'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('alphabeticallyAfterOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.website = {
eq: {
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('matchWithSqlLike'),
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('matchWithRegularExpressions'),
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
ruleHelpers.primitiveRuleTypes.number = {
eq: {
dropdownLabel: t('equalTo'),
treeLabel: rule => t('valueInColumnColNameIsEqualToValue-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('lessThan'),
treeLabel: rule => t('valueInColumnColNameIsLessThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('lessThanOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsLessThanOrEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('greaterThan'),
treeLabel: rule => t('valueInColumnColNameIsGreaterThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('greaterThanOrEqualTo'),
treeLabel: rule => t('valueInColumnColNameIsGreaterThanOrEqual', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
// FXIME - the localization here is still wrong
function getRelativeDateTreeLabel(rule, variants) {
if (rule.value === 0) {
return t(variants[0], {colName: ruleHelpers.getColumnName(rule.column)})
} else if (rule.value > 0) {
return t(variants[1], {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
} else {
return t(variants[2], {colName: ruleHelpers.getColumnName(rule.column), value: -rule.value});
}
}
ruleHelpers.primitiveRuleTypes.date = {
eq: {
dropdownLabel: t('on'),
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
lt: {
dropdownLabel: t('before'),
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
le: {
dropdownLabel: t('beforeOrOn'),
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
gt: {
dropdownLabel: t('after'),
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
ge: {
dropdownLabel: t('afterOrOn'),
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
},
eqTodayPlusDays: {
dropdownLabel: t('onXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsTheCurrentDate'), tMark('dateInColumnColNameIsTheValuethDayAfter'), tMark('dateInColumnColNameIsTheValuethDayBefore')]),
},
ltTodayPlusDays: {
dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeTheCurrent'), tMark('dateInColumnColNameIsBeforeTheValuethDay'), tMark('dateInColumnColNameIsBeforeTheValuethDay-1')]),
},
leTodayPlusDays: {
dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeOrOnThe'), tMark('dateInColumnColNameIsBeforeOrOnThe-1'), tMark('dateInColumnColNameIsBeforeOrOnThe-2')]),
},
gtTodayPlusDays: {
dropdownLabel: t('afterXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterTheCurrentDate'), tMark('dateInColumnColNameIsAfterTheValuethDay'), tMark('dateInColumnColNameIsAfterTheValuethDay-1')]),
},
geTodayPlusDays: {
dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'),
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterOrOnTheCurrent'), tMark('dateInColumnColNameIsAfterOrOnTheValueth'), tMark('dateInColumnColNameIsAfterOrOnTheValueth-1')]),
}
};
ruleHelpers.primitiveRuleTypes.birthday = {
eq: {
dropdownLabel: t('on'),
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
lt: {
dropdownLabel: t('before'),
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
le: {
dropdownLabel: t('beforeOrOn'),
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
gt: {
dropdownLabel: t('after'),
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
},
ge: {
dropdownLabel: t('afterOrOn'),
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
}
};
ruleHelpers.primitiveRuleTypes.option = {
isTrue: {
dropdownLabel: t('isSelected'),
treeLabel: rule => t('valueInColumnColNameIsSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
},
isFalse: {
dropdownLabel: t('isNotSelected'),
treeLabel: rule => t('valueInColumnColNameIsNotSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
}
};
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
eq: {
dropdownLabel: t('keyEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIsEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
like: {
dropdownLabel: t('keyMatchWithSqlLike'),
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
re: {
dropdownLabel: t('keyMatchWithRegularExpressions'),
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
lt: {
dropdownLabel: t('keyAlphabeticallyBefore'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
le: {
dropdownLabel: t('keyAlphabeticallyBeforeOrEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
gt: {
dropdownLabel: t('keyAlphabeticallyAfter'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
},
ge: {
dropdownLabel: t('keyAlphabeticallyAfterOrEqualTo'),
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
}
};
const stringValueSettings = allowEmpty => ({
getForm: () => <InputField id="value" label={t('value')} />,
getFormData: rule => ({
value: rule.value
}),
assignRuleSettings: (rule, getter) => {
rule.value = getter('value');
},
validate: state => {
if (!allowEmpty && !state.getIn(['value', 'value'])) {
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
} else {
state.setIn(['value', 'error'], null);
}
}
});
const numberValueSettings = {
getForm: () => <InputField id="value" label={t('value')} />,
getFormData: rule => ({
value: rule.value.toString()
}),
assignRuleSettings: (rule, getter) => {
rule.value = parseInt(getter('value'));
},
validate: state => {
const value = state.getIn(['value', 'value']).trim();
if (value === '') {
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
} else if (isNaN(value)) {
state.setIn(['value', 'error'], t('valueMustBeANumber'));
} else {
state.setIn(['value', 'error'], null);
}
}
};
const birthdayValueSettings = {
getForm: () => <DatePicker id="birthday" label={t('date')} birthday />,
getFormData: rule => ({
birthday: formatBirthday(DateFormat.INTL, rule.value)
}),
assignRuleSettings: (rule, getter) => {
rule.value = parseBirthday(DateFormat.INTL, getter('birthday')).toISOString();
},
validate: state => {
const value = state.getIn(['birthday', 'value']);
const date = parseBirthday(DateFormat.INTL, value);
if (!value) {
state.setIn(['birthday', 'error'], t('dateMustNotBeEmpty'));
} else if (!date) {
state.setIn(['birthday', 'error'], t('dateIsInvalid'));
} else {
state.setIn(['birthday', 'error'], null);
}
}
};
const dateValueSettings = {
getForm: () => <DatePicker id="date" label={t('date')} />,
getFormData: rule => ({
date: formatDate(DateFormat.INTL, rule.value)
}),
assignRuleSettings: (rule, getter) => {
rule.value = parseDate(DateFormat.INTL, getter('date')).toISOString();
},
validate: state => {
const value = state.getIn(['date', 'value']);
const date = parseDate(DateFormat.INTL, value);
if (!value) {
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
} else if (!date) {
state.setIn(['date', 'error'], t('dateIsInvalid'));
} else {
state.setIn(['date', 'error'], null);
}
}
};
const dateRelativeValueSettings = {
getForm: () =>
<div>
<InputField id="daysValue" label={t('numberOfDays')}/>
<Dropdown id="direction" label={t('beforeAfter')} options={[
{ key: 'before', label: t('beforeCurrentDate') },
{ key: 'after', label: t('afterCurrentDate') }
]}/>
</div>,
getFormData: rule => ({
daysValue: Math.abs(rule.value).toString(),
direction: rule.value >= 0 ? 'after' : 'before'
}),
assignRuleSettings: (rule, getter) => {
const direction = getter('direction');
rule.value = parseInt(getter('daysValue')) * (direction === 'before' ? -1 : 1);
},
validate: state => {
const value = state.getIn(['daysValue', 'value']);
if (!value) {
state.setIn(['daysValue', 'error'], t('numberOfDaysMustNotBeEmpty'));
} else if (isNaN(value)) {
state.setIn(['daysValue', 'error'], t('numberOfDaysMustBeANumber'));
} else {
state.setIn(['daysValue', 'error'], null);
}
}
};
const optionValueSettings = {
getForm: () => null,
getFormData: rule => ({}),
assignRuleSettings: (rule, getter) => {},
validate: state => {}
};
function assignSettingsToRuleTypes(ruleTypes, keys, settings) {
for (const key of keys) {
Object.assign(ruleTypes[key], settings);
}
}
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, ['eq', 'lt', 'le', 'gt', 'ge'], numberValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'], dateRelativeValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, ['isTrue', 'isFalse'], optionValueSettings);
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['eq', 'like', 're'], stringValueSettings(true));
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
ruleHelpers.primitiveRuleTypesFormDataDefaults = {
value: '',
date: '',
daysValue: '',
birthday: '',
direction: 'before'
};
ruleHelpers.getCompositeRuleTypeOptions = () => {
const order = ['all', 'some', 'none'];
return order.map(key => ({ key, label: ruleHelpers.compositeRuleTypes[key].dropdownLabel }));
};
ruleHelpers.getPrimitiveRuleTypeOptions = columnType => {
const order = {
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
website: ['eq', 'like', 're'],
number: ['eq', 'lt', 'le', 'gt', 'ge'],
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
option: ['isTrue', 'isFalse'],
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
};
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
};
const predefColumns = [
{
column: 'email',
name: t('emailAddress-1'),
type: 'text',
key: 'EMAIL'
},
{
column: 'opt_in_country',
name: t('signupCountry'),
type: 'text'
},
{
column: 'created',
name: t('signUpDate'),
type: 'date'
},
{
column: 'latest_open',
name: t('latestOpen'),
type: 'date'
},
{
column: 'latest_click',
name: t('latestClick'),
type: 'date'
},
{
column: 'is_test',
name: t('Test user'),
type: 'option'
}
];
ruleHelpers.fields = [
...predefColumns,
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
];
ruleHelpers.fieldsByColumn = {};
for (const fld of ruleHelpers.fields) {
ruleHelpers.fieldsByColumn[fld.column] = fld;
}
ruleHelpers.getColumnType = column => {
const field = ruleHelpers.fieldsByColumn[column];
if (field) {
return field.type;
}
};
ruleHelpers.getColumnName = column => {
const field = ruleHelpers.fieldsByColumn[column];
if (field) {
return field.name;
}
};
ruleHelpers.getRuleTypeSettings = rule => {
if (ruleHelpers.isCompositeRuleType(rule.type)) {
return ruleHelpers.compositeRuleTypes[rule.type];
} else {
const colType = ruleHelpers.getColumnType(rule.column);
if (colType) {
if (rule.type in ruleHelpers.primitiveRuleTypes[colType]) {
return ruleHelpers.primitiveRuleTypes[colType][rule.type];
}
}
}
};
ruleHelpers.isCompositeRuleType = ruleType => ruleType in ruleHelpers.compositeRuleTypes;
return ruleHelpers;
}

View file

@ -1,7 +1,7 @@
.mapping {
margin-top: 30px;
}
.erased {
color: #808080;
.mapping {
margin-top: 30px;
}
.erased {
color: #808080;
}

View file

@ -1,212 +1,212 @@
'use strict';
import React from "react";
import {SubscriptionStatus} from "../../../../shared/lists";
import {
ACEEditor,
CheckBox,
CheckBoxGroup,
DatePicker,
Dropdown,
InputField,
RadioGroup,
TextArea
} from "../../lib/form";
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
import {getFieldColumn} from '../../../../shared/lists';
import 'brace/mode/json';
export function getSubscriptionStatusLabels(t) {
const subscriptionStatusLabels = {
[SubscriptionStatus.SUBSCRIBED]: t('subscribed'),
[SubscriptionStatus.UNSUBSCRIBED]: t('unubscribed'),
[SubscriptionStatus.BOUNCED]: t('bounced'),
[SubscriptionStatus.COMPLAINED]: t('complained'),
};
return subscriptionStatusLabels;
}
export function getFieldTypes(t) {
const groupedFieldTypes = {};
const stringFieldType = long => ({
form: groupedField => long ? <TextArea key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/> : <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value || '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: true
});
const numberFieldType = {
form: groupedField => <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value ? value.toString() : '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {
data[getFieldColumn(groupedField)] = parseInt(data[getFieldColumn(groupedField)]);
},
validate: (groupedField, state) => {
const value = state.getIn([getFieldColumn(groupedField), 'value']).trim();
if (value !== '' && isNaN(value)) {
state.setIn([getFieldColumn(groupedField), 'error'], t('valueMustBeANumber'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
},
indexable: true
};
const dateFieldType = {
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} />,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value ? formatDate(groupedField.settings.dateFormat, value) : '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {
const date = parseDate(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
data[getFieldColumn(groupedField)] = date;
},
validate: (groupedField, state) => {
const value = state.getIn([getFieldColumn(groupedField), 'value']);
const date = parseDate(groupedField.settings.dateFormat, value);
if (value !== '' && !date) {
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
},
indexable: true
};
const birthdayFieldType = {
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} birthday />,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value ? formatBirthday(groupedField.settings.dateFormat, value) : '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {
const date = parseBirthday(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
data[getFieldColumn(groupedField)] = date;
},
validate: (groupedField, state) => {
const value = state.getIn([getFieldColumn(groupedField), 'value']);
const date = parseBirthday(groupedField.settings.dateFormat, value);
if (value !== '' && !date) {
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
},
indexable: true
};
const jsonFieldType = {
form: groupedField => <ACEEditor key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value || '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: false
};
const optionFieldType = {
form: groupedField => <CheckBox key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} text={groupedField.settings.checkedLabel} label={groupedField.name}/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = !!value;
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = false;
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: true
};
const enumSingleFieldType = componentType => ({
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
assignFormData: (groupedField, data) => {
if (data[getFieldColumn(groupedField)] === null) {
if (groupedField.default_value) {
data[getFieldColumn(groupedField)] = groupedField.default_value;
} else if (groupedField.settings.options.length > 0) {
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
} else {
data[getFieldColumn(groupedField)] = '';
}
}
},
initFormData: (groupedField, data) => {
if (groupedField.default_value) {
data[getFieldColumn(groupedField)] = groupedField.default_value;
} else if (groupedField.settings.options.length > 0) {
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
} else {
data[getFieldColumn(groupedField)] = '';
}
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: false
});
const enumMultipleFieldType = componentType => ({
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
assignFormData: (groupedField, data) => {
if (data[getFieldColumn(groupedField)] === null) {
data[getFieldColumn(groupedField)] = [];
}
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = [];
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: false
});
groupedFieldTypes.text = stringFieldType(false);
groupedFieldTypes.website = stringFieldType(false);
groupedFieldTypes.longtext = stringFieldType(true);
groupedFieldTypes.gpg = stringFieldType(true);
groupedFieldTypes.number = numberFieldType;
groupedFieldTypes.date = dateFieldType;
groupedFieldTypes.birthday = birthdayFieldType;
groupedFieldTypes.json = jsonFieldType;
groupedFieldTypes.option = optionFieldType;
groupedFieldTypes['dropdown-enum'] = enumSingleFieldType(Dropdown);
groupedFieldTypes['radio-enum'] = enumSingleFieldType(RadioGroup);
// Here we rely on the fact the model/groupedFields and model/subscriptions preprocess the groupedField info and subscription
// such that the grouped entries behave the same as the enum entries
groupedFieldTypes['checkbox-grouped'] = enumMultipleFieldType(CheckBoxGroup);
groupedFieldTypes['radio-grouped'] = enumSingleFieldType(RadioGroup);
groupedFieldTypes['dropdown-grouped'] = enumSingleFieldType(Dropdown);
return groupedFieldTypes;
'use strict';
import React from "react";
import {SubscriptionStatus} from "../../../../shared/lists";
import {
ACEEditor,
CheckBox,
CheckBoxGroup,
DatePicker,
Dropdown,
InputField,
RadioGroup,
TextArea
} from "../../lib/form";
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
import {getFieldColumn} from '../../../../shared/lists';
import 'brace/mode/json';
export function getSubscriptionStatusLabels(t) {
const subscriptionStatusLabels = {
[SubscriptionStatus.SUBSCRIBED]: t('subscribed'),
[SubscriptionStatus.UNSUBSCRIBED]: t('unubscribed'),
[SubscriptionStatus.BOUNCED]: t('bounced'),
[SubscriptionStatus.COMPLAINED]: t('complained'),
};
return subscriptionStatusLabels;
}
export function getFieldTypes(t) {
const groupedFieldTypes = {};
const stringFieldType = long => ({
form: groupedField => long ? <TextArea key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/> : <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value || '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: true
});
const numberFieldType = {
form: groupedField => <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value ? value.toString() : '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {
data[getFieldColumn(groupedField)] = parseInt(data[getFieldColumn(groupedField)]);
},
validate: (groupedField, state) => {
const value = state.getIn([getFieldColumn(groupedField), 'value']).trim();
if (value !== '' && isNaN(value)) {
state.setIn([getFieldColumn(groupedField), 'error'], t('valueMustBeANumber'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
},
indexable: true
};
const dateFieldType = {
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} />,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value ? formatDate(groupedField.settings.dateFormat, value) : '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {
const date = parseDate(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
data[getFieldColumn(groupedField)] = date;
},
validate: (groupedField, state) => {
const value = state.getIn([getFieldColumn(groupedField), 'value']);
const date = parseDate(groupedField.settings.dateFormat, value);
if (value !== '' && !date) {
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
},
indexable: true
};
const birthdayFieldType = {
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} birthday />,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value ? formatBirthday(groupedField.settings.dateFormat, value) : '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {
const date = parseBirthday(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
data[getFieldColumn(groupedField)] = date;
},
validate: (groupedField, state) => {
const value = state.getIn([getFieldColumn(groupedField), 'value']);
const date = parseBirthday(groupedField.settings.dateFormat, value);
if (value !== '' && !date) {
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
} else {
state.setIn([getFieldColumn(groupedField), 'error'], null);
}
},
indexable: true
};
const jsonFieldType = {
form: groupedField => <ACEEditor key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = value || '';
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = '';
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: false
};
const optionFieldType = {
form: groupedField => <CheckBox key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} text={groupedField.settings.checkedLabel} label={groupedField.name}/>,
assignFormData: (groupedField, data) => {
const value = data[getFieldColumn(groupedField)];
data[getFieldColumn(groupedField)] = !!value;
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = false;
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: true
};
const enumSingleFieldType = componentType => ({
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
assignFormData: (groupedField, data) => {
if (data[getFieldColumn(groupedField)] === null) {
if (groupedField.default_value) {
data[getFieldColumn(groupedField)] = groupedField.default_value;
} else if (groupedField.settings.options.length > 0) {
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
} else {
data[getFieldColumn(groupedField)] = '';
}
}
},
initFormData: (groupedField, data) => {
if (groupedField.default_value) {
data[getFieldColumn(groupedField)] = groupedField.default_value;
} else if (groupedField.settings.options.length > 0) {
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
} else {
data[getFieldColumn(groupedField)] = '';
}
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: false
});
const enumMultipleFieldType = componentType => ({
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
assignFormData: (groupedField, data) => {
if (data[getFieldColumn(groupedField)] === null) {
data[getFieldColumn(groupedField)] = [];
}
},
initFormData: (groupedField, data) => {
data[getFieldColumn(groupedField)] = [];
},
assignEntity: (groupedField, data) => {},
validate: (groupedField, state) => {},
indexable: false
});
groupedFieldTypes.text = stringFieldType(false);
groupedFieldTypes.website = stringFieldType(false);
groupedFieldTypes.longtext = stringFieldType(true);
groupedFieldTypes.gpg = stringFieldType(true);
groupedFieldTypes.number = numberFieldType;
groupedFieldTypes.date = dateFieldType;
groupedFieldTypes.birthday = birthdayFieldType;
groupedFieldTypes.json = jsonFieldType;
groupedFieldTypes.option = optionFieldType;
groupedFieldTypes['dropdown-enum'] = enumSingleFieldType(Dropdown);
groupedFieldTypes['radio-enum'] = enumSingleFieldType(RadioGroup);
// Here we rely on the fact the model/groupedFields and model/subscriptions preprocess the groupedField info and subscription
// such that the grouped entries behave the same as the enum entries
groupedFieldTypes['checkbox-grouped'] = enumMultipleFieldType(CheckBoxGroup);
groupedFieldTypes['radio-grouped'] = enumSingleFieldType(RadioGroup);
groupedFieldTypes['dropdown-grouped'] = enumSingleFieldType(Dropdown);
return groupedFieldTypes;
}

View file

@ -1,337 +1,337 @@
'use strict';
import React from "react";
import {MailerType, ZoneMTAType} from "../../../shared/send-configurations";
import {
CheckBox,
Dropdown,
Fieldset,
InputField,
TextArea
} from "../lib/form";
import {Trans} from "react-i18next";
import styles from "./styles.scss";
import mailtrainConfig from 'mailtrainConfig';
export const mailerTypesOrder = [
MailerType.ZONE_MTA,
MailerType.GENERIC_SMTP,
MailerType.AWS_SES
];
export function getMailerTypes(t) {
const mailerTypes = {};
function initFieldsIfMissing(mutStateData, mailerType) {
const initVals = mailerTypes[mailerType].initData();
for (const key in initVals) {
if (!mutStateData.hasIn([key])) {
mutStateData.setIn([key, 'value'], initVals[key]);
}
}
}
function clearBeforeSave(data) {
for (const mailerKey in mailerTypes) {
const initVals = mailerTypes[mailerKey].initData();
for (const fieldKey in initVals) {
delete data[fieldKey];
}
}
}
function validateNumber(state, field, label, emptyAllowed = false) {
const value = state.getIn([field, 'value']);
if (typeof value === 'string' && value.trim() === '' && !emptyAllowed) { // After load, the numerical values can be still numbers
state.setIn([field, 'error'], t('labelMustNotBeEmpty', {label}));
} else if (isNaN(value)) {
state.setIn([field, 'error'], t('labelMustBeANumber', {label}));
} else {
state.setIn([field, 'error'], null);
}
}
function getInitCommon() {
return {
maxConnections: '5',
throttling: '',
logTransactions: false
};
}
function getInitGenericSMTP() {
return {
...getInitCommon(),
smtpHostname: '',
smtpPort: '',
smtpEncryption: 'NONE',
smtpUseAuth: false,
smtpUser: '',
smtpPassword: '',
smtpAllowSelfSigned: false,
smtpMaxMessages: '100'
};
}
function afterLoadCommon(data) {
data.maxConnections = data.mailer_settings.maxConnections;
data.throttling = data.mailer_settings.throttling || '';
data.logTransactions = data.mailer_settings.logTransactions;
}
function afterLoadGenericSMTP(data) {
afterLoadCommon(data);
data.smtpHostname = data.mailer_settings.hostname;
data.smtpPort = data.mailer_settings.port || '';
data.smtpEncryption = data.mailer_settings.encryption;
data.smtpUseAuth = data.mailer_settings.useAuth;
data.smtpUser = data.mailer_settings.user;
data.smtpPassword = data.mailer_settings.password;
data.smtpAllowSelfSigned = data.mailer_settings.allowSelfSigned;
data.smtpMaxMessages = data.mailer_settings.maxMessages;
}
function beforeSaveCommon(data) {
data.mailer_settings = {};
data.mailer_settings.maxConnections = Number(data.maxConnections);
data.mailer_settings.throttling = Number(data.throttling);
data.mailer_settings.logTransactions = data.logTransactions;
}
function beforeSaveGenericSMTP(data, builtin = false) {
beforeSaveCommon(data);
if (!builtin) {
data.mailer_settings.hostname = data.smtpHostname;
data.mailer_settings.port = Number(data.smtpPort);
data.mailer_settings.encryption = data.smtpEncryption;
data.mailer_settings.useAuth = data.smtpUseAuth;
data.mailer_settings.user = data.smtpUser;
data.mailer_settings.password = data.smtpPassword;
data.mailer_settings.allowSelfSigned = data.smtpAllowSelfSigned;
}
data.mailer_settings.maxMessages = Number(data.smtpMaxMessages);
}
function validateCommon(state) {
validateNumber(state, 'maxConnections', 'Max connections');
validateNumber(state, 'throttling', 'Throttling', true);
}
function validateGenericSMTP(state) {
validateCommon(state);
validateNumber(state, 'smtpPort', 'Port', true);
validateNumber(state, 'smtpMaxMessages', 'Max messages');
}
const typeNames = {
[MailerType.GENERIC_SMTP]: t('genericSmtp'),
[MailerType.ZONE_MTA]: t('zoneMta'),
[MailerType.AWS_SES]: t('amazonSes')
};
const typeOptions = [
{ key: MailerType.GENERIC_SMTP, label: typeNames[MailerType.GENERIC_SMTP]},
{ key: MailerType.ZONE_MTA, label: typeNames[MailerType.ZONE_MTA]},
{ key: MailerType.AWS_SES, label: typeNames[MailerType.AWS_SES]}
];
const smtpEncryptionOptions = [
{ key: 'NONE', label: t('doNotUseEncryption')},
{ key: 'TLS', label: t('useTls UsuallySelectedForPort465')},
{ key: 'STARTTLS', label: t('useStarttls UsuallySelectedForPort587')}
];
const sesRegionOptions = [
{ key: 'us-east-1', label: t('useast1')},
{ key: 'us-west-2', label: t('uswest2')},
{ key: 'eu-west-1', label: t('euwest1')}
];
const zoneMtaTypeOptions = [];
if (mailtrainConfig.builtinZoneMTAEnabled) {
zoneMtaTypeOptions.push({ key: ZoneMTAType.BUILTIN, label: t('builtinZoneMta')});
}
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt')});
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_HTTP_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt-1')});
zoneMtaTypeOptions.push({ key: ZoneMTAType.REGULAR, label: t('noDynamicConfigurationOfDkimKeys')});
mailerTypes[MailerType.GENERIC_SMTP] = {
typeName: typeNames[MailerType.GENERIC_SMTP],
getForm: owner =>
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
}
</Fieldset>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
initData: () => ({
...getInitGenericSMTP()
}),
afterLoad: data => {
afterLoadGenericSMTP(data);
},
beforeSave: data => {
beforeSaveGenericSMTP(data);
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, MailerType.GENERIC_SMTP);
},
validate: state => {
validateGenericSMTP(state);
}
};
mailerTypes[MailerType.ZONE_MTA] = {
typeName: typeNames[MailerType.ZONE_MTA],
getForm: owner => {
const zoneMtaType = Number.parseInt(owner.getFormValue('zoneMtaType'));
return (
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<Dropdown id="zoneMtaType" label={t('dynamicConfiguration')} options={zoneMtaTypeOptions}/>
{(zoneMtaType === ZoneMTAType.REGULAR || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
<div>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
}
</div>
}
</Fieldset>
{(zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
<Fieldset label={t('dkimSigning')}>
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages.</p></Trans>
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
{zoneMtaType === ZoneMTAType.WITH_HTTP_CONF &&
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
}
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
<TextArea id="dkimPrivateKey" className={styles.dkimPrivateKey} label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
</Fieldset>
}
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>
);
},
initData: () => ({
...getInitGenericSMTP(),
zoneMtaType: mailtrainConfig.builtinZoneMTAEnabled ? ZoneMTAType.BUILTIN : ZoneMTAType.REGULAR,
dkimApiKey: '',
dkimDomain: '',
dkimSelector: '',
dkimPrivateKey: ''
}),
afterLoad: data => {
afterLoadGenericSMTP(data);
data.zoneMtaType = data.mailer_settings.zoneMtaType;
data.dkimApiKey = data.mailer_settings.dkimApiKey;
data.dkimDomain = data.mailer_settings.dkimDomain;
data.dkimSelector = data.mailer_settings.dkimSelector;
data.dkimPrivateKey = data.mailer_settings.dkimPrivateKey;
},
beforeSave: data => {
const zoneMtaType = Number.parseInt(data.zoneMtaType);
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
data.mailer_settings.zoneMtaType = zoneMtaType;
if (zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
data.mailer_settings.dkimDomain = data.dkimDomain;
data.mailer_settings.dkimSelector = data.dkimSelector;
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
}
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) {
data.mailer_settings.dkimApiKey = data.dkimApiKey;
}
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, MailerType.ZONE_MTA);
},
validate: state => {
validateGenericSMTP(state);
}
};
mailerTypes[MailerType.AWS_SES] = {
typeName: typeNames[MailerType.AWS_SES],
getForm: owner =>
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/>
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
</Fieldset>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
initData: () => ({
...getInitCommon(),
sesKey: '',
sesSecret: '',
sesRegion: ''
}),
afterLoad: data => {
afterLoadCommon(data);
data.sesKey = data.mailer_settings.key;
data.sesSecret = data.mailer_settings.secret;
data.sesRegion = data.mailer_settings.region;
},
beforeSave: data => {
beforeSaveCommon(data);
data.mailer_settings.key = data.sesKey;
data.mailer_settings.secret = data.sesSecret;
data.mailer_settings.region = data.sesRegion;
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, MailerType.AWS_SES);
},
validate: state => {
validateCommon(state);
}
};
return mailerTypes;
}
'use strict';
import React from "react";
import {MailerType, ZoneMTAType} from "../../../shared/send-configurations";
import {
CheckBox,
Dropdown,
Fieldset,
InputField,
TextArea
} from "../lib/form";
import {Trans} from "react-i18next";
import styles from "./styles.scss";
import mailtrainConfig from 'mailtrainConfig';
export const mailerTypesOrder = [
MailerType.ZONE_MTA,
MailerType.GENERIC_SMTP,
MailerType.AWS_SES
];
export function getMailerTypes(t) {
const mailerTypes = {};
function initFieldsIfMissing(mutStateData, mailerType) {
const initVals = mailerTypes[mailerType].initData();
for (const key in initVals) {
if (!mutStateData.hasIn([key])) {
mutStateData.setIn([key, 'value'], initVals[key]);
}
}
}
function clearBeforeSave(data) {
for (const mailerKey in mailerTypes) {
const initVals = mailerTypes[mailerKey].initData();
for (const fieldKey in initVals) {
delete data[fieldKey];
}
}
}
function validateNumber(state, field, label, emptyAllowed = false) {
const value = state.getIn([field, 'value']);
if (typeof value === 'string' && value.trim() === '' && !emptyAllowed) { // After load, the numerical values can be still numbers
state.setIn([field, 'error'], t('labelMustNotBeEmpty', {label}));
} else if (isNaN(value)) {
state.setIn([field, 'error'], t('labelMustBeANumber', {label}));
} else {
state.setIn([field, 'error'], null);
}
}
function getInitCommon() {
return {
maxConnections: '5',
throttling: '',
logTransactions: false
};
}
function getInitGenericSMTP() {
return {
...getInitCommon(),
smtpHostname: '',
smtpPort: '',
smtpEncryption: 'NONE',
smtpUseAuth: false,
smtpUser: '',
smtpPassword: '',
smtpAllowSelfSigned: false,
smtpMaxMessages: '100'
};
}
function afterLoadCommon(data) {
data.maxConnections = data.mailer_settings.maxConnections;
data.throttling = data.mailer_settings.throttling || '';
data.logTransactions = data.mailer_settings.logTransactions;
}
function afterLoadGenericSMTP(data) {
afterLoadCommon(data);
data.smtpHostname = data.mailer_settings.hostname;
data.smtpPort = data.mailer_settings.port || '';
data.smtpEncryption = data.mailer_settings.encryption;
data.smtpUseAuth = data.mailer_settings.useAuth;
data.smtpUser = data.mailer_settings.user;
data.smtpPassword = data.mailer_settings.password;
data.smtpAllowSelfSigned = data.mailer_settings.allowSelfSigned;
data.smtpMaxMessages = data.mailer_settings.maxMessages;
}
function beforeSaveCommon(data) {
data.mailer_settings = {};
data.mailer_settings.maxConnections = Number(data.maxConnections);
data.mailer_settings.throttling = Number(data.throttling);
data.mailer_settings.logTransactions = data.logTransactions;
}
function beforeSaveGenericSMTP(data, builtin = false) {
beforeSaveCommon(data);
if (!builtin) {
data.mailer_settings.hostname = data.smtpHostname;
data.mailer_settings.port = Number(data.smtpPort);
data.mailer_settings.encryption = data.smtpEncryption;
data.mailer_settings.useAuth = data.smtpUseAuth;
data.mailer_settings.user = data.smtpUser;
data.mailer_settings.password = data.smtpPassword;
data.mailer_settings.allowSelfSigned = data.smtpAllowSelfSigned;
}
data.mailer_settings.maxMessages = Number(data.smtpMaxMessages);
}
function validateCommon(state) {
validateNumber(state, 'maxConnections', 'Max connections');
validateNumber(state, 'throttling', 'Throttling', true);
}
function validateGenericSMTP(state) {
validateCommon(state);
validateNumber(state, 'smtpPort', 'Port', true);
validateNumber(state, 'smtpMaxMessages', 'Max messages');
}
const typeNames = {
[MailerType.GENERIC_SMTP]: t('genericSmtp'),
[MailerType.ZONE_MTA]: t('zoneMta'),
[MailerType.AWS_SES]: t('amazonSes')
};
const typeOptions = [
{ key: MailerType.GENERIC_SMTP, label: typeNames[MailerType.GENERIC_SMTP]},
{ key: MailerType.ZONE_MTA, label: typeNames[MailerType.ZONE_MTA]},
{ key: MailerType.AWS_SES, label: typeNames[MailerType.AWS_SES]}
];
const smtpEncryptionOptions = [
{ key: 'NONE', label: t('doNotUseEncryption')},
{ key: 'TLS', label: t('useTls UsuallySelectedForPort465')},
{ key: 'STARTTLS', label: t('useStarttls UsuallySelectedForPort587')}
];
const sesRegionOptions = [
{ key: 'us-east-1', label: t('useast1')},
{ key: 'us-west-2', label: t('uswest2')},
{ key: 'eu-west-1', label: t('euwest1')}
];
const zoneMtaTypeOptions = [];
if (mailtrainConfig.builtinZoneMTAEnabled) {
zoneMtaTypeOptions.push({ key: ZoneMTAType.BUILTIN, label: t('builtinZoneMta')});
}
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt')});
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_HTTP_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt-1')});
zoneMtaTypeOptions.push({ key: ZoneMTAType.REGULAR, label: t('noDynamicConfigurationOfDkimKeys')});
mailerTypes[MailerType.GENERIC_SMTP] = {
typeName: typeNames[MailerType.GENERIC_SMTP],
getForm: owner =>
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
}
</Fieldset>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
initData: () => ({
...getInitGenericSMTP()
}),
afterLoad: data => {
afterLoadGenericSMTP(data);
},
beforeSave: data => {
beforeSaveGenericSMTP(data);
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, MailerType.GENERIC_SMTP);
},
validate: state => {
validateGenericSMTP(state);
}
};
mailerTypes[MailerType.ZONE_MTA] = {
typeName: typeNames[MailerType.ZONE_MTA],
getForm: owner => {
const zoneMtaType = Number.parseInt(owner.getFormValue('zoneMtaType'));
return (
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<Dropdown id="zoneMtaType" label={t('dynamicConfiguration')} options={zoneMtaTypeOptions}/>
{(zoneMtaType === ZoneMTAType.REGULAR || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
<div>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
}
</div>
}
</Fieldset>
{(zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
<Fieldset label={t('dkimSigning')}>
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages.</p></Trans>
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
{zoneMtaType === ZoneMTAType.WITH_HTTP_CONF &&
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
}
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
<TextArea id="dkimPrivateKey" className={styles.dkimPrivateKey} label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
</Fieldset>
}
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>
);
},
initData: () => ({
...getInitGenericSMTP(),
zoneMtaType: mailtrainConfig.builtinZoneMTAEnabled ? ZoneMTAType.BUILTIN : ZoneMTAType.REGULAR,
dkimApiKey: '',
dkimDomain: '',
dkimSelector: '',
dkimPrivateKey: ''
}),
afterLoad: data => {
afterLoadGenericSMTP(data);
data.zoneMtaType = data.mailer_settings.zoneMtaType;
data.dkimApiKey = data.mailer_settings.dkimApiKey;
data.dkimDomain = data.mailer_settings.dkimDomain;
data.dkimSelector = data.mailer_settings.dkimSelector;
data.dkimPrivateKey = data.mailer_settings.dkimPrivateKey;
},
beforeSave: data => {
const zoneMtaType = Number.parseInt(data.zoneMtaType);
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
data.mailer_settings.zoneMtaType = zoneMtaType;
if (zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
data.mailer_settings.dkimDomain = data.dkimDomain;
data.mailer_settings.dkimSelector = data.dkimSelector;
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
}
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) {
data.mailer_settings.dkimApiKey = data.dkimApiKey;
}
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, MailerType.ZONE_MTA);
},
validate: state => {
validateGenericSMTP(state);
}
};
mailerTypes[MailerType.AWS_SES] = {
typeName: typeNames[MailerType.AWS_SES],
getForm: owner =>
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/>
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
</Fieldset>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
initData: () => ({
...getInitCommon(),
sesKey: '',
sesSecret: '',
sesRegion: ''
}),
afterLoad: data => {
afterLoadCommon(data);
data.sesKey = data.mailer_settings.key;
data.sesSecret = data.mailer_settings.secret;
data.sesRegion = data.mailer_settings.region;
},
beforeSave: data => {
beforeSaveCommon(data);
data.mailer_settings.key = data.sesKey;
data.mailer_settings.secret = data.sesSecret;
data.mailer_settings.region = data.sesRegion;
clearBeforeSave(data);
},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, MailerType.AWS_SES);
},
validate: state => {
validateCommon(state);
}
};
return mailerTypes;
}

File diff suppressed because it is too large Load diff

View file

@ -1,51 +1,51 @@
'use strict';
import React from "react";
import {ACEEditor} from "../../lib/form";
import 'brace/mode/html'
import 'brace/mode/xml'
export function getTemplateTypesOrder() {
return [/* 'mjml' , */ 'html'];
}
export function getTemplateTypes(t) {
const templateTypes = {};
function clearBeforeSend(data) {
delete data.html;
delete data.mjml;
}
templateTypes.html = {
typeName: t('html'),
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
afterLoad: data => {
data.html = data.data.html;
},
beforeSave: (data) => {
data.data = {
html: data.html
};
clearBeforeSend(data);
},
};
templateTypes.mjml = {
typeName: t('mjml'),
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
afterLoad: data => {
data.mjml = data.data.mjml;
},
beforeSave: (data) => {
data.data = {
mjml: data.mjml
};
clearBeforeSend(data);
},
};
return templateTypes;
'use strict';
import React from "react";
import {ACEEditor} from "../../lib/form";
import 'brace/mode/html'
import 'brace/mode/xml'
export function getTemplateTypesOrder() {
return [/* 'mjml' , */ 'html'];
}
export function getTemplateTypes(t) {
const templateTypes = {};
function clearBeforeSend(data) {
delete data.html;
delete data.mjml;
}
templateTypes.html = {
typeName: t('html'),
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
afterLoad: data => {
data.html = data.data.html;
},
beforeSave: (data) => {
data.data = {
html: data.html
};
clearBeforeSend(data);
},
};
templateTypes.mjml = {
typeName: t('mjml'),
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
afterLoad: data => {
data.mjml = data.data.mjml;
},
beforeSave: (data) => {
data.data = {
mjml: data.mjml
};
clearBeforeSend(data);
},
};
return templateTypes;
}

View file

@ -1,2 +1,2 @@
The file icons-rtl.gif is assumed by skin-common.less. However skin-bootstrap does not come with icons.
The file icons-rtl.gif is assumed by skin-common.less. However skin-bootstrap does not come with icons.
To avoid errors with webpack an empty file has been provided.

View file

@ -1,3 +1,3 @@
*
!.gitignore
*
!.gitignore
!README.md

View file

@ -1,7 +1,7 @@
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 400;
src: url('./noto-sans-400-normal.eot');
src: local('Noto Sans'), local('NotoSans'), url('./noto-sans-400-normal.eot#iefix') format('embedded-opentype'), url('./noto-sans-400-normal.woff') format('woff'), url('./noto-sans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 400;
src: url('./noto-sans-400-normal.eot');
src: local('Noto Sans'), local('NotoSans'), url('./noto-sans-400-normal.eot#iefix') format('embedded-opentype'), url('./noto-sans-400-normal.woff') format('woff'), url('./noto-sans-400-normal.ttf') format('truetype');
}

View file

@ -1,141 +1,141 @@
## Access Control
This document describes the key features and concepts of the current state of
access control in Mailtrain.
The current state provides user management and granular access control to reports
and report templates. The user management supports both local authentication and
LDAP-based authentication.
The access control has two abstractions levels: a high-level intended to be used through web UI,
and low-level, intended to be configured once through the Mailtrain config file. The high-level
layer serves for providing access to variuous resources, while the low-level layer is meant
to define the access roles in Mailtrain to reflect an organisational or process hierarchy.
### High-level access management (through web UI)
On the high abstraction level, which is accessible to users via the web-based UI, Mailtrain
recognizes different entities (reports, report templates, etc.) and user roles that regulate
access to these entities (e.g. role "reporter" that allows viewing a report but prevents editing
or deleting it). Access to entities is provided through so called "shares". A share is essentially
a triple: entity - role - user.
Mailtrain further features hierarchical namespaces. Every entity has to reside in a namespace
(in reality, the namespace itself is an entity to which access can be given).
While sharing an entity with a user gives the user access to the particular entity (in the
scope of the role), sharing a namespace amounts to giving access to all entities within
the namespace and transitively in all child namespaces. The role that regulates the access to the
particular namespaces further determines the access to all different entity types that can
reside in the namespace.
To simplify the management of permissions, every user is associated with one global role and
a namespace. The global role regulates access to global resources and operations (i.e. those
things that are not associated with any namespace). An example of such a global operation is
rebuilding the permission cache. Further, the global role determines a default share of the
root namespace and the namespace of the user. For example, an administrator's global role may
specify that a user get administrator's role in the root namespace, which effectively gives
him/her access to everything.
Mailtrain resets these default shares at start and also whenever permission cache is rebuilt
(essentially every time user, namespace or some entity is created or when share or user's
role is assigned). This effectively prevents deleting or overriding the default shares that
the user has through the global role.
### Low-level access management (through config file)
Internally, Mailtrain relies on fine-grained permissions, which are triplets:
user - operation - entity (e.g. user id 1 - view - report id 2). These permissions are stored
in a permission cache (in DB) and automatically generated at startup and whenever the permissions
could have changed.
Mailtrain's config file defines the roles (available in the high-level access management) and
specifies the mapping of roles to operations.
The roles are potentially different for each entity type/scope (currently global, namespace, report,
report template). Each role defines the permitted operations for the given entity type/scope.
A namespace role further defines allowed operations for entity types within and under the
namespace.
The following defines the role master for scope "global". This effectively means that in
"Create/Edit User" form, the user can be given role "Master".
The role gives the permission to rebuild the permission cache.
Further, it specifies that the
holder of the role will automatically be given access (share) to the root namespace in the
namespace role "master" (specified by ```rootNamespaceRole="master"```). This access to the root namespace is given irrespective of the namespace
in which the user is created. This highlight the dual purpose of namespaces: a) they group
entities w.r.t. access management, b) they allow categorizing entities and users in a hierarchy
to potentially reflect the organisational or process hierarchy. The latter is especially useful for
more enterprise applications where a single installation of Mailtrain serves a number of rather
independent groups.
The global role defined below is also an admin role (denoted by the ```admin=true```), which means that user id 1 will always be reset to this role.
This serves as a kind of bootstrap that makes sure that there is always a user that can be
used to give access to other users.
```
[roles.global.master]
name="Master"
admin=true
description="All permissions"
permissions=["rebuildPermissions"]
rootNamespaceRole="master"
```
Another example for a global role is the following. This one is intended for regular users.
As such, it does not automatically give access to everything. Rather, it gives limited access
to entities under the namespace in which the user has been created. This is specified by the
```ownNamespaceRole="editor"```
```
[roles.global.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=[]
ownNamespaceRole="editor"
```
The roles for entities are defined in a similar fashion. The example below shows the definition
of the role "master" for "report" entities. It lists the operations that a user
that has "master" access to a particular report can do with the report. Note that to get the
"master" access to a particular report through this role, the report would either have to be shared with the user
with role "master".
```
[roles.report.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
```
The same for the restricted role "editor" can look as follows.
```
[roles.report.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=["view", "viewContent", "viewOutput"]
```
The following defines the role "master" for "namespace" entities. Similarly to the example above,
it lists operations that relate to a namespace. In particular all "create" operations pertain
to a namespace rathen than to an entity, which at the time of creation does not exist yet.
Additionally, the namespace roles define permissions to all entity types under the namespace
(including child namespaces).
```
[roles.namespace.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
[roles.namespace.master.children]
reportTemplate=["view", "edit", "delete", "share", "execute"]
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
```
And the same for the more restricted role "editor".
```
[roles.namespace.editor.children]
reportTemplate=[]
report=["view", "viewContent", "viewOutput"]
namespace=["view", "edit", "delete"]
## Access Control
This document describes the key features and concepts of the current state of
access control in Mailtrain.
The current state provides user management and granular access control to reports
and report templates. The user management supports both local authentication and
LDAP-based authentication.
The access control has two abstractions levels: a high-level intended to be used through web UI,
and low-level, intended to be configured once through the Mailtrain config file. The high-level
layer serves for providing access to variuous resources, while the low-level layer is meant
to define the access roles in Mailtrain to reflect an organisational or process hierarchy.
### High-level access management (through web UI)
On the high abstraction level, which is accessible to users via the web-based UI, Mailtrain
recognizes different entities (reports, report templates, etc.) and user roles that regulate
access to these entities (e.g. role "reporter" that allows viewing a report but prevents editing
or deleting it). Access to entities is provided through so called "shares". A share is essentially
a triple: entity - role - user.
Mailtrain further features hierarchical namespaces. Every entity has to reside in a namespace
(in reality, the namespace itself is an entity to which access can be given).
While sharing an entity with a user gives the user access to the particular entity (in the
scope of the role), sharing a namespace amounts to giving access to all entities within
the namespace and transitively in all child namespaces. The role that regulates the access to the
particular namespaces further determines the access to all different entity types that can
reside in the namespace.
To simplify the management of permissions, every user is associated with one global role and
a namespace. The global role regulates access to global resources and operations (i.e. those
things that are not associated with any namespace). An example of such a global operation is
rebuilding the permission cache. Further, the global role determines a default share of the
root namespace and the namespace of the user. For example, an administrator's global role may
specify that a user get administrator's role in the root namespace, which effectively gives
him/her access to everything.
Mailtrain resets these default shares at start and also whenever permission cache is rebuilt
(essentially every time user, namespace or some entity is created or when share or user's
role is assigned). This effectively prevents deleting or overriding the default shares that
the user has through the global role.
### Low-level access management (through config file)
Internally, Mailtrain relies on fine-grained permissions, which are triplets:
user - operation - entity (e.g. user id 1 - view - report id 2). These permissions are stored
in a permission cache (in DB) and automatically generated at startup and whenever the permissions
could have changed.
Mailtrain's config file defines the roles (available in the high-level access management) and
specifies the mapping of roles to operations.
The roles are potentially different for each entity type/scope (currently global, namespace, report,
report template). Each role defines the permitted operations for the given entity type/scope.
A namespace role further defines allowed operations for entity types within and under the
namespace.
The following defines the role master for scope "global". This effectively means that in
"Create/Edit User" form, the user can be given role "Master".
The role gives the permission to rebuild the permission cache.
Further, it specifies that the
holder of the role will automatically be given access (share) to the root namespace in the
namespace role "master" (specified by ```rootNamespaceRole="master"```). This access to the root namespace is given irrespective of the namespace
in which the user is created. This highlight the dual purpose of namespaces: a) they group
entities w.r.t. access management, b) they allow categorizing entities and users in a hierarchy
to potentially reflect the organisational or process hierarchy. The latter is especially useful for
more enterprise applications where a single installation of Mailtrain serves a number of rather
independent groups.
The global role defined below is also an admin role (denoted by the ```admin=true```), which means that user id 1 will always be reset to this role.
This serves as a kind of bootstrap that makes sure that there is always a user that can be
used to give access to other users.
```
[roles.global.master]
name="Master"
admin=true
description="All permissions"
permissions=["rebuildPermissions"]
rootNamespaceRole="master"
```
Another example for a global role is the following. This one is intended for regular users.
As such, it does not automatically give access to everything. Rather, it gives limited access
to entities under the namespace in which the user has been created. This is specified by the
```ownNamespaceRole="editor"```
```
[roles.global.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=[]
ownNamespaceRole="editor"
```
The roles for entities are defined in a similar fashion. The example below shows the definition
of the role "master" for "report" entities. It lists the operations that a user
that has "master" access to a particular report can do with the report. Note that to get the
"master" access to a particular report through this role, the report would either have to be shared with the user
with role "master".
```
[roles.report.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
```
The same for the restricted role "editor" can look as follows.
```
[roles.report.editor]
name="Editor"
description="Anything under own namespace except operations related to sending and doing reports"
permissions=["view", "viewContent", "viewOutput"]
```
The following defines the role "master" for "namespace" entities. Similarly to the example above,
it lists operations that relate to a namespace. In particular all "create" operations pertain
to a namespace rathen than to an entity, which at the time of creation does not exist yet.
Additionally, the namespace roles define permissions to all entity types under the namespace
(including child namespaces).
```
[roles.namespace.master]
name="Master"
description="All permissions"
permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
[roles.namespace.master.children]
reportTemplate=["view", "edit", "delete", "share", "execute"]
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
```
And the same for the more restricted role "editor".
```
[roles.namespace.editor.children]
reportTemplate=[]
report=["view", "viewContent", "viewOutput"]
namespace=["view", "edit", "delete"]
```

View file

@ -1,363 +1,363 @@
'use strict';
// Processes statements like these:
// tUI(/*prefix:account*/'account.passwordChangeRequest', language)
// /*prefix:helpers*/<Trans i18nKey="userMessagesUnread" count={count}>Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.</Trans>
const fs = require('fs');
const path = require('path');
const klawSync = require('klaw-sync');
const acorn = require("acorn");
const acornJsx = require("acorn-jsx");
const ellipsize = require('ellipsize');
const camelCase = require('camelcase');
const slugify = require('slugify');
const readline = require('readline');
const localeFile = 'en-US/common.json';
const searchDirs = [
'../client/src',
'../server',
'../shared'
];
function findAllVariantsByPrefixInDict(dict, keyPrefix) {
const keyElems = keyPrefix.split('.');
for (const keyElem of keyElems.slice(0, -1)) {
if (dict[keyElem]) {
if (typeof dict[keyElem] === 'string') {
return [];
} else {
dict = dict[keyElem];
}
} else {
return [];
}
}
const prefix = keyElems[keyElems.length - 1];
const res = [];
for (const key in dict) {
if (key.startsWith(prefix)) {
res.push(key.substring(prefix.length));
}
}
return res;
}
function findInDict(dict, key) {
const keyElems = key.split('.');
for (const keyElem of keyElems.slice(0, -1)) {
if (dict[keyElem]) {
if (typeof dict[keyElem] === 'string') {
return undefined;
} else {
dict = dict[keyElem];
}
} else {
return undefined;
}
}
return dict[keyElems[keyElems.length - 1]];
}
function setInDict(dict, key, value) {
const keyElems = key.split('.');
for (const keyElem of keyElems.slice(0, -1)) {
if (dict[keyElem]) {
if (typeof dict[keyElem] === 'string') {
throw new Error(`Overlapping key ${key}`);
}
} else {
dict[keyElem] = {}
}
dict = dict[keyElem];
}
dict[keyElems[keyElems.length - 1]] = value;
}
const assignedKeys = new Map();
function getKeyFromValue(spec, value) {
let key = value.replace(/<\/?[0-9]+>/g, ''); // Remove Trans markup
key = slugify(key, { replacement: ' ', remove: /[\\()"':.,;\/\[\]\{\}*+-]/g, lower: false });
key = camelCase(key);
key = ellipsize(key, 40, {
chars: [...Array(26)].map((_, i) => String.fromCharCode('A'.charCodeAt(0) + i)) /* This is an array of characters A-Z */,
ellipse: ''
});
if (spec.prefix) {
key = spec.prefix + '.' + key;
}
let idx = 0;
while (true) {
const keyExt = key + (idx ? '-' + idx : '')
if (assignedKeys.has(keyExt)) {
if (assignedKeys.get(keyExt) === value) {
return keyExt;
}
} else {
assignedKeys.set(keyExt, value);
return keyExt;
}
idx++;
}
}
function allowedDirOrFile(item) {
const pp = path.parse(item.path)
return (
(item.stats.isDirectory() &&
pp.base !== 'node_modules'
) ||
(item.stats.isFile() &&
( pp.ext === '.js' || pp.ext === '.jsx' || pp.ext === '.hbs')
)
);
}
function parseSpec(specStr) {
const spec = {};
if (specStr) {
const entryMatcher = /([a-zA-Z]*)\s*:\s*(.*)/
const entries = specStr.split(/\s*,\s*/);
for (const entry of entries) {
const elems = entry.match(entryMatcher);
if (elems) {
spec[elems[1]] = elems[2];
} else {
spec[entry] = true;
}
}
}
return spec;
}
// see http://blog.stevenlevithan.com/archives/match-quoted-string
const tMatcher = /(^|[ {+(=.\[])((?:tUI|tLog|t|tMark)\s*\(\s*(?:\/\*(.*?)\*\/)?\s*)(["'])((?:(?!\4)[^\\]|\\.)*)(\4)/;
const jsxTransMatcher = /(\/\*(.*?)\*\/\s*)?(\<Trans[ >][\s\S]*?\<\/Trans\>)/;
const hbsTranslateMatcher = /(\{\{!--(.*?)--\}\}\s*)?(\{\{#translate\}\})([\s\S]*?)(\{\{\/translate\}\})/;
const jsxParser = acorn.Parser.extend(acornJsx());
function parseJsxTrans(fragment) {
const match = fragment.match(jsxTransMatcher);
const spec = parseSpec(match[2]);
const jsxStr = match[3];
const jsxStrSmpl = jsxStr.replace('{::', '{ '); // Acorn does not handle bind (::) operator. So we just leave it out because we are not interested in the code anyway.
const ast = jsxParser.parse(jsxStrSmpl);
function convertChildren(children) {
const entries = [];
let childNo = 0;
for (const child of children) {
const type = child.type;
if (type === 'JSXText') {
entries.push(child.value);
childNo++;
} else if (type === 'JSXElement') {
const inner = convertChildren(child.children);
entries.push(`<${childNo}>${convertChildren(child.children)}</${childNo}>`);
childNo++;
} else if (type === 'JSXExpressionContainer') {
entries.push(jsxStr.substring(child.start, child.end));
childNo++;
} else {
throw new Error('Unknown JSX node: ' + child);
}
}
return entries.join('');
}
const expr = ast.body[0].expression;
let originalKey;
for (const attr of expr.openingElement.attributes) {
const name = attr.name.name;
if (name === 'i18nKey') {
originalKey = attr.value.value;
}
}
const convValue = convertChildren(expr.children);
if (originalKey === undefined) {
originalKey = convValue;
}
let value;
const originalValue = findInDict(originalResDict, originalKey);
if (originalValue === undefined) {
value = convValue;
} else {
value = originalValue;
}
const key = getKeyFromValue(spec, value);
const replacement = `${match[1] || ''}<Trans i18nKey="${key}">${jsxStr.substring(expr.openingElement.end, expr.closingElement.start)}</Trans>`;
return { key, originalKey, value, replacement };
}
function parseHbsTranslate(fragment) {
const match = fragment.match(hbsTranslateMatcher);
const spec = parseSpec(match[2]);
const originalKey = match[4];
let value;
const originalValue = findInDict(originalResDict, originalKey);
if (originalValue === undefined) {
value = originalKey;
} else {
value = originalValue;
}
const key = getKeyFromValue(spec, value);
const replacement = `${match[1] || ''}${match[3]}${key}${match[5]}`;
return { key, originalKey, value, replacement };
}
function parseT(fragment) {
const match = fragment.match(tMatcher);
const originalKey = match[5];
const spec = parseSpec(match[3]);
if (spec.ignore) {
return null;
}
// console.log(`${fragment}`);
// console.log(` |${match[1]}|${match[2]}|${match[4]}|${match[5]}|${match[6]}| - ${JSON.stringify(spec)}`);
let value;
const originalValue = findInDict(originalResDict, originalKey);
if (originalValue === undefined) {
value = originalKey;
} else {
value = originalValue;
}
const key = getKeyFromValue(spec, value);
const replacement = `${match[1]}${match[2]}${match[4]}${key}${match[6]}`;
return { key, originalKey, value, originalValue, replacement };
}
const renamedKeys = new Map();
const resDict = {};
let anyUpdatesToResDict = false;
function processFile(file) {
let source = fs.readFileSync(file, 'utf8');
let anyUpdates = false;
function update(fragments, parseFun) {
if (fragments) {
for (const fragment of fragments) {
const parseStruct = parseFun(fragment);
if (parseStruct) {
const {key, originalKey, value, originalValue, replacement} = parseStruct;
// console.log(`${key} <- ${originalKey} | ${value} <- ${originalValue} | ${fragment} -> ${replacement}`);
source = source.split(fragment).join(replacement);
setInDict(resDict, key, value);
const variants = originalKey ? findAllVariantsByPrefixInDict(originalResDict, originalKey + '_') : [];
for (const variant of variants) {
setInDict(resDict, key + '_' + variant, findInDict(originalResDict, originalKey + '_' + variant));
}
if (originalKey !== key) {
renamedKeys.set(originalKey, key);
for (const variant of variants) {
renamedKeys.set(originalKey + '_' + variant, key + '_' + variant);
}
}
if (originalKey !== key || originalValue !== value) {
anyUpdates = true;
}
}
}
}
}
const lines = source.split(/\r?\n/g);
for (const line of lines) {
const fragments = line.match(new RegExp(tMatcher, 'g'));
update(fragments, parseT);
}
const hbsFragments = source.match(new RegExp(hbsTranslateMatcher, 'g'));
update(hbsFragments, parseHbsTranslate);
const jsxFragments = source.match(new RegExp(jsxTransMatcher, 'g'));
update(jsxFragments, parseJsxTrans);
if (anyUpdates) {
console.log(`Updating ${file}`);
fs.writeFileSync(file, source);
anyUpdatesToResDict = true;
}
}
const originalResDict = JSON.parse(fs.readFileSync(localeFile));
function run() {
for (const dir of searchDirs) {
const files = klawSync(dir, { nodir: true, filter: allowedDirOrFile })
for (const file of files) {
processFile(file.path);
}
}
if (anyUpdatesToResDict) {
console.log(`Updating ${localeFile}`);
fs.writeFileSync(localeFile, JSON.stringify(resDict, null, 2));
}
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('This script does modifications in the source tree. You should first commit all your files in git before proceeding.');
rl.question('To proceed type YES: ', (answer) => {
if (answer === 'YES') {
run();
}
rl.close();
});
'use strict';
// Processes statements like these:
// tUI(/*prefix:account*/'account.passwordChangeRequest', language)
// /*prefix:helpers*/<Trans i18nKey="userMessagesUnread" count={count}>Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.</Trans>
const fs = require('fs');
const path = require('path');
const klawSync = require('klaw-sync');
const acorn = require("acorn");
const acornJsx = require("acorn-jsx");
const ellipsize = require('ellipsize');
const camelCase = require('camelcase');
const slugify = require('slugify');
const readline = require('readline');
const localeFile = 'en-US/common.json';
const searchDirs = [
'../client/src',
'../server',
'../shared'
];
function findAllVariantsByPrefixInDict(dict, keyPrefix) {
const keyElems = keyPrefix.split('.');
for (const keyElem of keyElems.slice(0, -1)) {
if (dict[keyElem]) {
if (typeof dict[keyElem] === 'string') {
return [];
} else {
dict = dict[keyElem];
}
} else {
return [];
}
}
const prefix = keyElems[keyElems.length - 1];
const res = [];
for (const key in dict) {
if (key.startsWith(prefix)) {
res.push(key.substring(prefix.length));
}
}
return res;
}
function findInDict(dict, key) {
const keyElems = key.split('.');
for (const keyElem of keyElems.slice(0, -1)) {
if (dict[keyElem]) {
if (typeof dict[keyElem] === 'string') {
return undefined;
} else {
dict = dict[keyElem];
}
} else {
return undefined;
}
}
return dict[keyElems[keyElems.length - 1]];
}
function setInDict(dict, key, value) {
const keyElems = key.split('.');
for (const keyElem of keyElems.slice(0, -1)) {
if (dict[keyElem]) {
if (typeof dict[keyElem] === 'string') {
throw new Error(`Overlapping key ${key}`);
}
} else {
dict[keyElem] = {}
}
dict = dict[keyElem];
}
dict[keyElems[keyElems.length - 1]] = value;
}
const assignedKeys = new Map();
function getKeyFromValue(spec, value) {
let key = value.replace(/<\/?[0-9]+>/g, ''); // Remove Trans markup
key = slugify(key, { replacement: ' ', remove: /[\\()"':.,;\/\[\]\{\}*+-]/g, lower: false });
key = camelCase(key);
key = ellipsize(key, 40, {
chars: [...Array(26)].map((_, i) => String.fromCharCode('A'.charCodeAt(0) + i)) /* This is an array of characters A-Z */,
ellipse: ''
});
if (spec.prefix) {
key = spec.prefix + '.' + key;
}
let idx = 0;
while (true) {
const keyExt = key + (idx ? '-' + idx : '')
if (assignedKeys.has(keyExt)) {
if (assignedKeys.get(keyExt) === value) {
return keyExt;
}
} else {
assignedKeys.set(keyExt, value);
return keyExt;
}
idx++;
}
}
function allowedDirOrFile(item) {
const pp = path.parse(item.path)
return (
(item.stats.isDirectory() &&
pp.base !== 'node_modules'
) ||
(item.stats.isFile() &&
( pp.ext === '.js' || pp.ext === '.jsx' || pp.ext === '.hbs')
)
);
}
function parseSpec(specStr) {
const spec = {};
if (specStr) {
const entryMatcher = /([a-zA-Z]*)\s*:\s*(.*)/
const entries = specStr.split(/\s*,\s*/);
for (const entry of entries) {
const elems = entry.match(entryMatcher);
if (elems) {
spec[elems[1]] = elems[2];
} else {
spec[entry] = true;
}
}
}
return spec;
}
// see http://blog.stevenlevithan.com/archives/match-quoted-string
const tMatcher = /(^|[ {+(=.\[])((?:tUI|tLog|t|tMark)\s*\(\s*(?:\/\*(.*?)\*\/)?\s*)(["'])((?:(?!\4)[^\\]|\\.)*)(\4)/;
const jsxTransMatcher = /(\/\*(.*?)\*\/\s*)?(\<Trans[ >][\s\S]*?\<\/Trans\>)/;
const hbsTranslateMatcher = /(\{\{!--(.*?)--\}\}\s*)?(\{\{#translate\}\})([\s\S]*?)(\{\{\/translate\}\})/;
const jsxParser = acorn.Parser.extend(acornJsx());
function parseJsxTrans(fragment) {
const match = fragment.match(jsxTransMatcher);
const spec = parseSpec(match[2]);
const jsxStr = match[3];
const jsxStrSmpl = jsxStr.replace('{::', '{ '); // Acorn does not handle bind (::) operator. So we just leave it out because we are not interested in the code anyway.
const ast = jsxParser.parse(jsxStrSmpl);
function convertChildren(children) {
const entries = [];
let childNo = 0;
for (const child of children) {
const type = child.type;
if (type === 'JSXText') {
entries.push(child.value);
childNo++;
} else if (type === 'JSXElement') {
const inner = convertChildren(child.children);
entries.push(`<${childNo}>${convertChildren(child.children)}</${childNo}>`);
childNo++;
} else if (type === 'JSXExpressionContainer') {
entries.push(jsxStr.substring(child.start, child.end));
childNo++;
} else {
throw new Error('Unknown JSX node: ' + child);
}
}
return entries.join('');
}
const expr = ast.body[0].expression;
let originalKey;
for (const attr of expr.openingElement.attributes) {
const name = attr.name.name;
if (name === 'i18nKey') {
originalKey = attr.value.value;
}
}
const convValue = convertChildren(expr.children);
if (originalKey === undefined) {
originalKey = convValue;
}
let value;
const originalValue = findInDict(originalResDict, originalKey);
if (originalValue === undefined) {
value = convValue;
} else {
value = originalValue;
}
const key = getKeyFromValue(spec, value);
const replacement = `${match[1] || ''}<Trans i18nKey="${key}">${jsxStr.substring(expr.openingElement.end, expr.closingElement.start)}</Trans>`;
return { key, originalKey, value, replacement };
}
function parseHbsTranslate(fragment) {
const match = fragment.match(hbsTranslateMatcher);
const spec = parseSpec(match[2]);
const originalKey = match[4];
let value;
const originalValue = findInDict(originalResDict, originalKey);
if (originalValue === undefined) {
value = originalKey;
} else {
value = originalValue;
}
const key = getKeyFromValue(spec, value);
const replacement = `${match[1] || ''}${match[3]}${key}${match[5]}`;
return { key, originalKey, value, replacement };
}
function parseT(fragment) {
const match = fragment.match(tMatcher);
const originalKey = match[5];
const spec = parseSpec(match[3]);
if (spec.ignore) {
return null;
}
// console.log(`${fragment}`);
// console.log(` |${match[1]}|${match[2]}|${match[4]}|${match[5]}|${match[6]}| - ${JSON.stringify(spec)}`);
let value;
const originalValue = findInDict(originalResDict, originalKey);
if (originalValue === undefined) {
value = originalKey;
} else {
value = originalValue;
}
const key = getKeyFromValue(spec, value);
const replacement = `${match[1]}${match[2]}${match[4]}${key}${match[6]}`;
return { key, originalKey, value, originalValue, replacement };
}
const renamedKeys = new Map();
const resDict = {};
let anyUpdatesToResDict = false;
function processFile(file) {
let source = fs.readFileSync(file, 'utf8');
let anyUpdates = false;
function update(fragments, parseFun) {
if (fragments) {
for (const fragment of fragments) {
const parseStruct = parseFun(fragment);
if (parseStruct) {
const {key, originalKey, value, originalValue, replacement} = parseStruct;
// console.log(`${key} <- ${originalKey} | ${value} <- ${originalValue} | ${fragment} -> ${replacement}`);
source = source.split(fragment).join(replacement);
setInDict(resDict, key, value);
const variants = originalKey ? findAllVariantsByPrefixInDict(originalResDict, originalKey + '_') : [];
for (const variant of variants) {
setInDict(resDict, key + '_' + variant, findInDict(originalResDict, originalKey + '_' + variant));
}
if (originalKey !== key) {
renamedKeys.set(originalKey, key);
for (const variant of variants) {
renamedKeys.set(originalKey + '_' + variant, key + '_' + variant);
}
}
if (originalKey !== key || originalValue !== value) {
anyUpdates = true;
}
}
}
}
}
const lines = source.split(/\r?\n/g);
for (const line of lines) {
const fragments = line.match(new RegExp(tMatcher, 'g'));
update(fragments, parseT);
}
const hbsFragments = source.match(new RegExp(hbsTranslateMatcher, 'g'));
update(hbsFragments, parseHbsTranslate);
const jsxFragments = source.match(new RegExp(jsxTransMatcher, 'g'));
update(jsxFragments, parseJsxTrans);
if (anyUpdates) {
console.log(`Updating ${file}`);
fs.writeFileSync(file, source);
anyUpdatesToResDict = true;
}
}
const originalResDict = JSON.parse(fs.readFileSync(localeFile));
function run() {
for (const dir of searchDirs) {
const files = klawSync(dir, { nodir: true, filter: allowedDirOrFile })
for (const file of files) {
processFile(file.path);
}
}
if (anyUpdatesToResDict) {
console.log(`Updating ${localeFile}`);
fs.writeFileSync(localeFile, JSON.stringify(resDict, null, 2));
}
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('This script does modifications in the source tree. You should first commit all your files in git before proceeding.');
rl.question('To proceed type YES: ', (answer) => {
if (answer === 'YES') {
run();
}
rl.close();
});

View file

@ -1,2 +1,2 @@
/dist
/dist
/node_modules

View file

@ -1,2 +1,2 @@
/node_modules
/config/development.yaml
/node_modules
/config/development.yaml

View file

@ -1,77 +1,77 @@
mysql:
host: localhost
user: mvis
password: mvis
database: mvis
mailtrain:
url: http://localhost:3000/
namespaces:
campaigns: 2
userRole: mailtrainUser
www:
# HTTP interface to listen on
host: 127.0.0.1
# HTTP(S) port to listen on
trustedPort: 3010
trustedPortIsHttps: false
sandboxPort: 3011
sandboxPortIsHttps: false
apiPort: 3012
apiPortIsHttps: false
trustedUrlBase: http://localhost:3010
sandboxUrlBase: http://localhost:3011
roles:
global:
mailtrainUser:
name: "Mailtrain User"
admin: true
description: "Limited permissions that allow only read-only access"
permissions:
namespace:
mailtrainUser:
name: "Mailtrain User"
description: "Limited permissions that allow only read-only access"
permissions: ["view"]
children:
namespace: ["view"]
template: ["view", "viewFiles", "execute"]
workspace: ["view"]
panel: ["view"]
signal: ["view", "query"]
signalSet: ["view", "query"]
# template:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view", "viewFiles", "execute"]
#
# workspace:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view"]
#
# panel:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view"]
#
# signal:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view", "query"]
#
# signalSet:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view", "query"]
mysql:
host: localhost
user: mvis
password: mvis
database: mvis
mailtrain:
url: http://localhost:3000/
namespaces:
campaigns: 2
userRole: mailtrainUser
www:
# HTTP interface to listen on
host: 127.0.0.1
# HTTP(S) port to listen on
trustedPort: 3010
trustedPortIsHttps: false
sandboxPort: 3011
sandboxPortIsHttps: false
apiPort: 3012
apiPortIsHttps: false
trustedUrlBase: http://localhost:3010
sandboxUrlBase: http://localhost:3011
roles:
global:
mailtrainUser:
name: "Mailtrain User"
admin: true
description: "Limited permissions that allow only read-only access"
permissions:
namespace:
mailtrainUser:
name: "Mailtrain User"
description: "Limited permissions that allow only read-only access"
permissions: ["view"]
children:
namespace: ["view"]
template: ["view", "viewFiles", "execute"]
workspace: ["view"]
panel: ["view"]
signal: ["view", "query"]
signalSet: ["view", "query"]
# template:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view", "viewFiles", "execute"]
#
# workspace:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view"]
#
# panel:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view"]
#
# signal:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view", "query"]
#
# signalSet:
# mailtrainUser:
# name: "Mailtrain User"
# description: "Limited permissions that allow only read-only access"
# permissions: ["view", "query"]

View file

@ -1,2 +1,2 @@
/dist
/node_modules
/dist
/node_modules

View file

@ -1,8 +1,8 @@
<div id="evif-panel" />
<script type="text/javascript" src="/ivis.js"></script>
<script type="text/javascript">
IVIS.embedPanel('evif-panel', '{{ivisSandboxUrlBase}}', {{panelId}}, '{{token}}');
</script>
<div id="evif-panel" />
<script type="text/javascript" src="/ivis.js"></script>
<script type="text/javascript">
IVIS.embedPanel('evif-panel', '{{ivisSandboxUrlBase}}', {{panelId}}, '{{token}}');
</script>

View file

@ -1,32 +1,32 @@
'use strict';
function convertFileURLs(sourceCustom, fromEntityType, fromEntityId, toEntityType, toEntityId) {
function convertText(text) {
if (text) {
const fromUrl = `/files/${fromEntityType}/file/${fromEntityId}`;
const toUrl = `/files/${toEntityType}/file/${toEntityId}`;
const encodedFromUrl = encodeURIComponent(fromUrl);
const encodedToUrl = encodeURIComponent(toUrl);
text = text.split('[URL_BASE]' + fromUrl).join('[URL_BASE]' + toUrl);
text = text.split('[SANDBOX_URL_BASE]' + fromUrl).join('[SANDBOX_URL_BASE]' + toUrl);
text = text.split('[ENCODED_URL_BASE]' + encodedFromUrl).join('[ENCODED_URL_BASE]' + encodedToUrl);
text = text.split('[ENCODED_SANDBOX_URL_BASE]' + encodedFromUrl).join('[ENCODED_SANDBOX_URL_BASE]' + encodedToUrl);
}
return text;
}
sourceCustom.html = convertText(sourceCustom.html);
sourceCustom.text = convertText(sourceCustom.text);
if (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
}
}
'use strict';
function convertFileURLs(sourceCustom, fromEntityType, fromEntityId, toEntityType, toEntityId) {
function convertText(text) {
if (text) {
const fromUrl = `/files/${fromEntityType}/file/${fromEntityId}`;
const toUrl = `/files/${toEntityType}/file/${toEntityId}`;
const encodedFromUrl = encodeURIComponent(fromUrl);
const encodedToUrl = encodeURIComponent(toUrl);
text = text.split('[URL_BASE]' + fromUrl).join('[URL_BASE]' + toUrl);
text = text.split('[SANDBOX_URL_BASE]' + fromUrl).join('[SANDBOX_URL_BASE]' + toUrl);
text = text.split('[ENCODED_URL_BASE]' + encodedFromUrl).join('[ENCODED_URL_BASE]' + encodedToUrl);
text = text.split('[ENCODED_SANDBOX_URL_BASE]' + encodedFromUrl).join('[ENCODED_SANDBOX_URL_BASE]' + encodedToUrl);
}
return text;
}
sourceCustom.html = convertText(sourceCustom.html);
sourceCustom.text = convertText(sourceCustom.text);
if (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
}
}
module.exports.convertFileURLs = convertFileURLs;

View file

@ -1,151 +1,151 @@
'use strict';
const ReplacementBehavior = {
NONE: 1,
REPLACE: 2,
RENAME: 3
};
const entityTypes = {
namespace: {
entitiesTable: 'namespaces',
sharesTable: 'shares_namespace',
permissionsTable: 'permissions_namespace',
clientLink: id => `/namespaces/${id}`
},
list: {
entitiesTable: 'lists',
sharesTable: 'shares_list',
permissionsTable: 'permissions_list',
clientLink: id => `/lists/${id}`
},
customForm: {
entitiesTable: 'custom_forms',
sharesTable: 'shares_custom_form',
permissionsTable: 'permissions_custom_form',
clientLink: id => `/lists/forms/${id}`
},
campaign: {
entitiesTable: 'campaigns',
sharesTable: 'shares_campaign',
permissionsTable: 'permissions_campaign',
dependentPermissions: {
extraColumns: ['parent'],
getParent: entity => entity.parent
},
files: {
file: {
table: 'files_campaign_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
attachment: {
table: 'files_campaign_attachment',
permissions: {
view: 'viewAttachments',
manage: 'manageAttachments'
},
defaultReplacementBehavior: ReplacementBehavior.NONE
}
},
clientLink: id => `/campaigns/${id}`
},
template: {
entitiesTable: 'templates',
sharesTable: 'shares_template',
permissionsTable: 'permissions_template',
files: {
file: {
table: 'files_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/${id}`
},
sendConfiguration: {
entitiesTable: 'send_configurations',
sharesTable: 'shares_send_configuration',
permissionsTable: 'permissions_send_configuration',
clientLink: id => `/send-configurations/${id}`
},
report: {
entitiesTable: 'reports',
sharesTable: 'shares_report',
permissionsTable: 'permissions_report',
clientLink: id => `/reports/${id}`
},
reportTemplate: {
entitiesTable: 'report_templates',
sharesTable: 'shares_report_template',
permissionsTable: 'permissions_report_template',
clientLink: id => `/reports/templates/${id}`
},
mosaicoTemplate: {
entitiesTable: 'mosaico_templates',
sharesTable: 'shares_mosaico_template',
permissionsTable: 'permissions_mosaico_template',
files: {
file: {
table: 'files_mosaico_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
block: {
table: 'files_mosaico_template_block',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/mosaico/${id}`
},
user: {
entitiesTable: 'users',
clientLink: id => `/users/${id}`
}
};
const entityTypesWithPermissions = {};
for (const key in entityTypes) {
if (entityTypes[key].permissionsTable) {
entityTypesWithPermissions[key] = entityTypes[key];
}
}
function getEntityTypes() {
return entityTypes;
}
function getEntityTypesWithPermissions() {
return entityTypesWithPermissions;
}
function getEntityType(entityTypeId) {
const entityType = entityTypes[entityTypeId];
if (!entityType) {
throw new Error(`Unknown entity type ${entityTypeId}`);
}
return entityType
}
module.exports = {
getEntityTypes,
getEntityTypesWithPermissions,
getEntityType,
ReplacementBehavior
'use strict';
const ReplacementBehavior = {
NONE: 1,
REPLACE: 2,
RENAME: 3
};
const entityTypes = {
namespace: {
entitiesTable: 'namespaces',
sharesTable: 'shares_namespace',
permissionsTable: 'permissions_namespace',
clientLink: id => `/namespaces/${id}`
},
list: {
entitiesTable: 'lists',
sharesTable: 'shares_list',
permissionsTable: 'permissions_list',
clientLink: id => `/lists/${id}`
},
customForm: {
entitiesTable: 'custom_forms',
sharesTable: 'shares_custom_form',
permissionsTable: 'permissions_custom_form',
clientLink: id => `/lists/forms/${id}`
},
campaign: {
entitiesTable: 'campaigns',
sharesTable: 'shares_campaign',
permissionsTable: 'permissions_campaign',
dependentPermissions: {
extraColumns: ['parent'],
getParent: entity => entity.parent
},
files: {
file: {
table: 'files_campaign_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
attachment: {
table: 'files_campaign_attachment',
permissions: {
view: 'viewAttachments',
manage: 'manageAttachments'
},
defaultReplacementBehavior: ReplacementBehavior.NONE
}
},
clientLink: id => `/campaigns/${id}`
},
template: {
entitiesTable: 'templates',
sharesTable: 'shares_template',
permissionsTable: 'permissions_template',
files: {
file: {
table: 'files_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/${id}`
},
sendConfiguration: {
entitiesTable: 'send_configurations',
sharesTable: 'shares_send_configuration',
permissionsTable: 'permissions_send_configuration',
clientLink: id => `/send-configurations/${id}`
},
report: {
entitiesTable: 'reports',
sharesTable: 'shares_report',
permissionsTable: 'permissions_report',
clientLink: id => `/reports/${id}`
},
reportTemplate: {
entitiesTable: 'report_templates',
sharesTable: 'shares_report_template',
permissionsTable: 'permissions_report_template',
clientLink: id => `/reports/templates/${id}`
},
mosaicoTemplate: {
entitiesTable: 'mosaico_templates',
sharesTable: 'shares_mosaico_template',
permissionsTable: 'permissions_mosaico_template',
files: {
file: {
table: 'files_mosaico_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
block: {
table: 'files_mosaico_template_block',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/mosaico/${id}`
},
user: {
entitiesTable: 'users',
clientLink: id => `/users/${id}`
}
};
const entityTypesWithPermissions = {};
for (const key in entityTypes) {
if (entityTypes[key].permissionsTable) {
entityTypesWithPermissions[key] = entityTypes[key];
}
}
function getEntityTypes() {
return entityTypes;
}
function getEntityTypesWithPermissions() {
return entityTypesWithPermissions;
}
function getEntityType(entityTypeId) {
const entityType = entityTypes[entityTypeId];
if (!entityType) {
throw new Error(`Unknown entity type ${entityTypeId}`);
}
return entityType
}
module.exports = {
getEntityTypes,
getEntityTypesWithPermissions,
getEntityType,
ReplacementBehavior
}

View file

@ -1,8 +1,8 @@
'use strict';
const config = require('config');
const log = require('npmlog');
log.level = config.log.level;
'use strict';
const config = require('config');
const log = require('npmlog');
log.level = config.log.level;
module.exports = log;

View file

@ -1,24 +1,24 @@
'use strict';
const { enforce } = require('./helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const shares = require('../models/shares');
async function validateEntity(tx, entity) {
enforce(entity.namespace, 'Entity namespace not set');
if (!await tx('namespaces').where('id', entity.namespace).first()) {
throw new interoperableErrors.NamespaceNotFoundError();
}
}
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
if (existing.namespace !== entity.namespace) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
}
}
module.exports = {
validateEntity,
validateMove
'use strict';
const { enforce } = require('./helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const shares = require('../models/shares');
async function validateEntity(tx, entity) {
enforce(entity.namespace, 'Entity namespace not set');
if (!await tx('namespaces').where('id', entity.namespace).first()) {
throw new interoperableErrors.NamespaceNotFoundError();
}
}
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
if (existing.namespace !== entity.namespace) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
}
}
module.exports = {
validateEntity,
validateMove
};

View file

@ -1,15 +1,15 @@
'use strict';
const nodeify = require('nodeify');
module.exports.nodeifyPromise = nodeify;
module.exports.nodeifyFunction = (asyncFun) => {
return (...args) => {
const callback = args.pop();
const promise = asyncFun(...args);
return module.exports.nodeifyPromise(promise, callback);
};
};
'use strict';
const nodeify = require('nodeify');
module.exports.nodeifyPromise = nodeify;
module.exports.nodeifyFunction = (asyncFun) => {
return (...args) => {
const callback = args.pop();
const promise = asyncFun(...args);
return module.exports.nodeifyPromise(promise, callback);
};
};

View file

@ -1,72 +1,72 @@
'use strict';
const config = require('config');
const urllib = require('url');
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
const {getLangCodeFromExpressLocale} = require('./translate');
function getTrustedUrlBase() {
return urllib.resolve(config.www.trustedUrlBase, '');
}
function getSandboxUrlBase() {
return urllib.resolve(config.www.sandboxUrlBase, '');
}
function getPublicUrlBase() {
return urllib.resolve(config.www.publicUrlBase, '');
}
function _getUrl(urlBase, path, opts) {
const url = new URL(path || '', urlBase);
if (opts && opts.locale) {
url.searchParams.append('locale', getLangCodeFromExpressLocale(opts.locale));
}
return url.toString();
}
function getTrustedUrl(path, opts) {
return _getUrl(config.www.trustedUrlBase, path || '', opts);
}
function getSandboxUrl(path, context, opts) {
if (context && context.user && context.user.restrictedAccessToken) {
return _getUrl(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''), opts);
} else {
return _getUrl(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''), opts);
}
}
function getPublicUrl(path, opts) {
return _getUrl(config.www.publicUrlBase, path || '', opts);
}
function getTrustedUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.trustedUrlBase);
return mailtrainUrl.pathname;
}
function getSandboxUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.sandboxUrlBase);
return mailtrainUrl.pathname;
}
function getPublicUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.publicUrlBase);
return mailtrainUrl.pathname;
}
module.exports = {
getTrustedUrl,
getSandboxUrl,
getPublicUrl,
getTrustedUrlBase,
getSandboxUrlBase,
getPublicUrlBase,
getTrustedUrlBaseDir,
getSandboxUrlBaseDir,
getPublicUrlBaseDir
'use strict';
const config = require('config');
const urllib = require('url');
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
const {getLangCodeFromExpressLocale} = require('./translate');
function getTrustedUrlBase() {
return urllib.resolve(config.www.trustedUrlBase, '');
}
function getSandboxUrlBase() {
return urllib.resolve(config.www.sandboxUrlBase, '');
}
function getPublicUrlBase() {
return urllib.resolve(config.www.publicUrlBase, '');
}
function _getUrl(urlBase, path, opts) {
const url = new URL(path || '', urlBase);
if (opts && opts.locale) {
url.searchParams.append('locale', getLangCodeFromExpressLocale(opts.locale));
}
return url.toString();
}
function getTrustedUrl(path, opts) {
return _getUrl(config.www.trustedUrlBase, path || '', opts);
}
function getSandboxUrl(path, context, opts) {
if (context && context.user && context.user.restrictedAccessToken) {
return _getUrl(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''), opts);
} else {
return _getUrl(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''), opts);
}
}
function getPublicUrl(path, opts) {
return _getUrl(config.www.publicUrlBase, path || '', opts);
}
function getTrustedUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.trustedUrlBase);
return mailtrainUrl.pathname;
}
function getSandboxUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.sandboxUrlBase);
return mailtrainUrl.pathname;
}
function getPublicUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.publicUrlBase);
return mailtrainUrl.pathname;
}
module.exports = {
getTrustedUrl,
getSandboxUrl,
getPublicUrl,
getTrustedUrlBase,
getSandboxUrlBase,
getPublicUrlBase,
getTrustedUrlBaseDir,
getSandboxUrlBaseDir,
getPublicUrlBaseDir
};

View file

@ -1,3 +1,3 @@
*
!.gitignore
*
!.gitignore
!README.md

View file

@ -1,9 +1,9 @@
'use strict';
if (!process.env.NODE_CONFIG_DIR) {
process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
}
const config = require('server/setup/knex/config');
module.exports = config;
'use strict';
if (!process.env.NODE_CONFIG_DIR) {
process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
}
const config = require('server/setup/knex/config');
module.exports = config;

View file

@ -1,8 +1,8 @@
'use strict';
const config = require('./config');
module.exports = {
client: 'mysql',
connection: config.mysql
};
'use strict';
const config = require('./config');
module.exports = {
client: 'mysql',
connection: config.mysql
};

View file

@ -1,18 +1,18 @@
{{#if uaCode}}
<script>
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function() {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', '{{uaCode}}', 'auto');
ga('send', 'pageview');
</script>
{{/if}}
{{#if uaCode}}
<script>
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function() {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', '{{uaCode}}', 'auto');
ga('send', 'pageview');
</script>
{{/if}}

View file

@ -1,78 +1,78 @@
'use strict';
const CampaignSource = {
MIN: 1,
TEMPLATE: 1,
CUSTOM: 2,
CUSTOM_FROM_TEMPLATE: 3,
CUSTOM_FROM_CAMPAIGN: 4,
URL: 5,
MAX: 5
};
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,
// For campaign types: NORMAL, RSS_ENTRY
SENDING: 7,
MAX: 8
};
const campaignOverridables = ['from_name', 'from_email', 'reply_to', 'subject'];
function getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration) {
let allowedOverride = false;
let disallowedOverride = false;
for (const overridable of campaignOverridables) {
if (campaign[overridable + '_override'] !== null) {
if (sendConfiguration[overridable + '_overridable']) {
allowedOverride = true;
} else {
disallowedOverride = true;
}
}
}
let requiredPermission = 'sendWithoutOverrides';
if (allowedOverride) {
requiredPermission = 'sendWithAllowedOverrides';
}
if (disallowedOverride) {
requiredPermission = 'sendWithAnyOverrides';
}
return requiredPermission;
}
module.exports = {
CampaignSource,
CampaignType,
CampaignStatus,
campaignOverridables,
getSendConfigurationPermissionRequiredForSend
'use strict';
const CampaignSource = {
MIN: 1,
TEMPLATE: 1,
CUSTOM: 2,
CUSTOM_FROM_TEMPLATE: 3,
CUSTOM_FROM_CAMPAIGN: 4,
URL: 5,
MAX: 5
};
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,
// For campaign types: NORMAL, RSS_ENTRY
SENDING: 7,
MAX: 8
};
const campaignOverridables = ['from_name', 'from_email', 'reply_to', 'subject'];
function getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration) {
let allowedOverride = false;
let disallowedOverride = false;
for (const overridable of campaignOverridables) {
if (campaign[overridable + '_override'] !== null) {
if (sendConfiguration[overridable + '_overridable']) {
allowedOverride = true;
} else {
disallowedOverride = true;
}
}
}
let requiredPermission = 'sendWithoutOverrides';
if (allowedOverride) {
requiredPermission = 'sendWithAllowedOverrides';
}
if (disallowedOverride) {
requiredPermission = 'sendWithAnyOverrides';
}
return requiredPermission;
}
module.exports = {
CampaignSource,
CampaignType,
CampaignStatus,
campaignOverridables,
getSendConfigurationPermissionRequiredForSend
};

View file

@ -1,75 +1,75 @@
'use strict';
const moment = require('moment');
const birthdayYear = 2000;
const DateFormat = {
US: 'us',
EU: 'eur',
INTL: 'intl'
};
const dateFormatStrings = {
'us': 'MM/DD/YYYY',
'eur': 'DD/MM/YYYY',
'intl': 'YYYY-MM-DD'
};
const birthdayFormatStrings = {
'us': 'MM/DD',
'eur': 'DD/MM',
'intl': 'MM/DD'
};
function parseDate(format, text) {
const date = moment.utc(text, dateFormatStrings[format]);
if (date.isValid()) {
return date.toDate();
}
}
function parseBirthday(format, text) {
const fullDateStr = format === DateFormat.INTL ? birthdayYear + '-' + text : text + '-' + birthdayYear;
const date = moment.utc(fullDateStr, dateFormatStrings[format]);
if (date.isValid()) {
return date.toDate();
}
}
function formatDate(format, date) {
if (date === null) {
return '';
} else {
return moment.utc(date).format(dateFormatStrings[format]);
}
}
function formatBirthday(format, date) {
if (date === null) {
return '';
} else {
return moment.utc(date).format(birthdayFormatStrings[format]);
}
}
function getDateFormatString(format) {
return dateFormatStrings[format];
}
function getBirthdayFormatString(format) {
return birthdayFormatStrings[format];
}
module.exports = {
DateFormat,
birthdayYear,
parseDate,
parseBirthday,
formatDate,
formatBirthday,
getDateFormatString,
getBirthdayFormatString
'use strict';
const moment = require('moment');
const birthdayYear = 2000;
const DateFormat = {
US: 'us',
EU: 'eur',
INTL: 'intl'
};
const dateFormatStrings = {
'us': 'MM/DD/YYYY',
'eur': 'DD/MM/YYYY',
'intl': 'YYYY-MM-DD'
};
const birthdayFormatStrings = {
'us': 'MM/DD',
'eur': 'DD/MM',
'intl': 'MM/DD'
};
function parseDate(format, text) {
const date = moment.utc(text, dateFormatStrings[format]);
if (date.isValid()) {
return date.toDate();
}
}
function parseBirthday(format, text) {
const fullDateStr = format === DateFormat.INTL ? birthdayYear + '-' + text : text + '-' + birthdayYear;
const date = moment.utc(fullDateStr, dateFormatStrings[format]);
if (date.isValid()) {
return date.toDate();
}
}
function formatDate(format, date) {
if (date === null) {
return '';
} else {
return moment.utc(date).format(dateFormatStrings[format]);
}
}
function formatBirthday(format, date) {
if (date === null) {
return '';
} else {
return moment.utc(date).format(birthdayFormatStrings[format]);
}
}
function getDateFormatString(format) {
return dateFormatStrings[format];
}
function getBirthdayFormatString(format) {
return birthdayFormatStrings[format];
}
module.exports = {
DateFormat,
birthdayYear,
parseDate,
parseBirthday,
formatDate,
formatBirthday,
getDateFormatString,
getBirthdayFormatString
};

View file

@ -1,82 +1,82 @@
'use strict';
const ImportSource = {
MIN: 0,
CSV_FILE: 0,
LIST: 1,
MAX: 1
};
const MappingType = {
MIN: 0,
BASIC_SUBSCRIBE: 0,
BASIC_UNSUBSCRIBE: 1,
MAX: 1
};
const ImportStatus = {
PREP_SCHEDULED: 0,
PREP_RUNNING: 1,
PREP_STOPPING: 2,
PREP_FINISHED: 3,
PREP_FAILED: 4,
RUN_SCHEDULED: 5,
RUN_RUNNING: 6,
RUN_STOPPING: 7,
RUN_FINISHED: 8,
RUN_FAILED: 9
};
const RunStatus = {
SCHEDULED: 0,
RUNNING: 1,
STOPPING: 2,
FINISHED: 3,
FAILED: 4
};
function prepInProgress(status) {
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING;
}
function runInProgress(status) {
return status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
}
function inProgress(status) {
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING ||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
}
function prepFinished(status) {
return status === ImportStatus.PREP_FINISHED ||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING ||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
}
function prepFinishedAndNotInProgress(status) {
return status === ImportStatus.PREP_FINISHED ||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
}
function runStatusInProgress(status) {
return status === RunStatus.SCHEDULED || status === RunStatus.RUNNING || status === RunStatus.STOPPING;
}
module.exports = {
ImportSource,
MappingType,
ImportStatus,
RunStatus,
prepInProgress,
runInProgress,
prepFinished,
prepFinishedAndNotInProgress,
inProgress,
runStatusInProgress
'use strict';
const ImportSource = {
MIN: 0,
CSV_FILE: 0,
LIST: 1,
MAX: 1
};
const MappingType = {
MIN: 0,
BASIC_SUBSCRIBE: 0,
BASIC_UNSUBSCRIBE: 1,
MAX: 1
};
const ImportStatus = {
PREP_SCHEDULED: 0,
PREP_RUNNING: 1,
PREP_STOPPING: 2,
PREP_FINISHED: 3,
PREP_FAILED: 4,
RUN_SCHEDULED: 5,
RUN_RUNNING: 6,
RUN_STOPPING: 7,
RUN_FINISHED: 8,
RUN_FAILED: 9
};
const RunStatus = {
SCHEDULED: 0,
RUNNING: 1,
STOPPING: 2,
FINISHED: 3,
FAILED: 4
};
function prepInProgress(status) {
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING;
}
function runInProgress(status) {
return status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
}
function inProgress(status) {
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING ||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
}
function prepFinished(status) {
return status === ImportStatus.PREP_FINISHED ||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING ||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
}
function prepFinishedAndNotInProgress(status) {
return status === ImportStatus.PREP_FINISHED ||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
}
function runStatusInProgress(status) {
return status === RunStatus.SCHEDULED || status === RunStatus.RUNNING || status === RunStatus.STOPPING;
}
module.exports = {
ImportSource,
MappingType,
ImportStatus,
RunStatus,
prepInProgress,
runInProgress,
prepFinished,
prepFinishedAndNotInProgress,
inProgress,
runStatusInProgress
};

View file

@ -1,150 +1,150 @@
'use strict';
class InteroperableError extends Error {
constructor(type, msg, data) {
super(msg);
this.type = type;
this.data = data;
}
}
class NotLoggedInError extends InteroperableError {
constructor(msg, data) {
super('NotLoggedInError', msg, data);
}
}
class ChangedError extends InteroperableError {
constructor(msg, data) {
super('ChangedError', msg, data);
}
}
class NotFoundError extends InteroperableError {
constructor(msg, data) {
super('NotFoundError', msg || 'Not Found', data);
this.status = 404;
}
}
class LoopDetectedError extends InteroperableError {
constructor(msg, data) {
super('LoopDetectedError', msg, data);
}
}
class DuplicitNameError extends InteroperableError {
constructor(msg, data) {
super('DuplicitNameError', msg, data);
}
}
class DuplicitEmailError extends InteroperableError {
constructor(msg, data) {
super('DuplicitEmailError', msg, data);
}
}
class DuplicitKeyError extends InteroperableError {
constructor(msg, data) {
super('DuplicitKeyError', msg, data);
}
}
class IncorrectPasswordError extends InteroperableError {
constructor(msg, data) {
super('IncorrectPasswordError', msg, data);
}
}
class InvalidTokenError extends InteroperableError {
constructor(msg, data) {
super('InvalidTokenError', msg, data);
}
}
class DependencyNotFoundError extends InteroperableError {
constructor(msg, data) {
super('DependencyNotFoundError', msg, data);
}
}
class NamespaceNotFoundError extends InteroperableError {
constructor(msg, data) {
super('NamespaceNotFoundError', msg, data);
}
}
class PermissionDeniedError extends InteroperableError {
constructor(msg, data) {
super('PermissionDeniedError', msg || 'Permission Denied', data);
this.status = 403;
}
}
class InvalidConfirmationForSubscriptionError extends InteroperableError {
constructor(msg, data) {
super('InvalidConfirmationForSubscriptionError', msg, data);
}
}
class InvalidConfirmationForAddressChangeError extends InteroperableError {
constructor(msg, data) {
super('InvalidConfirmationForAddressChangeError', msg, data);
}
}
class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
constructor(msg, data) {
super('InvalidConfirmationForUnsubscriptionError', msg, data);
}
}
class DependencyPresentError extends InteroperableError {
constructor(msg, data) {
super('DependencyPresentError', msg, data);
}
}
class InvalidStateError extends InteroperableError {
constructor(msg, data) {
super('InvalidStateError', msg, data);
}
}
const errorTypes = {
InteroperableError,
NotLoggedInError,
ChangedError,
NotFoundError,
LoopDetectedError,
DuplicitNameError,
DuplicitEmailError,
DuplicitKeyError,
IncorrectPasswordError,
InvalidTokenError,
DependencyNotFoundError,
NamespaceNotFoundError,
PermissionDeniedError,
InvalidConfirmationForSubscriptionError,
InvalidConfirmationForAddressChangeError,
InvalidConfirmationForUnsubscriptionError,
DependencyPresentError,
InvalidStateError
};
function deserialize(errorObj) {
if (errorObj.type) {
const ctor = errorTypes[errorObj.type];
if (ctor) {
return new ctor(errorObj.message, errorObj.data);
} else {
console.log('Warning unknown type of interoperable error: ' + errorObj.type);
}
}
}
module.exports = Object.assign({}, errorTypes, {
deserialize
'use strict';
class InteroperableError extends Error {
constructor(type, msg, data) {
super(msg);
this.type = type;
this.data = data;
}
}
class NotLoggedInError extends InteroperableError {
constructor(msg, data) {
super('NotLoggedInError', msg, data);
}
}
class ChangedError extends InteroperableError {
constructor(msg, data) {
super('ChangedError', msg, data);
}
}
class NotFoundError extends InteroperableError {
constructor(msg, data) {
super('NotFoundError', msg || 'Not Found', data);
this.status = 404;
}
}
class LoopDetectedError extends InteroperableError {
constructor(msg, data) {
super('LoopDetectedError', msg, data);
}
}
class DuplicitNameError extends InteroperableError {
constructor(msg, data) {
super('DuplicitNameError', msg, data);
}
}
class DuplicitEmailError extends InteroperableError {
constructor(msg, data) {
super('DuplicitEmailError', msg, data);
}
}
class DuplicitKeyError extends InteroperableError {
constructor(msg, data) {
super('DuplicitKeyError', msg, data);
}
}
class IncorrectPasswordError extends InteroperableError {
constructor(msg, data) {
super('IncorrectPasswordError', msg, data);
}
}
class InvalidTokenError extends InteroperableError {
constructor(msg, data) {
super('InvalidTokenError', msg, data);
}
}
class DependencyNotFoundError extends InteroperableError {
constructor(msg, data) {
super('DependencyNotFoundError', msg, data);
}
}
class NamespaceNotFoundError extends InteroperableError {
constructor(msg, data) {
super('NamespaceNotFoundError', msg, data);
}
}
class PermissionDeniedError extends InteroperableError {
constructor(msg, data) {
super('PermissionDeniedError', msg || 'Permission Denied', data);
this.status = 403;
}
}
class InvalidConfirmationForSubscriptionError extends InteroperableError {
constructor(msg, data) {
super('InvalidConfirmationForSubscriptionError', msg, data);
}
}
class InvalidConfirmationForAddressChangeError extends InteroperableError {
constructor(msg, data) {
super('InvalidConfirmationForAddressChangeError', msg, data);
}
}
class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
constructor(msg, data) {
super('InvalidConfirmationForUnsubscriptionError', msg, data);
}
}
class DependencyPresentError extends InteroperableError {
constructor(msg, data) {
super('DependencyPresentError', msg, data);
}
}
class InvalidStateError extends InteroperableError {
constructor(msg, data) {
super('InvalidStateError', msg, data);
}
}
const errorTypes = {
InteroperableError,
NotLoggedInError,
ChangedError,
NotFoundError,
LoopDetectedError,
DuplicitNameError,
DuplicitEmailError,
DuplicitKeyError,
IncorrectPasswordError,
InvalidTokenError,
DependencyNotFoundError,
NamespaceNotFoundError,
PermissionDeniedError,
InvalidConfirmationForSubscriptionError,
InvalidConfirmationForAddressChangeError,
InvalidConfirmationForUnsubscriptionError,
DependencyPresentError,
InvalidStateError
};
function deserialize(errorObj) {
if (errorObj.type) {
const ctor = errorTypes[errorObj.type];
if (ctor) {
return new ctor(errorObj.message, errorObj.data);
} else {
console.log('Warning unknown type of interoperable error: ' + errorObj.type);
}
}
}
module.exports = Object.assign({}, errorTypes, {
deserialize
});

View file

@ -1,66 +1,66 @@
'use strict';
function convertToFake(dict) {
function convertValueToFakeLang(str) {
let from = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+\\|`~[{]};:'\",<.>/?";
let to = "ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅX⅄Z0123456789¡@#$%ᵥ⅋⁎()-_=+\\|,~[{]};:,„´<.>/¿";
return str.replace(/(\{\{[^\}]+\}\}|%s)/g, '\x00\x04$1\x00').split('\x00').map(c => {
if (c.charAt(0) === '\x04') {
return c;
}
let r = '';
for (let i = 0, len = c.length; i < len; i++) {
let pos = from.indexOf(c.charAt(i));
if (pos < 0) {
r += c.charAt(i);
} else {
r += to.charAt(pos);
}
}
return r;
}).join('\x00').replace(/[\x00\x04]/g, '');
}
function _convertToFake(dict, fakeDict) {
for (const key in dict) {
const val = dict[key];
if (typeof val === 'string') {
fakeDict[key] = convertValueToFakeLang(val);
} else {
fakeDict[key] = _convertToFake(val, {});
}
}
return fakeDict;
}
return _convertToFake(dict, {});
}
// The langugage labels below are intentionally not localized so that they are always native in the langugae of their speaker (regardless of the currently selected language)
const langCodes = {
'en-US': {
getShortLabel: t => 'EN',
getLabel: t => 'English',
longCode: 'en-US'
},
'es-ES': {
getShortLabel: t => 'ES',
getLabel: t => 'Español',
longCode: 'es-ES'
},
'fk-FK': {
getShortLabel: t => 'FK',
getLabel: t => 'Fake',
longCode: 'fk-FK'
}
}
function getLang(lng) {
return langCodes[lng];
}
module.exports.convertToFake = convertToFake;
'use strict';
function convertToFake(dict) {
function convertValueToFakeLang(str) {
let from = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+\\|`~[{]};:'\",<.>/?";
let to = "ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅX⅄Z0123456789¡@#$%ᵥ⅋⁎()-_=+\\|,~[{]};:,„´<.>/¿";
return str.replace(/(\{\{[^\}]+\}\}|%s)/g, '\x00\x04$1\x00').split('\x00').map(c => {
if (c.charAt(0) === '\x04') {
return c;
}
let r = '';
for (let i = 0, len = c.length; i < len; i++) {
let pos = from.indexOf(c.charAt(i));
if (pos < 0) {
r += c.charAt(i);
} else {
r += to.charAt(pos);
}
}
return r;
}).join('\x00').replace(/[\x00\x04]/g, '');
}
function _convertToFake(dict, fakeDict) {
for (const key in dict) {
const val = dict[key];
if (typeof val === 'string') {
fakeDict[key] = convertValueToFakeLang(val);
} else {
fakeDict[key] = _convertToFake(val, {});
}
}
return fakeDict;
}
return _convertToFake(dict, {});
}
// The langugage labels below are intentionally not localized so that they are always native in the langugae of their speaker (regardless of the currently selected language)
const langCodes = {
'en-US': {
getShortLabel: t => 'EN',
getLabel: t => 'English',
longCode: 'en-US'
},
'es-ES': {
getShortLabel: t => 'ES',
getLabel: t => 'Español',
longCode: 'es-ES'
},
'fk-FK': {
getShortLabel: t => 'FK',
getLabel: t => 'Fake',
longCode: 'fk-FK'
}
}
function getLang(lng) {
return langCodes[lng];
}
module.exports.convertToFake = convertToFake;
module.exports.getLang = getLang;

View file

@ -1,51 +1,51 @@
'use strict';
const UnsubscriptionMode = {
MIN: 0,
ONE_STEP: 0,
ONE_STEP_WITH_FORM: 1,
TWO_STEP: 2,
TWO_STEP_WITH_FORM: 3,
MANUAL: 4,
MAX: 4
};
const SubscriptionStatus = {
MIN: 0,
SUBSCRIBED: 1,
UNSUBSCRIBED: 2,
BOUNCED: 3,
COMPLAINED: 4,
MAX: 4
};
const SubscriptionSource = {
ADMIN_FORM: -1,
SUBSCRIPTION_FORM: -2,
API: -3,
NOT_IMPORTED_V1: -4,
IMPORTED_V1: -5,
ERASED: -6
};
const FieldWizard = {
NONE: 'none',
NAME: 'full_name',
FIRST_LAST_NAME: 'first_last_name'
}
function getFieldColumn(field) {
return field.column || 'grouped_' + field.id;
}
module.exports = {
UnsubscriptionMode,
SubscriptionStatus,
SubscriptionSource,
FieldWizard,
getFieldColumn
'use strict';
const UnsubscriptionMode = {
MIN: 0,
ONE_STEP: 0,
ONE_STEP_WITH_FORM: 1,
TWO_STEP: 2,
TWO_STEP_WITH_FORM: 3,
MANUAL: 4,
MAX: 4
};
const SubscriptionStatus = {
MIN: 0,
SUBSCRIBED: 1,
UNSUBSCRIBED: 2,
BOUNCED: 3,
COMPLAINED: 4,
MAX: 4
};
const SubscriptionSource = {
ADMIN_FORM: -1,
SUBSCRIPTION_FORM: -2,
API: -3,
NOT_IMPORTED_V1: -4,
IMPORTED_V1: -5,
ERASED: -6
};
const FieldWizard = {
NONE: 'none',
NAME: 'full_name',
FIRST_LAST_NAME: 'first_last_name'
}
function getFieldColumn(field) {
return field.column || 'grouped_' + field.id;
}
module.exports = {
UnsubscriptionMode,
SubscriptionStatus,
SubscriptionSource,
FieldWizard,
getFieldColumn
};

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
'use strict';
function getGlobalNamespaceId() {
return 1;
}
module.exports = {
getGlobalNamespaceId
'use strict';
function getGlobalNamespaceId() {
return 1;
}
module.exports = {
getGlobalNamespaceId
};

View file

@ -1,37 +1,37 @@
'use strict';
const util = require('util');
const owaspPasswordStrengthTest = require('owasp-password-strength-test');
function passwordValidator(t) {
const config = {
allowPassphrases: true,
maxLength: 128,
minLength: 10,
minPhraseLength: 20,
minOptionalTestsToPass: 4
};
if (t) {
config.translate = {
minLength: function (minLength) {
return t('thePasswordMustBeAtLeastMinLength', { minLength });
},
maxLength: function (maxLength) {
return t('thePasswordMustBeFewerThanMaxLength', { maxLength });
},
repeat: t('thePasswordMayNotContainSequencesOfThree'),
lowercase: t('thePasswordMustContainAtLeastOne'),
uppercase: t('thePasswordMustContainAtLeastOne-1'),
number: t('thePasswordMustContainAtLeastOneNumber'),
special: t('thePasswordMustContainAtLeastOneSpecial')
}
}
const passwordValidator = owaspPasswordStrengthTest.create();
passwordValidator.config(config);
return passwordValidator;
}
'use strict';
const util = require('util');
const owaspPasswordStrengthTest = require('owasp-password-strength-test');
function passwordValidator(t) {
const config = {
allowPassphrases: true,
maxLength: 128,
minLength: 10,
minPhraseLength: 20,
minOptionalTestsToPass: 4
};
if (t) {
config.translate = {
minLength: function (minLength) {
return t('thePasswordMustBeAtLeastMinLength', { minLength });
},
maxLength: function (maxLength) {
return t('thePasswordMustBeFewerThanMaxLength', { maxLength });
},
repeat: t('thePasswordMayNotContainSequencesOfThree'),
lowercase: t('thePasswordMustContainAtLeastOne'),
uppercase: t('thePasswordMustContainAtLeastOne-1'),
number: t('thePasswordMustContainAtLeastOneNumber'),
special: t('thePasswordMustContainAtLeastOneSpecial')
}
}
const passwordValidator = owaspPasswordStrengthTest.create();
passwordValidator.config(config);
return passwordValidator;
}
module.exports = passwordValidator;

View file

@ -1,16 +1,16 @@
'use strict';
const ReportState = {
MIN: 0,
SCHEDULED: 0,
PROCESSING: 1,
FINISHED: 2,
FAILED: 3,
MAX: 3
};
module.exports = {
ReportState
'use strict';
const ReportState = {
MIN: 0,
SCHEDULED: 0,
PROCESSING: 1,
FINISHED: 2,
FAILED: 3,
MAX: 3
};
module.exports = {
ReportState
};

View file

@ -1,29 +1,29 @@
'use strict';
const MailerType = {
GENERIC_SMTP: 'generic_smtp',
ZONE_MTA: 'zone_mta',
AWS_SES: 'aws_ses'
};
const ZoneMTAType = {
REGULAR: 0,
WITH_HTTP_CONF: 1,
WITH_MAILTRAIN_HEADER_CONF: 2,
BUILTIN: 3
}
function getSystemSendConfigurationId() {
return 1;
}
function getSystemSendConfigurationCid() {
return 'system';
}
module.exports = {
MailerType,
ZoneMTAType,
getSystemSendConfigurationId,
getSystemSendConfigurationCid
'use strict';
const MailerType = {
GENERIC_SMTP: 'generic_smtp',
ZONE_MTA: 'zone_mta',
AWS_SES: 'aws_ses'
};
const ZoneMTAType = {
REGULAR: 0,
WITH_HTTP_CONF: 1,
WITH_MAILTRAIN_HEADER_CONF: 2,
BUILTIN: 3
}
function getSystemSendConfigurationId() {
return 1;
}
function getSystemSendConfigurationCid() {
return 'system';
}
module.exports = {
MailerType,
ZoneMTAType,
getSystemSendConfigurationId,
getSystemSendConfigurationCid
};

View file

@ -1,62 +1,62 @@
'use strict';
function _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
if (trustedBaseUrl.endsWith('/')) {
trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1);
}
if (sandboxBaseUrl.endsWith('/')) {
sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1);
}
if (publicBaseUrl.endsWith('/')) {
publicBaseUrl = publicBaseUrl.substring(0, publicBaseUrl.length - 1);
}
return {trustedBaseUrl, sandboxBaseUrl, publicBaseUrl};
}
function getMergeTagsForBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
return {
URL_BASE: bases.publicBaseUrl,
TRUSTED_URL_BASE: bases.trustedBaseUrl,
SANDBOX_URL_BASE: bases.sandboxBaseUrl,
ENCODED_URL_BASE: encodeURIComponent(bases.publicBaseUrl),
ENCODED_TRUSTED_URL_BASE: encodeURIComponent(bases.trustedBaseUrl),
ENCODED_SANDBOX_URL_BASE: encodeURIComponent(bases.sandboxBaseUrl)
};
}
function base(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
text = text.split('[URL_BASE]').join(bases.publicBaseUrl);
text = text.split('[TRUSTED_URL_BASE]').join(bases.trustedBaseUrl);
text = text.split('[SANDBOX_URL_BASE]').join(bases.sandboxBaseUrl);
text = text.split('[ENCODED_URL_BASE]').join(encodeURIComponent(bases.publicBaseUrl));
text = text.split('[ENCODED_TRUSTED_URL_BASE]').join(encodeURIComponent(bases.trustedBaseUrl));
text = text.split('[ENCODED_SANDBOX_URL_BASE]').join(encodeURIComponent(bases.sandboxBaseUrl));
return text;
}
function unbase(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl, treatAllAsPublic = false) {
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
text = text.split(bases.publicBaseUrl).join('[URL_BASE]');
text = text.split(bases.trustedBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[TRUSTED_URL_BASE]');
text = text.split(bases.sandboxBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[SANDBOX_URL_BASE]');
text = text.split(encodeURIComponent(bases.publicBaseUrl)).join('[ENCODED_URL_BASE]');
text = text.split(encodeURIComponent(bases.trustedBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_TRUSTED_URL_BASE]');
text = text.split(encodeURIComponent(bases.sandboxBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_SANDBOX_URL_BASE]');
return text;
}
module.exports = {
base,
unbase,
getMergeTagsForBases
'use strict';
function _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
if (trustedBaseUrl.endsWith('/')) {
trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1);
}
if (sandboxBaseUrl.endsWith('/')) {
sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1);
}
if (publicBaseUrl.endsWith('/')) {
publicBaseUrl = publicBaseUrl.substring(0, publicBaseUrl.length - 1);
}
return {trustedBaseUrl, sandboxBaseUrl, publicBaseUrl};
}
function getMergeTagsForBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
return {
URL_BASE: bases.publicBaseUrl,
TRUSTED_URL_BASE: bases.trustedBaseUrl,
SANDBOX_URL_BASE: bases.sandboxBaseUrl,
ENCODED_URL_BASE: encodeURIComponent(bases.publicBaseUrl),
ENCODED_TRUSTED_URL_BASE: encodeURIComponent(bases.trustedBaseUrl),
ENCODED_SANDBOX_URL_BASE: encodeURIComponent(bases.sandboxBaseUrl)
};
}
function base(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
text = text.split('[URL_BASE]').join(bases.publicBaseUrl);
text = text.split('[TRUSTED_URL_BASE]').join(bases.trustedBaseUrl);
text = text.split('[SANDBOX_URL_BASE]').join(bases.sandboxBaseUrl);
text = text.split('[ENCODED_URL_BASE]').join(encodeURIComponent(bases.publicBaseUrl));
text = text.split('[ENCODED_TRUSTED_URL_BASE]').join(encodeURIComponent(bases.trustedBaseUrl));
text = text.split('[ENCODED_SANDBOX_URL_BASE]').join(encodeURIComponent(bases.sandboxBaseUrl));
return text;
}
function unbase(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl, treatAllAsPublic = false) {
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
text = text.split(bases.publicBaseUrl).join('[URL_BASE]');
text = text.split(bases.trustedBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[TRUSTED_URL_BASE]');
text = text.split(bases.sandboxBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[SANDBOX_URL_BASE]');
text = text.split(encodeURIComponent(bases.publicBaseUrl)).join('[ENCODED_URL_BASE]');
text = text.split(encodeURIComponent(bases.trustedBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_TRUSTED_URL_BASE]');
text = text.split(encodeURIComponent(bases.sandboxBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_SANDBOX_URL_BASE]');
return text;
}
module.exports = {
base,
unbase,
getMergeTagsForBases
};

View file

@ -1,48 +1,48 @@
'use strict';
const Entity = {
SUBSCRIPTION: 'subscription',
CAMPAIGN: 'campaign'
};
const Event = {
[Entity.SUBSCRIPTION]: {
CREATED: 'created',
LATEST_OPEN: 'latest_open',
LATEST_CLICK: 'latest_click'
},
[Entity.CAMPAIGN]: {
DELIVERED: 'delivered',
OPENED: 'opened',
CLICKED: 'clicked',
NOT_OPENED: 'not_opened',
NOT_CLICKED: 'not_clicked'
}
};
const EntityVals = {
subscription: 'SUBSCRIPTION',
campaign: 'CAMPAIGN'
};
const EventVals = {
[Entity.SUBSCRIPTION]: {
created: 'CREATED',
latest_open: 'LATEST_OPEN',
latest_click: 'LATEST_CLICK'
},
[Entity.CAMPAIGN]: {
delivered: 'DELIVERED',
opened: 'OPENED',
clicked: 'CLICKED',
not_opened: 'NOT_OPENED',
not_clicked: 'NOT_CLICKED'
}
};
module.exports = {
Entity,
Event,
EntityVals,
EventVals
'use strict';
const Entity = {
SUBSCRIPTION: 'subscription',
CAMPAIGN: 'campaign'
};
const Event = {
[Entity.SUBSCRIPTION]: {
CREATED: 'created',
LATEST_OPEN: 'latest_open',
LATEST_CLICK: 'latest_click'
},
[Entity.CAMPAIGN]: {
DELIVERED: 'delivered',
OPENED: 'opened',
CLICKED: 'clicked',
NOT_OPENED: 'not_opened',
NOT_CLICKED: 'not_clicked'
}
};
const EntityVals = {
subscription: 'SUBSCRIPTION',
campaign: 'CAMPAIGN'
};
const EventVals = {
[Entity.SUBSCRIPTION]: {
created: 'CREATED',
latest_open: 'LATEST_OPEN',
latest_click: 'LATEST_CLICK'
},
[Entity.CAMPAIGN]: {
delivered: 'DELIVERED',
opened: 'OPENED',
clicked: 'CLICKED',
not_opened: 'NOT_OPENED',
not_clicked: 'NOT_CLICKED'
}
};
module.exports = {
Entity,
Event,
EntityVals,
EventVals
};

View file

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

View file

@ -1,9 +1,9 @@
'use strict';
function getAdminId() {
return 1;
}
module.exports = {
getAdminId
'use strict';
function getAdminId() {
return 1;
}
module.exports = {
getAdminId
};

View file

@ -1,9 +1,9 @@
'use strict';
function mergeTagValid(mergeTag) {
return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
}
module.exports = {
mergeTagValid
'use strict';
function mergeTagValid(mergeTag) {
return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
}
module.exports = {
mergeTagValid
};