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 ### Front page
- Some dashboard - Some dashboard
### Campaigns ### Campaigns
- List of sent RSS campaigns (?) - List of sent RSS campaigns (?)
### Pull requests ### Pull requests
- Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591 - Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591
- Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b - Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b
- Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13 - Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13
- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3 - Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e - Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
### API ### API
- Add API extensions - Add API extensions
### GDPR ### GDPR
- Refuse editing subscriptions which have been anonymized - Refuse editing subscriptions which have been anonymized
- Add field to subscriptions which says till when the consent has been given - 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 - 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) - 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 ## Migration from Mailtrain v1 to Mailtrain v2
The migration should happen almost automatically. There are however the following caveats: 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`) 1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`)
and update your configs accordingly. 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`). 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`. 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`. 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. 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 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. 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 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 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. 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. 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 { .api {
:global .card h4 { :global .card h4 {
margin-top: 0px; margin-top: 0px;
} }
h4 { h4 {
margin-top: 45px; margin-top: 45px;
} }
} }

View file

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

View file

@ -1,35 +1,35 @@
'use strict'; 'use strict';
import {Entity, Event} from '../../../../shared/triggers'; import {Entity, Event} from '../../../../shared/triggers';
export function getTriggerTypes(t) { export function getTriggerTypes(t) {
const entityLabels = { const entityLabels = {
[Entity.SUBSCRIPTION]: t('subscription'), [Entity.SUBSCRIPTION]: t('subscription'),
[Entity.CAMPAIGN]: t('campaign') [Entity.CAMPAIGN]: t('campaign')
}; };
const SubscriptionEvent = Event[Entity.SUBSCRIPTION]; const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
const CampaignEvent = Event[Entity.CAMPAIGN]; const CampaignEvent = Event[Entity.CAMPAIGN];
const eventLabels = { const eventLabels = {
[Entity.SUBSCRIPTION]: { [Entity.SUBSCRIPTION]: {
[SubscriptionEvent.CREATED]: t('created'), [SubscriptionEvent.CREATED]: t('created'),
[SubscriptionEvent.LATEST_OPEN]: t('latestOpen'), [SubscriptionEvent.LATEST_OPEN]: t('latestOpen'),
[SubscriptionEvent.LATEST_CLICK]: t('latestClick') [SubscriptionEvent.LATEST_CLICK]: t('latestClick')
}, },
[Entity.CAMPAIGN]: { [Entity.CAMPAIGN]: {
[CampaignEvent.DELIVERED]: t('delivered'), [CampaignEvent.DELIVERED]: t('delivered'),
[CampaignEvent.OPENED]: t('opened'), [CampaignEvent.OPENED]: t('opened'),
[CampaignEvent.CLICKED]: t('clicked'), [CampaignEvent.CLICKED]: t('clicked'),
[CampaignEvent.NOT_OPENED]: t('notOpened'), [CampaignEvent.NOT_OPENED]: t('notOpened'),
[CampaignEvent.NOT_CLICKED]: t('notClicked') [CampaignEvent.NOT_CLICKED]: t('notClicked')
} }
}; };
return { return {
entityLabels, entityLabels,
eventLabels eventLabels
}; };
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,51 +1,51 @@
'use strict'; 'use strict';
import React from "react"; import React from "react";
import {ACEEditor} from "../../lib/form"; import {ACEEditor} from "../../lib/form";
import 'brace/mode/html' import 'brace/mode/html'
import 'brace/mode/xml' import 'brace/mode/xml'
export function getTemplateTypesOrder() { export function getTemplateTypesOrder() {
return [/* 'mjml' , */ 'html']; return [/* 'mjml' , */ 'html'];
} }
export function getTemplateTypes(t) { export function getTemplateTypes(t) {
const templateTypes = {}; const templateTypes = {};
function clearBeforeSend(data) { function clearBeforeSend(data) {
delete data.html; delete data.html;
delete data.mjml; delete data.mjml;
} }
templateTypes.html = { templateTypes.html = {
typeName: t('html'), typeName: t('html'),
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>, getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
afterLoad: data => { afterLoad: data => {
data.html = data.data.html; data.html = data.data.html;
}, },
beforeSave: (data) => { beforeSave: (data) => {
data.data = { data.data = {
html: data.html html: data.html
}; };
clearBeforeSend(data); clearBeforeSend(data);
}, },
}; };
templateTypes.mjml = { templateTypes.mjml = {
typeName: t('mjml'), typeName: t('mjml'),
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>, getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
afterLoad: data => { afterLoad: data => {
data.mjml = data.data.mjml; data.mjml = data.data.mjml;
}, },
beforeSave: (data) => { beforeSave: (data) => {
data.data = { data.data = {
mjml: data.mjml mjml: data.mjml
}; };
clearBeforeSend(data); clearBeforeSend(data);
}, },
}; };
return templateTypes; 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. To avoid errors with webpack an empty file has been provided.

View file

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

View file

@ -1,7 +1,7 @@
@font-face { @font-face {
font-family: 'Noto Sans'; font-family: 'Noto Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('./noto-sans-400-normal.eot'); 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'); 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 ## Access Control
This document describes the key features and concepts of the current state of This document describes the key features and concepts of the current state of
access control in Mailtrain. access control in Mailtrain.
The current state provides user management and granular access control to reports The current state provides user management and granular access control to reports
and report templates. The user management supports both local authentication and and report templates. The user management supports both local authentication and
LDAP-based authentication. LDAP-based authentication.
The access control has two abstractions levels: a high-level intended to be used through web UI, 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 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 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. to define the access roles in Mailtrain to reflect an organisational or process hierarchy.
### High-level access management (through web UI) ### High-level access management (through web UI)
On the high abstraction level, which is accessible to users via the web-based UI, Mailtrain 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 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 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 or deleting it). Access to entities is provided through so called "shares". A share is essentially
a triple: entity - role - user. a triple: entity - role - user.
Mailtrain further features hierarchical namespaces. Every entity has to reside in a namespace 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). (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 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 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 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 particular namespaces further determines the access to all different entity types that can
reside in the namespace. reside in the namespace.
To simplify the management of permissions, every user is associated with one global role and 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 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 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 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 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 specify that a user get administrator's role in the root namespace, which effectively gives
him/her access to everything. him/her access to everything.
Mailtrain resets these default shares at start and also whenever permission cache is rebuilt 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 (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 role is assigned). This effectively prevents deleting or overriding the default shares that
the user has through the global role. the user has through the global role.
### Low-level access management (through config file) ### Low-level access management (through config file)
Internally, Mailtrain relies on fine-grained permissions, which are triplets: 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 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 in a permission cache (in DB) and automatically generated at startup and whenever the permissions
could have changed. could have changed.
Mailtrain's config file defines the roles (available in the high-level access management) and Mailtrain's config file defines the roles (available in the high-level access management) and
specifies the mapping of roles to operations. specifies the mapping of roles to operations.
The roles are potentially different for each entity type/scope (currently global, namespace, report, 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. 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 A namespace role further defines allowed operations for entity types within and under the
namespace. namespace.
The following defines the role master for scope "global". This effectively means that in 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". "Create/Edit User" form, the user can be given role "Master".
The role gives the permission to rebuild the permission cache. The role gives the permission to rebuild the permission cache.
Further, it specifies that the Further, it specifies that the
holder of the role will automatically be given access (share) to the root namespace in 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 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 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 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 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 more enterprise applications where a single installation of Mailtrain serves a number of rather
independent groups. 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. 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 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. used to give access to other users.
``` ```
[roles.global.master] [roles.global.master]
name="Master" name="Master"
admin=true admin=true
description="All permissions" description="All permissions"
permissions=["rebuildPermissions"] permissions=["rebuildPermissions"]
rootNamespaceRole="master" rootNamespaceRole="master"
``` ```
Another example for a global role is the following. This one is intended for regular users. 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 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 to entities under the namespace in which the user has been created. This is specified by the
```ownNamespaceRole="editor"``` ```ownNamespaceRole="editor"```
``` ```
[roles.global.editor] [roles.global.editor]
name="Editor" name="Editor"
description="Anything under own namespace except operations related to sending and doing reports" description="Anything under own namespace except operations related to sending and doing reports"
permissions=[] permissions=[]
ownNamespaceRole="editor" ownNamespaceRole="editor"
``` ```
The roles for entities are defined in a similar fashion. The example below shows the definition 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 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 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 "master" access to a particular report through this role, the report would either have to be shared with the user
with role "master". with role "master".
``` ```
[roles.report.master] [roles.report.master]
name="Master" name="Master"
description="All permissions" description="All permissions"
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"] permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
``` ```
The same for the restricted role "editor" can look as follows. The same for the restricted role "editor" can look as follows.
``` ```
[roles.report.editor] [roles.report.editor]
name="Editor" name="Editor"
description="Anything under own namespace except operations related to sending and doing reports" description="Anything under own namespace except operations related to sending and doing reports"
permissions=["view", "viewContent", "viewOutput"] permissions=["view", "viewContent", "viewOutput"]
``` ```
The following defines the role "master" for "namespace" entities. Similarly to the example above, 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 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. 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 Additionally, the namespace roles define permissions to all entity types under the namespace
(including child namespaces). (including child namespaces).
``` ```
[roles.namespace.master] [roles.namespace.master]
name="Master" name="Master"
description="All permissions" description="All permissions"
permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"] permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
[roles.namespace.master.children] [roles.namespace.master.children]
reportTemplate=["view", "edit", "delete", "share", "execute"] reportTemplate=["view", "edit", "delete", "share", "execute"]
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"] report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"] namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
``` ```
And the same for the more restricted role "editor". And the same for the more restricted role "editor".
``` ```
[roles.namespace.editor.children] [roles.namespace.editor.children]
reportTemplate=[] reportTemplate=[]
report=["view", "viewContent", "viewOutput"] report=["view", "viewContent", "viewOutput"]
namespace=["view", "edit", "delete"] namespace=["view", "edit", "delete"]
``` ```

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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