Line endings fixed so that we don't have CRLF in Git. Better now than later.
This commit is contained in:
parent
2fe7f82be3
commit
d482d214d9
69 changed files with 6405 additions and 6405 deletions
42
TODO.md
42
TODO.md
|
@ -1,21 +1,21 @@
|
|||
### Front page
|
||||
- Some dashboard
|
||||
|
||||
### Campaigns
|
||||
- List of sent RSS campaigns (?)
|
||||
|
||||
### Pull requests
|
||||
- Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591
|
||||
- Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b
|
||||
- Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13
|
||||
- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
|
||||
- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
|
||||
|
||||
### API
|
||||
- Add API extensions
|
||||
|
||||
### GDPR
|
||||
- Refuse editing subscriptions which have been anonymized
|
||||
- Add field to subscriptions which says till when the consent has been given
|
||||
- Provide a link (and merge tag) that will update the consent date to now
|
||||
- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds)
|
||||
### Front page
|
||||
- Some dashboard
|
||||
|
||||
### Campaigns
|
||||
- List of sent RSS campaigns (?)
|
||||
|
||||
### Pull requests
|
||||
- Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591
|
||||
- Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b
|
||||
- Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13
|
||||
- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
|
||||
- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
|
||||
|
||||
### API
|
||||
- Add API extensions
|
||||
|
||||
### GDPR
|
||||
- Refuse editing subscriptions which have been anonymized
|
||||
- Add field to subscriptions which says till when the consent has been given
|
||||
- Provide a link (and merge tag) that will update the consent date to now
|
||||
- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds)
|
||||
|
|
42
UPGRADE.md
42
UPGRADE.md
|
@ -1,21 +1,21 @@
|
|||
## Migration from Mailtrain v1 to Mailtrain v2
|
||||
|
||||
The migration should happen almost automatically. There are however the following caveats:
|
||||
|
||||
1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`)
|
||||
and update your configs accordingly.
|
||||
|
||||
2. Images uploaded in a template editor (Mosaico, Grapesjs, etc.) need to be manually moved to a new destination (under `client`).
|
||||
For Mosaico, this means to move folders named by a number from `public/mosaico` to `client/static/mosaico`.
|
||||
|
||||
3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/static/mosaico/templates`.
|
||||
|
||||
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
|
||||
|
||||
5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be
|
||||
part of the URL - e.g. webhooks/zone-mta/sender-config/system.
|
||||
|
||||
6. If there are lists that contain birthday or date fields that were created before
|
||||
commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead
|
||||
of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME.
|
||||
Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth.
|
||||
## Migration from Mailtrain v1 to Mailtrain v2
|
||||
|
||||
The migration should happen almost automatically. There are however the following caveats:
|
||||
|
||||
1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`)
|
||||
and update your configs accordingly.
|
||||
|
||||
2. Images uploaded in a template editor (Mosaico, Grapesjs, etc.) need to be manually moved to a new destination (under `client`).
|
||||
For Mosaico, this means to move folders named by a number from `public/mosaico` to `client/static/mosaico`.
|
||||
|
||||
3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/static/mosaico/templates`.
|
||||
|
||||
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
|
||||
|
||||
5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be
|
||||
part of the URL - e.g. webhooks/zone-mta/sender-config/system.
|
||||
|
||||
6. If there are lists that contain birthday or date fields that were created before
|
||||
commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead
|
||||
of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME.
|
||||
Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth.
|
||||
|
|
2
client/.gitignore
vendored
2
client/.gitignore
vendored
|
@ -1 +1 @@
|
|||
/dist
|
||||
/dist
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
.api {
|
||||
:global .card h4 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 45px;
|
||||
}
|
||||
.api {
|
||||
:global .card h4 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 45px;
|
||||
}
|
||||
}
|
|
@ -1,34 +1,34 @@
|
|||
'use strict';
|
||||
|
||||
import {
|
||||
CampaignStatus,
|
||||
CampaignType
|
||||
} from "../../../shared/campaigns";
|
||||
|
||||
export function getCampaignLabels(t) {
|
||||
|
||||
const campaignTypeLabels = {
|
||||
[CampaignType.REGULAR]: t('regular'),
|
||||
[CampaignType.TRIGGERED]: t('triggered'),
|
||||
[CampaignType.RSS]: t('rss')
|
||||
};
|
||||
|
||||
const campaignStatusLabels = {
|
||||
[CampaignStatus.IDLE]: t('idle'),
|
||||
[CampaignStatus.SCHEDULED]: t('scheduled'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.FINISHED]: t('finished'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.INACTIVE]: t('inactive'),
|
||||
[CampaignStatus.ACTIVE]: t('active'),
|
||||
[CampaignStatus.SENDING]: t('sending')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
campaignStatusLabels,
|
||||
campaignTypeLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
CampaignStatus,
|
||||
CampaignType
|
||||
} from "../../../shared/campaigns";
|
||||
|
||||
export function getCampaignLabels(t) {
|
||||
|
||||
const campaignTypeLabels = {
|
||||
[CampaignType.REGULAR]: t('regular'),
|
||||
[CampaignType.TRIGGERED]: t('triggered'),
|
||||
[CampaignType.RSS]: t('rss')
|
||||
};
|
||||
|
||||
const campaignStatusLabels = {
|
||||
[CampaignStatus.IDLE]: t('idle'),
|
||||
[CampaignStatus.SCHEDULED]: t('scheduled'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.FINISHED]: t('finished'),
|
||||
[CampaignStatus.PAUSED]: t('paused'),
|
||||
[CampaignStatus.INACTIVE]: t('inactive'),
|
||||
[CampaignStatus.ACTIVE]: t('active'),
|
||||
[CampaignStatus.SENDING]: t('sending')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
campaignStatusLabels,
|
||||
campaignTypeLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
import {Entity, Event} from '../../../../shared/triggers';
|
||||
|
||||
export function getTriggerTypes(t) {
|
||||
|
||||
const entityLabels = {
|
||||
[Entity.SUBSCRIPTION]: t('subscription'),
|
||||
[Entity.CAMPAIGN]: t('campaign')
|
||||
};
|
||||
|
||||
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
|
||||
const CampaignEvent = Event[Entity.CAMPAIGN];
|
||||
|
||||
const eventLabels = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
[SubscriptionEvent.CREATED]: t('created'),
|
||||
[SubscriptionEvent.LATEST_OPEN]: t('latestOpen'),
|
||||
[SubscriptionEvent.LATEST_CLICK]: t('latestClick')
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
[CampaignEvent.DELIVERED]: t('delivered'),
|
||||
[CampaignEvent.OPENED]: t('opened'),
|
||||
[CampaignEvent.CLICKED]: t('clicked'),
|
||||
[CampaignEvent.NOT_OPENED]: t('notOpened'),
|
||||
[CampaignEvent.NOT_CLICKED]: t('notClicked')
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
entityLabels,
|
||||
eventLabels
|
||||
};
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import {Entity, Event} from '../../../../shared/triggers';
|
||||
|
||||
export function getTriggerTypes(t) {
|
||||
|
||||
const entityLabels = {
|
||||
[Entity.SUBSCRIPTION]: t('subscription'),
|
||||
[Entity.CAMPAIGN]: t('campaign')
|
||||
};
|
||||
|
||||
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
|
||||
const CampaignEvent = Event[Entity.CAMPAIGN];
|
||||
|
||||
const eventLabels = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
[SubscriptionEvent.CREATED]: t('created'),
|
||||
[SubscriptionEvent.LATEST_OPEN]: t('latestOpen'),
|
||||
[SubscriptionEvent.LATEST_CLICK]: t('latestClick')
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
[CampaignEvent.DELIVERED]: t('delivered'),
|
||||
[CampaignEvent.OPENED]: t('opened'),
|
||||
[CampaignEvent.CLICKED]: t('clicked'),
|
||||
[CampaignEvent.NOT_OPENED]: t('notOpened'),
|
||||
[CampaignEvent.NOT_CLICKED]: t('notClicked')
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
entityLabels,
|
||||
eventLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
import csrfToken from 'csrfToken';
|
||||
import axios from 'axios';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
|
||||
const axiosInst = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const axiosWrapper = {
|
||||
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
|
||||
};
|
||||
|
||||
const HTTPMethod = {
|
||||
GET: axiosWrapper.get,
|
||||
PUT: axiosWrapper.put,
|
||||
POST: axiosWrapper.post,
|
||||
DELETE: axiosWrapper.delete
|
||||
};
|
||||
|
||||
axiosWrapper.method = (method, ...args) => method(...args);
|
||||
|
||||
export default axiosWrapper;
|
||||
export {
|
||||
HTTPMethod
|
||||
'use strict';
|
||||
|
||||
import csrfToken from 'csrfToken';
|
||||
import axios from 'axios';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
|
||||
const axiosInst = axios.create({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const axiosWrapper = {
|
||||
get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
|
||||
delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
|
||||
};
|
||||
|
||||
const HTTPMethod = {
|
||||
GET: axiosWrapper.get,
|
||||
PUT: axiosWrapper.put,
|
||||
POST: axiosWrapper.post,
|
||||
DELETE: axiosWrapper.delete
|
||||
};
|
||||
|
||||
axiosWrapper.method = (method, ...args) => method(...args);
|
||||
|
||||
export default axiosWrapper;
|
||||
export {
|
||||
HTTPMethod
|
||||
}
|
662
client/src/lib/bootstrap-components.js
vendored
662
client/src/lib/bootstrap-components.js
vendored
|
@ -1,331 +1,331 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {
|
||||
withAsyncErrorHandler,
|
||||
withErrorHandling
|
||||
} from './error-handling';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class DismissibleAlert extends Component {
|
||||
static propTypes = {
|
||||
severity: PropTypes.string.isRequired,
|
||||
onCloseAsync: PropTypes.func
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Icon extends Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
family: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
family: 'fas'
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
if (props.family === 'fas' || props.family === 'far') {
|
||||
return <i className={`${props.family} fa-${props.icon} ${props.className || ''}`} title={props.title}></i>;
|
||||
} else {
|
||||
console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class Button extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
iconTitle: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'btn';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
let type = props.type || 'button';
|
||||
|
||||
let icon;
|
||||
if (props.icon) {
|
||||
icon = <Icon icon={props.icon} title={props.iconTitle}/>
|
||||
}
|
||||
|
||||
let iconSpacer;
|
||||
if (props.icon && props.label) {
|
||||
iconSpacer = ' ';
|
||||
}
|
||||
|
||||
return (
|
||||
<button type={type} className={className} onClick={::this.onClick} title={this.props.title} disabled={this.props.disabled}>{icon}{iconSpacer}{props.label}</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonDropdown extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
className: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
const className = 'dropdown' + (props.className ? ' ' + props.className : '');
|
||||
const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : '');
|
||||
const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
|
||||
|
||||
return (
|
||||
<div className="dropdown" className={className}>
|
||||
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{props.label}
|
||||
</button>
|
||||
<ul className={menuClassName}>
|
||||
{props.children}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class ActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
href: PropTypes.string
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
return (
|
||||
<a href={props.href || ''} className={props.className} onClick={::this.onClick}>{props.children}</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let clsName = "dropdown-item ";
|
||||
if (props.disabled) {
|
||||
clsName += "disabled ";
|
||||
}
|
||||
|
||||
clsName += props.className;
|
||||
|
||||
return (
|
||||
<ActionLink className={clsName} onClickAsync={props.onClickAsync}>{props.children}</ActionLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownDivider extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'dropdown-divider';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class ModalDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ]
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
onCloseAsync: PropTypes.func,
|
||||
onButtonClickAsync: PropTypes.func,
|
||||
buttons: PropTypes.array,
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
/*
|
||||
this.props.hidden - this is the desired state of the modal
|
||||
this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
|
||||
jqModal.on('shown.bs.modal', () => jqModal.focus());
|
||||
jqModal.on('hide.bs.modal', ::this.onHide);
|
||||
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal({
|
||||
show: !this.props.hidden
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.hidden != this.hidden) {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal(this.props.hidden ? 'hide' : 'show');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too.
|
||||
jQuery('.modal-backdrop').remove();
|
||||
}
|
||||
|
||||
onHide(evt) {
|
||||
// Hide event is emited is both when hidden through user action or through API. We have to let the API
|
||||
// calls through, otherwise the modal would never hide. The user actions, which change the desired state,
|
||||
// are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to
|
||||
// hide the modal or not.
|
||||
if (!this.props.hidden) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onClose();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
await this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
async onButtonClick(idx) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
if (buttonSpec.onClickAsync) {
|
||||
await buttonSpec.onClickAsync(idx);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const t = props.t;
|
||||
|
||||
const buttons = [];
|
||||
for (let idx = 0; idx < this.state.buttons.length; idx++) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={async () => this.onButtonClick(idx)} />
|
||||
buttons.push(button);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(domElem) => { this.domModal = domElem; }}
|
||||
className={'modal fade' + (props.className ? ' ' + props.className : '')}
|
||||
tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">{this.props.title}</h4>
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div className="modal-body">{this.props.children}</div>
|
||||
<div className="modal-footer">
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import PropTypes
|
||||
from 'prop-types';
|
||||
import {
|
||||
withAsyncErrorHandler,
|
||||
withErrorHandling
|
||||
} from './error-handling';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class DismissibleAlert extends Component {
|
||||
static propTypes = {
|
||||
severity: PropTypes.string.isRequired,
|
||||
onCloseAsync: PropTypes.func
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div className={`alert alert-${this.props.severity} alert-dismissible`} role="alert">
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Icon extends Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
family: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
family: 'fas'
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
if (props.family === 'fas' || props.family === 'far') {
|
||||
return <i className={`${props.family} fa-${props.icon} ${props.className || ''}`} title={props.title}></i>;
|
||||
} else {
|
||||
console.error(`Icon font family ${props.family} not supported. (icon: ${props.icon}, title: ${props.title})`)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class Button extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
iconTitle: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'btn';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
let type = props.type || 'button';
|
||||
|
||||
let icon;
|
||||
if (props.icon) {
|
||||
icon = <Icon icon={props.icon} title={props.iconTitle}/>
|
||||
}
|
||||
|
||||
let iconSpacer;
|
||||
if (props.icon && props.label) {
|
||||
iconSpacer = ' ';
|
||||
}
|
||||
|
||||
return (
|
||||
<button type={type} className={className} onClick={::this.onClick} title={this.props.title} disabled={this.props.disabled}>{icon}{iconSpacer}{props.label}</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonDropdown extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
className: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
const className = 'dropdown' + (props.className ? ' ' + props.className : '');
|
||||
const buttonClassName = 'btn dropdown-toggle' + (props.buttonClassName ? ' ' + props.buttonClassName : '');
|
||||
const menuClassName = 'dropdown-menu' + (props.menuClassName ? ' ' + props.menuClassName : '');
|
||||
|
||||
return (
|
||||
<div className="dropdown" className={className}>
|
||||
<button type="button" className={buttonClassName} data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{props.label}
|
||||
</button>
|
||||
<ul className={menuClassName}>
|
||||
{props.children}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@withComponentMixins([
|
||||
withErrorHandling
|
||||
])
|
||||
export class ActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
href: PropTypes.string
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClick(evt) {
|
||||
if (this.props.onClickAsync) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
await this.props.onClickAsync(evt);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
return (
|
||||
<a href={props.href || ''} className={props.className} onClick={::this.onClick}>{props.children}</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownActionLink extends Component {
|
||||
static propTypes = {
|
||||
onClickAsync: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let clsName = "dropdown-item ";
|
||||
if (props.disabled) {
|
||||
clsName += "disabled ";
|
||||
}
|
||||
|
||||
clsName += props.className;
|
||||
|
||||
return (
|
||||
<ActionLink className={clsName} onClickAsync={props.onClickAsync}>{props.children}</ActionLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DropdownDivider extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'dropdown-divider';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation,
|
||||
withErrorHandling
|
||||
])
|
||||
export class ModalDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.state = {
|
||||
buttons: this.props.buttons || [ { label: t('close'), className: 'btn-secondary', onClickAsync: null } ]
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
onCloseAsync: PropTypes.func,
|
||||
onButtonClickAsync: PropTypes.func,
|
||||
buttons: PropTypes.array,
|
||||
hidden: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
/*
|
||||
this.props.hidden - this is the desired state of the modal
|
||||
this.hidden - this is the actual state of the modal - this is because there is no public API on Bootstrap modal to know whether the modal is shown or not
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
|
||||
jqModal.on('shown.bs.modal', () => jqModal.focus());
|
||||
jqModal.on('hide.bs.modal', ::this.onHide);
|
||||
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal({
|
||||
show: !this.props.hidden
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.hidden != this.hidden) {
|
||||
const jqModal = jQuery(this.domModal);
|
||||
this.hidden = this.props.hidden;
|
||||
jqModal.modal(this.props.hidden ? 'hide' : 'show');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// We discard the modal in a hard way (without hiding it). Thus we have to take care of the backgrop too.
|
||||
jQuery('.modal-backdrop').remove();
|
||||
}
|
||||
|
||||
onHide(evt) {
|
||||
// Hide event is emited is both when hidden through user action or through API. We have to let the API
|
||||
// calls through, otherwise the modal would never hide. The user actions, which change the desired state,
|
||||
// are capture, converted to onClose callback and prevented. It's up to the parent to decide whether to
|
||||
// hide the modal or not.
|
||||
if (!this.props.hidden) {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.onClose();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async onClose() {
|
||||
if (this.props.onCloseAsync) {
|
||||
await this.props.onCloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
async onButtonClick(idx) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
if (buttonSpec.onClickAsync) {
|
||||
await buttonSpec.onClickAsync(idx);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const t = props.t;
|
||||
|
||||
const buttons = [];
|
||||
for (let idx = 0; idx < this.state.buttons.length; idx++) {
|
||||
const buttonSpec = this.state.buttons[idx];
|
||||
const button = <Button key={idx} label={buttonSpec.label} className={buttonSpec.className} onClickAsync={async () => this.onButtonClick(idx)} />
|
||||
buttons.push(button);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(domElem) => { this.domModal = domElem; }}
|
||||
className={'modal fade' + (props.className ? ' ' + props.className : '')}
|
||||
tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title">{this.props.title}</h4>
|
||||
<button type="button" className="close" aria-label={t('close')} onClick={::this.onClose}><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div className="modal-body">{this.props.children}</div>
|
||||
<div className="modal-footer">
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,128 +1,128 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
export function createComponentMixin(contexts, deps, decoratorFn) {
|
||||
return {
|
||||
contexts,
|
||||
deps,
|
||||
decoratorFn
|
||||
};
|
||||
}
|
||||
|
||||
export function withComponentMixins(mixins, delegateFuns) {
|
||||
const mixinsClosure = new Set();
|
||||
for (const mixin of mixins) {
|
||||
mixinsClosure.add(mixin);
|
||||
for (const dep of mixin.deps) {
|
||||
mixinsClosure.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
const contexts = new Map();
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
for (const ctx of mixin.contexts) {
|
||||
contexts.set(ctx.propName, ctx.context);
|
||||
}
|
||||
}
|
||||
|
||||
return TargetClass => {
|
||||
const ctors = [];
|
||||
const mixinDelegateFuns = [];
|
||||
|
||||
if (delegateFuns) {
|
||||
mixinDelegateFuns.push(...delegateFuns);
|
||||
}
|
||||
|
||||
function TargetClassWithCtors(props) {
|
||||
if (!new.target) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
const self = Reflect.construct(TargetClass, [props], new.target);
|
||||
|
||||
for (const ctor of ctors) {
|
||||
ctor(self, props);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
TargetClassWithCtors.prototype = TargetClass.prototype;
|
||||
|
||||
for (const attr in TargetClass) {
|
||||
TargetClassWithCtors[attr] = TargetClass[attr];
|
||||
}
|
||||
|
||||
|
||||
class ComponentMixinsInner extends React.Component {
|
||||
render() {
|
||||
const props = {
|
||||
...this.props,
|
||||
ref: this.props._decoratorInnerInstanceRefFn
|
||||
};
|
||||
delete props._decoratorInnerInstanceRefFn;
|
||||
|
||||
return (
|
||||
<TargetClassWithCtors {...props}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let DecoratedInner = ComponentMixinsInner;
|
||||
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors);
|
||||
|
||||
if (res.cls) {
|
||||
DecoratedInner = res.cls;
|
||||
}
|
||||
|
||||
if (res.ctor) {
|
||||
ctors.push(res.ctor);
|
||||
}
|
||||
|
||||
if (res.delegateFuns) {
|
||||
mixinDelegateFuns.push(...res.delegateFuns);
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentMixinsOuter extends React.Component {
|
||||
render() {
|
||||
let innerFn = parentProps => {
|
||||
const props = {
|
||||
...parentProps,
|
||||
_decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node
|
||||
};
|
||||
|
||||
return <DecoratedInner {...props}/>
|
||||
}
|
||||
|
||||
for (const [propName, Context] of contexts.entries()) {
|
||||
const existingInnerFn = innerFn;
|
||||
innerFn = parentProps => (
|
||||
<Context.Consumer>
|
||||
{
|
||||
value => existingInnerFn({
|
||||
...parentProps,
|
||||
[propName]: value
|
||||
})
|
||||
}
|
||||
</Context.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
return innerFn(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
for (const fun of mixinDelegateFuns) {
|
||||
ComponentMixinsOuter.prototype[fun] = function (...args) {
|
||||
return this._decoratorInnerInstance[fun](...args);
|
||||
}
|
||||
}
|
||||
|
||||
return ComponentMixinsOuter;
|
||||
};
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
export function createComponentMixin(contexts, deps, decoratorFn) {
|
||||
return {
|
||||
contexts,
|
||||
deps,
|
||||
decoratorFn
|
||||
};
|
||||
}
|
||||
|
||||
export function withComponentMixins(mixins, delegateFuns) {
|
||||
const mixinsClosure = new Set();
|
||||
for (const mixin of mixins) {
|
||||
mixinsClosure.add(mixin);
|
||||
for (const dep of mixin.deps) {
|
||||
mixinsClosure.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
const contexts = new Map();
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
for (const ctx of mixin.contexts) {
|
||||
contexts.set(ctx.propName, ctx.context);
|
||||
}
|
||||
}
|
||||
|
||||
return TargetClass => {
|
||||
const ctors = [];
|
||||
const mixinDelegateFuns = [];
|
||||
|
||||
if (delegateFuns) {
|
||||
mixinDelegateFuns.push(...delegateFuns);
|
||||
}
|
||||
|
||||
function TargetClassWithCtors(props) {
|
||||
if (!new.target) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
const self = Reflect.construct(TargetClass, [props], new.target);
|
||||
|
||||
for (const ctor of ctors) {
|
||||
ctor(self, props);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
TargetClassWithCtors.prototype = TargetClass.prototype;
|
||||
|
||||
for (const attr in TargetClass) {
|
||||
TargetClassWithCtors[attr] = TargetClass[attr];
|
||||
}
|
||||
|
||||
|
||||
class ComponentMixinsInner extends React.Component {
|
||||
render() {
|
||||
const props = {
|
||||
...this.props,
|
||||
ref: this.props._decoratorInnerInstanceRefFn
|
||||
};
|
||||
delete props._decoratorInnerInstanceRefFn;
|
||||
|
||||
return (
|
||||
<TargetClassWithCtors {...props}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let DecoratedInner = ComponentMixinsInner;
|
||||
|
||||
for (const mixin of mixinsClosure.values()) {
|
||||
const res = mixin.decoratorFn(DecoratedInner, TargetClassWithCtors);
|
||||
|
||||
if (res.cls) {
|
||||
DecoratedInner = res.cls;
|
||||
}
|
||||
|
||||
if (res.ctor) {
|
||||
ctors.push(res.ctor);
|
||||
}
|
||||
|
||||
if (res.delegateFuns) {
|
||||
mixinDelegateFuns.push(...res.delegateFuns);
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentMixinsOuter extends React.Component {
|
||||
render() {
|
||||
let innerFn = parentProps => {
|
||||
const props = {
|
||||
...parentProps,
|
||||
_decoratorInnerInstanceRefFn: node => this._decoratorInnerInstance = node
|
||||
};
|
||||
|
||||
return <DecoratedInner {...props}/>
|
||||
}
|
||||
|
||||
for (const [propName, Context] of contexts.entries()) {
|
||||
const existingInnerFn = innerFn;
|
||||
innerFn = parentProps => (
|
||||
<Context.Consumer>
|
||||
{
|
||||
value => existingInnerFn({
|
||||
...parentProps,
|
||||
[propName]: value
|
||||
})
|
||||
}
|
||||
</Context.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
return innerFn(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
for (const fun of mixinDelegateFuns) {
|
||||
ComponentMixinsOuter.prototype[fun] = function (...args) {
|
||||
return this._decoratorInnerInstance[fun](...args);
|
||||
}
|
||||
}
|
||||
|
||||
return ComponentMixinsOuter;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +1,76 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
function handleError(that, error) {
|
||||
let errorHandled;
|
||||
if (that.errorHandler) {
|
||||
errorHandled = that.errorHandler(error);
|
||||
}
|
||||
|
||||
if (!errorHandled && that.props.parentErrorHandler) {
|
||||
errorHandled = handleError(that.props.parentErrorHandler, error);
|
||||
}
|
||||
|
||||
if (!errorHandled) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return errorHandled;
|
||||
}
|
||||
|
||||
export const ParentErrorHandlerContext = React.createContext(null);
|
||||
export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => {
|
||||
/* Example of use:
|
||||
this.getFormValuesFromURL(....).catch(error => this.handleError(error));
|
||||
|
||||
It's equivalent to:
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(...);
|
||||
}
|
||||
*/
|
||||
|
||||
const originalRender = InnerClass.prototype.render;
|
||||
|
||||
InnerClass.prototype.render = function() {
|
||||
return (
|
||||
<ParentErrorHandlerContext.Provider value={this}>
|
||||
{originalRender.apply(this)}
|
||||
</ParentErrorHandlerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
InnerClass.prototype.handleError = function(error) {
|
||||
handleError(this, error);
|
||||
};
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
export function withAsyncErrorHandler(target, name, descriptor) {
|
||||
let fn = descriptor.value;
|
||||
|
||||
descriptor.value = async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(this, error);
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
export function wrapWithAsyncErrorHandler(self, fn) {
|
||||
return async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(self, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
function handleError(that, error) {
|
||||
let errorHandled;
|
||||
if (that.errorHandler) {
|
||||
errorHandled = that.errorHandler(error);
|
||||
}
|
||||
|
||||
if (!errorHandled && that.props.parentErrorHandler) {
|
||||
errorHandled = handleError(that.props.parentErrorHandler, error);
|
||||
}
|
||||
|
||||
if (!errorHandled) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return errorHandled;
|
||||
}
|
||||
|
||||
export const ParentErrorHandlerContext = React.createContext(null);
|
||||
export const withErrorHandling = createComponentMixin([{context: ParentErrorHandlerContext, propName: 'parentErrorHandler'}], [], (TargetClass, InnerClass) => {
|
||||
/* Example of use:
|
||||
this.getFormValuesFromURL(....).catch(error => this.handleError(error));
|
||||
|
||||
It's equivalent to:
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(...);
|
||||
}
|
||||
*/
|
||||
|
||||
const originalRender = InnerClass.prototype.render;
|
||||
|
||||
InnerClass.prototype.render = function() {
|
||||
return (
|
||||
<ParentErrorHandlerContext.Provider value={this}>
|
||||
{originalRender.apply(this)}
|
||||
</ParentErrorHandlerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
InnerClass.prototype.handleError = function(error) {
|
||||
handleError(this, error);
|
||||
};
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
export function withAsyncErrorHandler(target, name, descriptor) {
|
||||
let fn = descriptor.value;
|
||||
|
||||
descriptor.value = async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(this, error);
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
export function wrapWithAsyncErrorHandler(self, fn) {
|
||||
return async function () {
|
||||
try {
|
||||
await fn.apply(this, arguments)
|
||||
} catch (error) {
|
||||
handleError(self, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
import ellipsize from "ellipsize";
|
||||
|
||||
|
||||
export function ellipsizeBreadcrumbLabel(label) {
|
||||
return ellipsize(label, 40)
|
||||
'use strict';
|
||||
|
||||
import ellipsize from "ellipsize";
|
||||
|
||||
|
||||
export function ellipsizeBreadcrumbLabel(label) {
|
||||
return ellipsize(label, 40)
|
||||
}
|
|
@ -1,75 +1,75 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import i18n
|
||||
from 'i18next';
|
||||
import {withNamespaces} from "react-i18next";
|
||||
import LanguageDetector
|
||||
from 'i18next-browser-languagedetector';
|
||||
import mailtrainConfig
|
||||
from 'mailtrainConfig';
|
||||
|
||||
import {convertToFake, getLang} from '../../../shared/langs';
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
import lang_en_US_common from "../../../locales/en-US/common";
|
||||
import lang_es_ES_common from "../../../locales/es-ES/common";
|
||||
|
||||
|
||||
const resourcesCommon = {
|
||||
'en-US': lang_en_US_common,
|
||||
'es-ES': lang_es_ES_common,
|
||||
'fk-FK': convertToFake(lang_en_US_common)
|
||||
};
|
||||
|
||||
const resources = {};
|
||||
for (const lng of mailtrainConfig.enabledLanguages) {
|
||||
const langDesc = getLang(lng);
|
||||
resources[langDesc.longCode] = {
|
||||
common: resourcesCommon[langDesc.longCode]
|
||||
};
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
resources,
|
||||
|
||||
fallbackLng: mailtrainConfig.defaultLanguage,
|
||||
defaultNS: 'common',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react
|
||||
},
|
||||
|
||||
react: {
|
||||
wait: true
|
||||
},
|
||||
|
||||
detection: {
|
||||
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
|
||||
lookupQuerystring: 'locale',
|
||||
lookupCookie: 'i18nextLng',
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
caches: ['localStorage', 'cookie']
|
||||
},
|
||||
|
||||
whitelist: mailtrainConfig.enabledLanguages,
|
||||
load: 'currentOnly',
|
||||
|
||||
debug: false
|
||||
});
|
||||
|
||||
|
||||
export default i18n;
|
||||
|
||||
|
||||
export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => {
|
||||
return {
|
||||
cls: withNamespaces()(TargetClass)
|
||||
};
|
||||
});
|
||||
|
||||
export function tMark(key) {
|
||||
return key;
|
||||
}
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import i18n
|
||||
from 'i18next';
|
||||
import {withNamespaces} from "react-i18next";
|
||||
import LanguageDetector
|
||||
from 'i18next-browser-languagedetector';
|
||||
import mailtrainConfig
|
||||
from 'mailtrainConfig';
|
||||
|
||||
import {convertToFake, getLang} from '../../../shared/langs';
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
import lang_en_US_common from "../../../locales/en-US/common";
|
||||
import lang_es_ES_common from "../../../locales/es-ES/common";
|
||||
|
||||
|
||||
const resourcesCommon = {
|
||||
'en-US': lang_en_US_common,
|
||||
'es-ES': lang_es_ES_common,
|
||||
'fk-FK': convertToFake(lang_en_US_common)
|
||||
};
|
||||
|
||||
const resources = {};
|
||||
for (const lng of mailtrainConfig.enabledLanguages) {
|
||||
const langDesc = getLang(lng);
|
||||
resources[langDesc.longCode] = {
|
||||
common: resourcesCommon[langDesc.longCode]
|
||||
};
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
resources,
|
||||
|
||||
fallbackLng: mailtrainConfig.defaultLanguage,
|
||||
defaultNS: 'common',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react
|
||||
},
|
||||
|
||||
react: {
|
||||
wait: true
|
||||
},
|
||||
|
||||
detection: {
|
||||
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
|
||||
lookupQuerystring: 'locale',
|
||||
lookupCookie: 'i18nextLng',
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
caches: ['localStorage', 'cookie']
|
||||
},
|
||||
|
||||
whitelist: mailtrainConfig.enabledLanguages,
|
||||
load: 'currentOnly',
|
||||
|
||||
debug: false
|
||||
});
|
||||
|
||||
|
||||
export default i18n;
|
||||
|
||||
|
||||
export const withTranslation = createComponentMixin([], [], (TargetClass, InnerClass) => {
|
||||
return {
|
||||
cls: withNamespaces()(TargetClass)
|
||||
};
|
||||
});
|
||||
|
||||
export function tMark(key) {
|
||||
return key;
|
||||
}
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import {TreeTableSelect} from './form';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation
|
||||
])
|
||||
class NamespaceSelect extends Component {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<TreeTableSelect id="namespace" label={t('namespace')} dataUrl="rest/namespaces-tree"/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNamespace(t, state) {
|
||||
if (!state.getIn(['namespace', 'value'])) {
|
||||
state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
|
||||
} else {
|
||||
state.setIn(['namespace', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
NamespaceSelect,
|
||||
validateNamespace
|
||||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {withTranslation} from './i18n';
|
||||
import {TreeTableSelect} from './form';
|
||||
import {withComponentMixins} from "./decorator-helpers";
|
||||
|
||||
|
||||
@withComponentMixins([
|
||||
withTranslation
|
||||
])
|
||||
class NamespaceSelect extends Component {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<TreeTableSelect id="namespace" label={t('namespace')} dataUrl="rest/namespaces-tree"/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNamespace(t, state) {
|
||||
if (!state.getIn(['namespace', 'value'])) {
|
||||
state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
|
||||
} else {
|
||||
state.setIn(['namespace', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
NamespaceSelect,
|
||||
validateNamespace
|
||||
};
|
|
@ -1,146 +1,146 @@
|
|||
'use strict';
|
||||
|
||||
import React
|
||||
from "react";
|
||||
import {withRouter} from "react-router";
|
||||
import {withErrorHandling} from "./error-handling";
|
||||
import axios
|
||||
from "../lib/axios";
|
||||
import {getUrl} from "./urls";
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
export function needsResolve(route, nextRoute, match, nextMatch) {
|
||||
const resolve = route.resolve;
|
||||
const nextResolve = nextRoute.resolve;
|
||||
|
||||
// This compares whether two objects have the same content and returns TRUE if they don't
|
||||
if (Object.keys(resolve).length === Object.keys(nextResolve).length) {
|
||||
for (const key in nextResolve) {
|
||||
if (!(key in resolve) ||
|
||||
resolve[key](match.params) !== nextResolve[key](nextMatch.params)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolve(route, match) {
|
||||
const keys = Object.keys(route.resolve);
|
||||
|
||||
const promises = keys.map(key => {
|
||||
const url = route.resolve[key](match.params);
|
||||
if (url) {
|
||||
return axios.get(getUrl(url));
|
||||
} else {
|
||||
return Promise.resolve({data: null});
|
||||
}
|
||||
});
|
||||
const resolvedArr = await Promise.all(promises);
|
||||
|
||||
const resolved = {};
|
||||
for (let idx = 0; idx < keys.length; idx++) {
|
||||
resolved[keys[idx]] = resolvedArr[idx].data;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
|
||||
let routes = [];
|
||||
for (let routeKey in structure) {
|
||||
const entry = structure[routeKey];
|
||||
|
||||
let path = urlPrefix + routeKey;
|
||||
let pathWithParams = path;
|
||||
|
||||
if (entry.extraParams) {
|
||||
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
|
||||
}
|
||||
|
||||
let entryResolve;
|
||||
if (entry.resolve) {
|
||||
entryResolve = Object.assign({}, resolve, entry.resolve);
|
||||
} else {
|
||||
entryResolve = resolve;
|
||||
}
|
||||
|
||||
let navKeys;
|
||||
const entryNavs = [];
|
||||
if (entry.navs) {
|
||||
navKeys = Object.keys(entry.navs);
|
||||
|
||||
for (const navKey of navKeys) {
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
entryNavs.push({
|
||||
title: nav.title,
|
||||
visible: nav.visible,
|
||||
link: nav.link,
|
||||
externalLink: nav.externalLink
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const route = {
|
||||
path: (pathWithParams === '' ? '/' : pathWithParams),
|
||||
panelComponent: entry.panelComponent,
|
||||
panelRender: entry.panelRender,
|
||||
primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
|
||||
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
|
||||
title: entry.title,
|
||||
link: entry.link,
|
||||
panelInFullScreen: entry.panelInFullScreen,
|
||||
insideIframe: entry.insideIframe,
|
||||
resolve: entryResolve,
|
||||
parents,
|
||||
navs: [...navs, ...entryNavs]
|
||||
};
|
||||
|
||||
routes.push(route);
|
||||
|
||||
const childrenParents = [...parents, route];
|
||||
|
||||
if (entry.navs) {
|
||||
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
|
||||
const navKey = navKeys[navKeyIdx];
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
const childNavs = [...entryNavs];
|
||||
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
|
||||
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.children) {
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export const SectionContentContext = React.createContext(null);
|
||||
export const withPageHelpers = createComponentMixin([{context: SectionContentContext, propName: 'sectionContent'}], [withErrorHandling], (TargetClass, InnerClass) => {
|
||||
InnerClass.prototype.setFlashMessage = function(severity, text) {
|
||||
return this.props.sectionContent.setFlashMessage(severity, text);
|
||||
};
|
||||
|
||||
InnerClass.prototype.navigateTo = function(path) {
|
||||
return this.props.sectionContent.navigateTo(path);
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateBack = function() {
|
||||
return this.props.sectionContent.navigateBack();
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateToWithFlashMessage = function(path, severity, text) {
|
||||
return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
'use strict';
|
||||
|
||||
import React
|
||||
from "react";
|
||||
import {withRouter} from "react-router";
|
||||
import {withErrorHandling} from "./error-handling";
|
||||
import axios
|
||||
from "../lib/axios";
|
||||
import {getUrl} from "./urls";
|
||||
import {createComponentMixin} from "./decorator-helpers";
|
||||
|
||||
export function needsResolve(route, nextRoute, match, nextMatch) {
|
||||
const resolve = route.resolve;
|
||||
const nextResolve = nextRoute.resolve;
|
||||
|
||||
// This compares whether two objects have the same content and returns TRUE if they don't
|
||||
if (Object.keys(resolve).length === Object.keys(nextResolve).length) {
|
||||
for (const key in nextResolve) {
|
||||
if (!(key in resolve) ||
|
||||
resolve[key](match.params) !== nextResolve[key](nextMatch.params)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolve(route, match) {
|
||||
const keys = Object.keys(route.resolve);
|
||||
|
||||
const promises = keys.map(key => {
|
||||
const url = route.resolve[key](match.params);
|
||||
if (url) {
|
||||
return axios.get(getUrl(url));
|
||||
} else {
|
||||
return Promise.resolve({data: null});
|
||||
}
|
||||
});
|
||||
const resolvedArr = await Promise.all(promises);
|
||||
|
||||
const resolved = {};
|
||||
for (let idx = 0; idx < keys.length; idx++) {
|
||||
resolved[keys[idx]] = resolvedArr[idx].data;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function getRoutes(urlPrefix, resolve, parents, structure, navs, primaryMenuComponent, secondaryMenuComponent) {
|
||||
let routes = [];
|
||||
for (let routeKey in structure) {
|
||||
const entry = structure[routeKey];
|
||||
|
||||
let path = urlPrefix + routeKey;
|
||||
let pathWithParams = path;
|
||||
|
||||
if (entry.extraParams) {
|
||||
pathWithParams = pathWithParams + '/' + entry.extraParams.join('/');
|
||||
}
|
||||
|
||||
let entryResolve;
|
||||
if (entry.resolve) {
|
||||
entryResolve = Object.assign({}, resolve, entry.resolve);
|
||||
} else {
|
||||
entryResolve = resolve;
|
||||
}
|
||||
|
||||
let navKeys;
|
||||
const entryNavs = [];
|
||||
if (entry.navs) {
|
||||
navKeys = Object.keys(entry.navs);
|
||||
|
||||
for (const navKey of navKeys) {
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
entryNavs.push({
|
||||
title: nav.title,
|
||||
visible: nav.visible,
|
||||
link: nav.link,
|
||||
externalLink: nav.externalLink
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const route = {
|
||||
path: (pathWithParams === '' ? '/' : pathWithParams),
|
||||
panelComponent: entry.panelComponent,
|
||||
panelRender: entry.panelRender,
|
||||
primaryMenuComponent: entry.primaryMenuComponent || primaryMenuComponent,
|
||||
secondaryMenuComponent: entry.secondaryMenuComponent || secondaryMenuComponent,
|
||||
title: entry.title,
|
||||
link: entry.link,
|
||||
panelInFullScreen: entry.panelInFullScreen,
|
||||
insideIframe: entry.insideIframe,
|
||||
resolve: entryResolve,
|
||||
parents,
|
||||
navs: [...navs, ...entryNavs]
|
||||
};
|
||||
|
||||
routes.push(route);
|
||||
|
||||
const childrenParents = [...parents, route];
|
||||
|
||||
if (entry.navs) {
|
||||
for (let navKeyIdx = 0; navKeyIdx < navKeys.length; navKeyIdx++) {
|
||||
const navKey = navKeys[navKeyIdx];
|
||||
const nav = entry.navs[navKey];
|
||||
|
||||
const childNavs = [...entryNavs];
|
||||
childNavs[navKeyIdx] = Object.assign({}, childNavs[navKeyIdx], { active: true });
|
||||
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, { [navKey]: nav }, childNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.children) {
|
||||
routes = routes.concat(getRoutes(path + '/', entryResolve, childrenParents, entry.children, entryNavs, route.primaryMenuComponent, route.secondaryMenuComponent));
|
||||
}
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
export const SectionContentContext = React.createContext(null);
|
||||
export const withPageHelpers = createComponentMixin([{context: SectionContentContext, propName: 'sectionContent'}], [withErrorHandling], (TargetClass, InnerClass) => {
|
||||
InnerClass.prototype.setFlashMessage = function(severity, text) {
|
||||
return this.props.sectionContent.setFlashMessage(severity, text);
|
||||
};
|
||||
|
||||
InnerClass.prototype.navigateTo = function(path) {
|
||||
return this.props.sectionContent.navigateTo(path);
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateBack = function() {
|
||||
return this.props.sectionContent.navigateBack();
|
||||
}
|
||||
|
||||
InnerClass.prototype.navigateToWithFlashMessage = function(path, severity, text) {
|
||||
return this.props.sectionContent.navigateToWithFlashMessage(path, severity, text);
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
import axios from "./axios";
|
||||
|
||||
async function checkPermissions(request) {
|
||||
return await axios.post(getUrl('rest/permissions-check'), request);
|
||||
}
|
||||
|
||||
export {
|
||||
checkPermissions
|
||||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
import axios from "./axios";
|
||||
|
||||
async function checkPermissions(request) {
|
||||
return await axios.post(getUrl('rest/permissions-check'), request);
|
||||
}
|
||||
|
||||
export {
|
||||
checkPermissions
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
__webpack_public_path__ = getUrl('client/');
|
||||
'use strict';
|
||||
|
||||
import {getUrl} from "./urls";
|
||||
|
||||
__webpack_public_path__ = getUrl('client/');
|
||||
|
|
|
@ -1,90 +1,90 @@
|
|||
$navbarHeight: 34px;
|
||||
$editorNormalHeight: 800px !default;
|
||||
|
||||
.editor {
|
||||
.host {
|
||||
@if $editorNormalHeight {
|
||||
height: $editorNormalHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editorFullscreen {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
margin-top: $navbarHeight;
|
||||
|
||||
.navbar {
|
||||
margin-top: -$navbarHeight;
|
||||
}
|
||||
|
||||
.host {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #f86c6b;
|
||||
width: 100%;
|
||||
height: $navbarHeight;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbarLeft {
|
||||
.logo {
|
||||
display: inline-block;
|
||||
height: $navbarHeight;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
padding: 5px 0 5px 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
color: white;
|
||||
height: $navbarHeight;
|
||||
}
|
||||
}
|
||||
|
||||
.navbarRight {
|
||||
.btn, .btnDisabled {
|
||||
display: inline-block;
|
||||
padding: 0px 15px;
|
||||
line-height: $navbarHeight;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #c05454;
|
||||
text-decoration: none;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btnDisabled {
|
||||
cursor: default;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: #621d1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
$navbarHeight: 34px;
|
||||
$editorNormalHeight: 800px !default;
|
||||
|
||||
.editor {
|
||||
.host {
|
||||
@if $editorNormalHeight {
|
||||
height: $editorNormalHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editorFullscreen {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
margin-top: $navbarHeight;
|
||||
|
||||
.navbar {
|
||||
margin-top: -$navbarHeight;
|
||||
}
|
||||
|
||||
.host {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #f86c6b;
|
||||
width: 100%;
|
||||
height: $navbarHeight;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbarLeft {
|
||||
.logo {
|
||||
display: inline-block;
|
||||
height: $navbarHeight;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
padding: 5px 0 5px 10px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
color: white;
|
||||
height: $navbarHeight;
|
||||
}
|
||||
}
|
||||
|
||||
.navbarRight {
|
||||
.btn, .btnDisabled {
|
||||
display: inline-block;
|
||||
padding: 0px 15px;
|
||||
line-height: $navbarHeight;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #c05454;
|
||||
text-decoration: none;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.btnDisabled {
|
||||
cursor: default;
|
||||
|
||||
&, &:not([href]):not([tabindex]) { // This is to override reboot.scss in bootstrap
|
||||
color: #621d1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
$editorNormalHeight: false;
|
||||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
$editorNormalHeight: false;
|
||||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,35 +1,35 @@
|
|||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
}
|
||||
|
||||
.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aceEditorWithPreview {
|
||||
border-right: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.aceEditorWithoutPreview {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview {
|
||||
border-left: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px none;
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
@import "sandbox-common";
|
||||
|
||||
.sandbox {
|
||||
}
|
||||
|
||||
.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aceEditorWithPreview {
|
||||
border-right: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.aceEditorWithoutPreview {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview {
|
||||
border-left: #e8e8e8 solid 2px;
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px none;
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
@import "sandbox-common";
|
||||
|
||||
:global .grapesjs-body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
:global .gjs-editor-cont {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:global .gjs-devices-c .gjs-devices {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
:global .gjs-pn-devices-c, :global .gjs-pn-views {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@import "sandbox-common";
|
||||
|
||||
:global .grapesjs-body {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
:global .gjs-editor-cont {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:global .gjs-devices-c .gjs-devices {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
:global .gjs-pn-devices-c, :global .gjs-pn-views {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@import "sandbox-common";
|
||||
|
||||
:global .mo-standalone {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
@import "sandbox-common";
|
||||
|
||||
:global .mo-standalone {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
|
|
@ -1,92 +1,92 @@
|
|||
@import "../scss/variables.scss";
|
||||
|
||||
:global {
|
||||
|
||||
.mt-treetable-container .fancytree-container {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-expander {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active:hover span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-expander,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container>tbody>tr.fancytree-active>td {
|
||||
outline: 0px none;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mt-treetable-container {
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.mt-treetable-container>table.fancytree-ext-table {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
|
||||
border-top: 0px none;
|
||||
}
|
||||
|
||||
.mt-treetable-container .mt-treetable-title {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-group .mt-treetable-container {
|
||||
border: $input-border-width solid $input-border-color;
|
||||
border-radius: $input-border-radius;
|
||||
padding-top: $input-padding-y;
|
||||
padding-bottom: $input-padding-y;
|
||||
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-valid {
|
||||
border-color: $form-feedback-valid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-invalid {
|
||||
border-color: $form-feedback-invalid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.mt-treetable-container .table td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
@import "../scss/variables.scss";
|
||||
|
||||
:global {
|
||||
|
||||
.mt-treetable-container .fancytree-container {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-expander {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active:hover>td,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active:hover span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node span.fancytree-title {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable>table.fancytree-ext-table.fancytree-container>tbody>tr.fancytree-active>td span.fancytree-expander,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container span.fancytree-node.fancytree-active span.fancytree-title,
|
||||
.mt-treetable-container.mt-treetable-inactivable .fancytree-container>tbody>tr.fancytree-active>td {
|
||||
outline: 0px none;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.mt-treetable-container span.fancytree-node span.fancytree-expander:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mt-treetable-container {
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.mt-treetable-container>table.fancytree-ext-table {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.mt-treetable-container.mt-treetable-noheader>.table>tbody>tr>td {
|
||||
border-top: 0px none;
|
||||
}
|
||||
|
||||
.mt-treetable-container .mt-treetable-title {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-group .mt-treetable-container {
|
||||
border: $input-border-width solid $input-border-color;
|
||||
border-radius: $input-border-radius;
|
||||
padding-top: $input-padding-y;
|
||||
padding-bottom: $input-padding-y;
|
||||
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-valid {
|
||||
border-color: $form-feedback-valid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container.is-invalid {
|
||||
border-color: $form-feedback-invalid-color;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.mt-treetable-container .table td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,60 +1,60 @@
|
|||
'use strict';
|
||||
|
||||
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
|
||||
import {AppType} from '../../../shared/app';
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
import i18n from './i18n';
|
||||
|
||||
let restrictedAccessToken = anonymousRestrictedAccessToken;
|
||||
|
||||
function setRestrictedAccessToken(token) {
|
||||
restrictedAccessToken = token;
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return mailtrainConfig.trustedUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path, customRestrictedAccessToken) {
|
||||
const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken;
|
||||
return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || '');
|
||||
}
|
||||
|
||||
function getPublicUrl(path, opts) {
|
||||
const url = new URL(path || '', mailtrainConfig.publicUrlBase);
|
||||
|
||||
if (opts && opts.withLocale) {
|
||||
url.searchParams.append('locale', i18n.language);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getUrl(path) {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return getTrustedUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return getSandboxUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return getPublicUrl(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseDir() {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return mailtrainConfig.trustedUrlBaseDir;
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return mailtrainConfig.sandboxUrlBaseDir + restrictedAccessToken;
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return mailtrainConfig.publicUrlBaseDir;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getPublicUrl,
|
||||
getUrl,
|
||||
getBaseDir,
|
||||
setRestrictedAccessToken
|
||||
'use strict';
|
||||
|
||||
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
|
||||
import {AppType} from '../../../shared/app';
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
import i18n from './i18n';
|
||||
|
||||
let restrictedAccessToken = anonymousRestrictedAccessToken;
|
||||
|
||||
function setRestrictedAccessToken(token) {
|
||||
restrictedAccessToken = token;
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return mailtrainConfig.trustedUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path, customRestrictedAccessToken) {
|
||||
const localRestrictedAccessToken = customRestrictedAccessToken || restrictedAccessToken;
|
||||
return mailtrainConfig.sandboxUrlBase + localRestrictedAccessToken + '/' + (path || '');
|
||||
}
|
||||
|
||||
function getPublicUrl(path, opts) {
|
||||
const url = new URL(path || '', mailtrainConfig.publicUrlBase);
|
||||
|
||||
if (opts && opts.withLocale) {
|
||||
url.searchParams.append('locale', i18n.language);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getUrl(path) {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return getTrustedUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return getSandboxUrl(path);
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return getPublicUrl(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseDir() {
|
||||
if (mailtrainConfig.appType === AppType.TRUSTED) {
|
||||
return mailtrainConfig.trustedUrlBaseDir;
|
||||
} else if (mailtrainConfig.appType === AppType.SANDBOXED) {
|
||||
return mailtrainConfig.sandboxUrlBaseDir + restrictedAccessToken;
|
||||
} else if (mailtrainConfig.appType === AppType.PUBLIC) {
|
||||
return mailtrainConfig.publicUrlBaseDir;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getPublicUrl,
|
||||
getUrl,
|
||||
getBaseDir,
|
||||
setRestrictedAccessToken
|
||||
}
|
|
@ -1,55 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {Fieldset, InputField} from "../../lib/form";
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const fieldTypes = {
|
||||
text: {
|
||||
label: t('text'),
|
||||
},
|
||||
website: {
|
||||
label: t('website'),
|
||||
},
|
||||
longtext: {
|
||||
label: t('multilineText'),
|
||||
},
|
||||
gpg: {
|
||||
label: t('gpgPublicKey'),
|
||||
},
|
||||
number: {
|
||||
label: t('number'),
|
||||
},
|
||||
'checkbox-grouped': {
|
||||
label: t('checkboxesFromOptionFields'),
|
||||
},
|
||||
'radio-grouped': {
|
||||
label: t('radioButtonsFromOptionFields')
|
||||
},
|
||||
'dropdown-grouped': {
|
||||
label: t('dropDownFromOptionFields')
|
||||
},
|
||||
'radio-enum': {
|
||||
label: t('radioButtonsEnumerated')
|
||||
},
|
||||
'dropdown-enum': {
|
||||
label: t('dropDownEnumerated')
|
||||
},
|
||||
'date': {
|
||||
label: t('date')
|
||||
},
|
||||
'birthday': {
|
||||
label: t('birthday')
|
||||
},
|
||||
json: {
|
||||
label: t('jsonValueForCustomRendering')
|
||||
},
|
||||
option: {
|
||||
label: t('option')
|
||||
}
|
||||
};
|
||||
|
||||
return fieldTypes;
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {Fieldset, InputField} from "../../lib/form";
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const fieldTypes = {
|
||||
text: {
|
||||
label: t('text'),
|
||||
},
|
||||
website: {
|
||||
label: t('website'),
|
||||
},
|
||||
longtext: {
|
||||
label: t('multilineText'),
|
||||
},
|
||||
gpg: {
|
||||
label: t('gpgPublicKey'),
|
||||
},
|
||||
number: {
|
||||
label: t('number'),
|
||||
},
|
||||
'checkbox-grouped': {
|
||||
label: t('checkboxesFromOptionFields'),
|
||||
},
|
||||
'radio-grouped': {
|
||||
label: t('radioButtonsFromOptionFields')
|
||||
},
|
||||
'dropdown-grouped': {
|
||||
label: t('dropDownFromOptionFields')
|
||||
},
|
||||
'radio-enum': {
|
||||
label: t('radioButtonsEnumerated')
|
||||
},
|
||||
'dropdown-enum': {
|
||||
label: t('dropDownEnumerated')
|
||||
},
|
||||
'date': {
|
||||
label: t('date')
|
||||
},
|
||||
'birthday': {
|
||||
label: t('birthday')
|
||||
},
|
||||
json: {
|
||||
label: t('jsonValueForCustomRendering')
|
||||
},
|
||||
option: {
|
||||
label: t('option')
|
||||
}
|
||||
};
|
||||
|
||||
return fieldTypes;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
$editorNormalHeight: 400px;
|
||||
@import "../../lib/sandbox-common";
|
||||
|
||||
.editor {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.host {
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
$editorNormalHeight: 400px;
|
||||
@import "../../lib/sandbox-common";
|
||||
|
||||
.editor {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.host {
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {ImportSource, MappingType, ImportStatus, RunStatus} from '../../../../shared/imports';
|
||||
|
||||
export function getImportLabels(t) {
|
||||
|
||||
const importSourceLabels = {
|
||||
[ImportSource.CSV_FILE]: t('csvFile'),
|
||||
[ImportSource.LIST]: t('list'),
|
||||
};
|
||||
|
||||
const importStatusLabels = {
|
||||
[ImportStatus.PREP_SCHEDULED]: t('created'),
|
||||
[ImportStatus.PREP_RUNNING]: t('preparing'),
|
||||
[ImportStatus.PREP_STOPPING]: t('stopping'),
|
||||
[ImportStatus.PREP_FINISHED]: t('ready'),
|
||||
[ImportStatus.PREP_FAILED]: t('preparationFailed'),
|
||||
[ImportStatus.RUN_SCHEDULED]: t('scheduled'),
|
||||
[ImportStatus.RUN_RUNNING]: t('running'),
|
||||
[ImportStatus.RUN_STOPPING]: t('stopping'),
|
||||
[ImportStatus.RUN_FINISHED]: t('finished'),
|
||||
[ImportStatus.RUN_FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const runStatusLabels = {
|
||||
[RunStatus.SCHEDULED]: t('starting'),
|
||||
[RunStatus.RUNNING]: t('running'),
|
||||
[RunStatus.STOPPING]: t('stopping'),
|
||||
[RunStatus.FINISHED]: t('finished'),
|
||||
[RunStatus.FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const mappingTypeLabels = {
|
||||
[MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
|
||||
[MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
|
||||
}
|
||||
|
||||
return {
|
||||
importStatusLabels,
|
||||
mappingTypeLabels,
|
||||
importSourceLabels,
|
||||
runStatusLabels
|
||||
};
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {ImportSource, MappingType, ImportStatus, RunStatus} from '../../../../shared/imports';
|
||||
|
||||
export function getImportLabels(t) {
|
||||
|
||||
const importSourceLabels = {
|
||||
[ImportSource.CSV_FILE]: t('csvFile'),
|
||||
[ImportSource.LIST]: t('list'),
|
||||
};
|
||||
|
||||
const importStatusLabels = {
|
||||
[ImportStatus.PREP_SCHEDULED]: t('created'),
|
||||
[ImportStatus.PREP_RUNNING]: t('preparing'),
|
||||
[ImportStatus.PREP_STOPPING]: t('stopping'),
|
||||
[ImportStatus.PREP_FINISHED]: t('ready'),
|
||||
[ImportStatus.PREP_FAILED]: t('preparationFailed'),
|
||||
[ImportStatus.RUN_SCHEDULED]: t('scheduled'),
|
||||
[ImportStatus.RUN_RUNNING]: t('running'),
|
||||
[ImportStatus.RUN_STOPPING]: t('stopping'),
|
||||
[ImportStatus.RUN_FINISHED]: t('finished'),
|
||||
[ImportStatus.RUN_FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const runStatusLabels = {
|
||||
[RunStatus.SCHEDULED]: t('starting'),
|
||||
[RunStatus.RUNNING]: t('running'),
|
||||
[RunStatus.STOPPING]: t('stopping'),
|
||||
[RunStatus.FINISHED]: t('finished'),
|
||||
[RunStatus.FAILED]: t('failed')
|
||||
};
|
||||
|
||||
const mappingTypeLabels = {
|
||||
[MappingType.BASIC_SUBSCRIBE]: t('basicImportOfSubscribers'),
|
||||
[MappingType.BASIC_UNSUBSCRIBE]: t('unsubscribeEmails'),
|
||||
}
|
||||
|
||||
return {
|
||||
importStatusLabels,
|
||||
mappingTypeLabels,
|
||||
importSourceLabels,
|
||||
runStatusLabels
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,152 +1,152 @@
|
|||
$desktopMinWidth: 768px;
|
||||
|
||||
$mobileLeftPaneResidualWidth: 0px;
|
||||
$mobileAnimationStartPosition: 100px;
|
||||
|
||||
$desktopLeftPaneResidualWidth: 200px;
|
||||
$desktopAnimationStartPosition: 300px;
|
||||
|
||||
@mixin optionsHidden {
|
||||
transform: translateX($mobileAnimationStartPosition);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopAnimationStartPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin optionsVisible {
|
||||
transform: translateX($mobileLeftPaneResidualWidth);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopLeftPaneResidualWidth);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ruleActionLink {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.rulePane {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.leftPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
|
||||
.leftPaneInner {
|
||||
.ruleTree {
|
||||
background: #fbfbfb;
|
||||
border: #cfcfcf 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px 0px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
// Without this, the placeholders when rearranging the tree are not shown
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leftPaneOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
width: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
width: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('./divider.png') repeat-y;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
padding-left: 50px;
|
||||
z-index: 1;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.paneDividerSolidBackground {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.rightPaneInner {
|
||||
margin-right: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
margin-right: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
|
||||
.ruleOptions {
|
||||
margin-left: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsVisible {
|
||||
.leftPaneOverlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsHidden {
|
||||
.paneDivider {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
$desktopMinWidth: 768px;
|
||||
|
||||
$mobileLeftPaneResidualWidth: 0px;
|
||||
$mobileAnimationStartPosition: 100px;
|
||||
|
||||
$desktopLeftPaneResidualWidth: 200px;
|
||||
$desktopAnimationStartPosition: 300px;
|
||||
|
||||
@mixin optionsHidden {
|
||||
transform: translateX($mobileAnimationStartPosition);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopAnimationStartPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin optionsVisible {
|
||||
transform: translateX($mobileLeftPaneResidualWidth);
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
transform: translateX($desktopLeftPaneResidualWidth);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ruleActionLink {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.rulePane {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.leftPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
|
||||
.leftPaneInner {
|
||||
.ruleTree {
|
||||
background: #fbfbfb;
|
||||
border: #cfcfcf 1px solid;
|
||||
border-radius: 4px;
|
||||
padding: 10px 0px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
// Without this, the placeholders when rearranging the tree are not shown
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.leftPaneOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
|
||||
width: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
width: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('./divider.png') repeat-y;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
padding-left: 50px;
|
||||
z-index: 1;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.paneDividerSolidBackground {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
|
||||
@include optionsHidden;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
||||
.rightPaneInner {
|
||||
margin-right: $mobileLeftPaneResidualWidth;
|
||||
@media (min-width: $desktopMinWidth) {
|
||||
margin-right: $desktopLeftPaneResidualWidth;
|
||||
}
|
||||
|
||||
.ruleOptions {
|
||||
margin-left: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsVisible {
|
||||
.leftPaneOverlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paneDivider {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: transform 300ms ease-out, opacity 100ms ease-out;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
@include optionsVisible;
|
||||
}
|
||||
}
|
||||
|
||||
&.ruleOptionsHidden {
|
||||
.paneDivider {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
|
||||
.rightPane {
|
||||
transition: visibility 0s linear 300ms, transform 300ms ease-in, opacity 100ms ease-in 200ms;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,459 +1,459 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {DatePicker, Dropdown, InputField} from "../../lib/form";
|
||||
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../../shared/date';
|
||||
import { tMark } from "../../lib/i18n";
|
||||
|
||||
export function getRuleHelpers(t, fields) {
|
||||
|
||||
const ruleHelpers = {};
|
||||
|
||||
ruleHelpers.compositeRuleTypes = {
|
||||
all: {
|
||||
dropdownLabel: t('allRulesMustMatch'),
|
||||
treeLabel: rule => t('allRulesMustMatch')
|
||||
},
|
||||
some: {
|
||||
dropdownLabel: t('atLeastOneRuleMustMatch'),
|
||||
treeLabel: rule => t('atLeastOneRuleMustMatch')
|
||||
},
|
||||
none: {
|
||||
dropdownLabel: t('noRuleMayMatch'),
|
||||
treeLabel: rule => t('noRuleMayMatch')
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes = {};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.text = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('alphabeticallyBefore'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('alphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('alphabeticallyAfter'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('alphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.website = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.number = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('lessThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('lessThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanOrEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('greaterThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('greaterThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanOrEqual', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
// FXIME - the localization here is still wrong
|
||||
function getRelativeDateTreeLabel(rule, variants) {
|
||||
if (rule.value === 0) {
|
||||
return t(variants[0], {colName: ruleHelpers.getColumnName(rule.column)})
|
||||
} else if (rule.value > 0) {
|
||||
return t(variants[1], {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
|
||||
} else {
|
||||
return t(variants[2], {colName: ruleHelpers.getColumnName(rule.column), value: -rule.value});
|
||||
}
|
||||
}
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.date = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
eqTodayPlusDays: {
|
||||
dropdownLabel: t('onXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsTheCurrentDate'), tMark('dateInColumnColNameIsTheValuethDayAfter'), tMark('dateInColumnColNameIsTheValuethDayBefore')]),
|
||||
},
|
||||
ltTodayPlusDays: {
|
||||
dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeTheCurrent'), tMark('dateInColumnColNameIsBeforeTheValuethDay'), tMark('dateInColumnColNameIsBeforeTheValuethDay-1')]),
|
||||
},
|
||||
leTodayPlusDays: {
|
||||
dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeOrOnThe'), tMark('dateInColumnColNameIsBeforeOrOnThe-1'), tMark('dateInColumnColNameIsBeforeOrOnThe-2')]),
|
||||
},
|
||||
gtTodayPlusDays: {
|
||||
dropdownLabel: t('afterXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterTheCurrentDate'), tMark('dateInColumnColNameIsAfterTheValuethDay'), tMark('dateInColumnColNameIsAfterTheValuethDay-1')]),
|
||||
},
|
||||
geTodayPlusDays: {
|
||||
dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterOrOnTheCurrent'), tMark('dateInColumnColNameIsAfterOrOnTheValueth'), tMark('dateInColumnColNameIsAfterOrOnTheValueth-1')]),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.birthday = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.option = {
|
||||
isTrue: {
|
||||
dropdownLabel: t('isSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
},
|
||||
isFalse: {
|
||||
dropdownLabel: t('isNotSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsNotSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
|
||||
eq: {
|
||||
dropdownLabel: t('keyEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIsEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('keyMatchWithSqlLike'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('keyMatchWithRegularExpressions'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('keyAlphabeticallyBefore'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('keyAlphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfter'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const stringValueSettings = allowEmpty => ({
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = getter('value');
|
||||
},
|
||||
validate: state => {
|
||||
if (!allowEmpty && !state.getIn(['value', 'value'])) {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const numberValueSettings = {
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value.toString()
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseInt(getter('value'));
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['value', 'value']).trim();
|
||||
if (value === '') {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['value', 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const birthdayValueSettings = {
|
||||
getForm: () => <DatePicker id="birthday" label={t('date')} birthday />,
|
||||
getFormData: rule => ({
|
||||
birthday: formatBirthday(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseBirthday(DateFormat.INTL, getter('birthday')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['birthday', 'value']);
|
||||
const date = parseBirthday(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['birthday', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['birthday', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['birthday', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateValueSettings = {
|
||||
getForm: () => <DatePicker id="date" label={t('date')} />,
|
||||
getFormData: rule => ({
|
||||
date: formatDate(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseDate(DateFormat.INTL, getter('date')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['date', 'value']);
|
||||
const date = parseDate(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['date', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['date', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateRelativeValueSettings = {
|
||||
getForm: () =>
|
||||
<div>
|
||||
<InputField id="daysValue" label={t('numberOfDays')}/>
|
||||
<Dropdown id="direction" label={t('beforeAfter')} options={[
|
||||
{ key: 'before', label: t('beforeCurrentDate') },
|
||||
{ key: 'after', label: t('afterCurrentDate') }
|
||||
]}/>
|
||||
</div>,
|
||||
getFormData: rule => ({
|
||||
daysValue: Math.abs(rule.value).toString(),
|
||||
direction: rule.value >= 0 ? 'after' : 'before'
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
const direction = getter('direction');
|
||||
rule.value = parseInt(getter('daysValue')) * (direction === 'before' ? -1 : 1);
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['daysValue', 'value']);
|
||||
if (!value) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['daysValue', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const optionValueSettings = {
|
||||
getForm: () => null,
|
||||
getFormData: rule => ({}),
|
||||
assignRuleSettings: (rule, getter) => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
|
||||
function assignSettingsToRuleTypes(ruleTypes, keys, settings) {
|
||||
for (const key of keys) {
|
||||
Object.assign(ruleTypes[key], settings);
|
||||
}
|
||||
}
|
||||
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, ['eq', 'lt', 'le', 'gt', 'ge'], numberValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'], dateRelativeValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, ['isTrue', 'isFalse'], optionValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
|
||||
ruleHelpers.primitiveRuleTypesFormDataDefaults = {
|
||||
value: '',
|
||||
date: '',
|
||||
daysValue: '',
|
||||
birthday: '',
|
||||
direction: 'before'
|
||||
};
|
||||
|
||||
|
||||
|
||||
ruleHelpers.getCompositeRuleTypeOptions = () => {
|
||||
const order = ['all', 'some', 'none'];
|
||||
return order.map(key => ({ key, label: ruleHelpers.compositeRuleTypes[key].dropdownLabel }));
|
||||
};
|
||||
|
||||
ruleHelpers.getPrimitiveRuleTypeOptions = columnType => {
|
||||
const order = {
|
||||
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
website: ['eq', 'like', 're'],
|
||||
number: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
|
||||
option: ['isTrue', 'isFalse'],
|
||||
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
|
||||
};
|
||||
|
||||
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
|
||||
};
|
||||
|
||||
const predefColumns = [
|
||||
{
|
||||
column: 'email',
|
||||
name: t('emailAddress-1'),
|
||||
type: 'text',
|
||||
key: 'EMAIL'
|
||||
},
|
||||
{
|
||||
column: 'opt_in_country',
|
||||
name: t('signupCountry'),
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
column: 'created',
|
||||
name: t('signUpDate'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_open',
|
||||
name: t('latestOpen'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_click',
|
||||
name: t('latestClick'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'is_test',
|
||||
name: t('Test user'),
|
||||
type: 'option'
|
||||
}
|
||||
];
|
||||
|
||||
ruleHelpers.fields = [
|
||||
...predefColumns,
|
||||
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
|
||||
];
|
||||
|
||||
ruleHelpers.fieldsByColumn = {};
|
||||
for (const fld of ruleHelpers.fields) {
|
||||
ruleHelpers.fieldsByColumn[fld.column] = fld;
|
||||
}
|
||||
|
||||
ruleHelpers.getColumnType = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.type;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getColumnName = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.name;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getRuleTypeSettings = rule => {
|
||||
if (ruleHelpers.isCompositeRuleType(rule.type)) {
|
||||
return ruleHelpers.compositeRuleTypes[rule.type];
|
||||
} else {
|
||||
const colType = ruleHelpers.getColumnType(rule.column);
|
||||
|
||||
if (colType) {
|
||||
if (rule.type in ruleHelpers.primitiveRuleTypes[colType]) {
|
||||
return ruleHelpers.primitiveRuleTypes[colType][rule.type];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.isCompositeRuleType = ruleType => ruleType in ruleHelpers.compositeRuleTypes;
|
||||
|
||||
return ruleHelpers;
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {DatePicker, Dropdown, InputField} from "../../lib/form";
|
||||
import { parseDate, parseBirthday, formatDate, formatBirthday, DateFormat, birthdayYear, getDateFormatString, getBirthdayFormatString } from '../../../../shared/date';
|
||||
import { tMark } from "../../lib/i18n";
|
||||
|
||||
export function getRuleHelpers(t, fields) {
|
||||
|
||||
const ruleHelpers = {};
|
||||
|
||||
ruleHelpers.compositeRuleTypes = {
|
||||
all: {
|
||||
dropdownLabel: t('allRulesMustMatch'),
|
||||
treeLabel: rule => t('allRulesMustMatch')
|
||||
},
|
||||
some: {
|
||||
dropdownLabel: t('atLeastOneRuleMustMatch'),
|
||||
treeLabel: rule => t('atLeastOneRuleMustMatch')
|
||||
},
|
||||
none: {
|
||||
dropdownLabel: t('noRuleMayMatch'),
|
||||
treeLabel: rule => t('noRuleMayMatch')
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes = {};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.text = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('alphabeticallyBefore'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('alphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('alphabeticallyAfter'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('alphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsAlphabetically-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.website = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('matchWithSqlLike'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithSqlLike', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('matchWithRegularExpressions'),
|
||||
treeLabel: rule => t('valueInColumnColNameMatchesWithRegular', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.number = {
|
||||
eq: {
|
||||
dropdownLabel: t('equalTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsEqualToValue-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('lessThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('lessThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsLessThanOrEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('greaterThan'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanValue', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('greaterThanOrEqualTo'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsGreaterThanOrEqual', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
// FXIME - the localization here is still wrong
|
||||
function getRelativeDateTreeLabel(rule, variants) {
|
||||
if (rule.value === 0) {
|
||||
return t(variants[0], {colName: ruleHelpers.getColumnName(rule.column)})
|
||||
} else if (rule.value > 0) {
|
||||
return t(variants[1], {colName: ruleHelpers.getColumnName(rule.column), value: rule.value});
|
||||
} else {
|
||||
return t(variants[2], {colName: ruleHelpers.getColumnName(rule.column), value: -rule.value});
|
||||
}
|
||||
}
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.date = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatDate(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
eqTodayPlusDays: {
|
||||
dropdownLabel: t('onXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsTheCurrentDate'), tMark('dateInColumnColNameIsTheValuethDayAfter'), tMark('dateInColumnColNameIsTheValuethDayBefore')]),
|
||||
},
|
||||
ltTodayPlusDays: {
|
||||
dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeTheCurrent'), tMark('dateInColumnColNameIsBeforeTheValuethDay'), tMark('dateInColumnColNameIsBeforeTheValuethDay-1')]),
|
||||
},
|
||||
leTodayPlusDays: {
|
||||
dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsBeforeOrOnThe'), tMark('dateInColumnColNameIsBeforeOrOnThe-1'), tMark('dateInColumnColNameIsBeforeOrOnThe-2')]),
|
||||
},
|
||||
gtTodayPlusDays: {
|
||||
dropdownLabel: t('afterXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterTheCurrentDate'), tMark('dateInColumnColNameIsAfterTheValuethDay'), tMark('dateInColumnColNameIsAfterTheValuethDay-1')]),
|
||||
},
|
||||
geTodayPlusDays: {
|
||||
dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'),
|
||||
treeLabel: rule => getRelativeDateTreeLabel(rule, [tMark('dateInColumnColNameIsAfterOrOnTheCurrent'), tMark('dateInColumnColNameIsAfterOrOnTheValueth'), tMark('dateInColumnColNameIsAfterOrOnTheValueth-1')]),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.birthday = {
|
||||
eq: {
|
||||
dropdownLabel: t('on'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('before'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('beforeOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsBeforeOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('after'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('afterOrOn'),
|
||||
treeLabel: rule => t('dateInColumnColNameIsAfterOrOnValue', {colName: ruleHelpers.getColumnName(rule.column), value: formatBirthday(DateFormat.INTL, rule.value)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes.option = {
|
||||
isTrue: {
|
||||
dropdownLabel: t('isSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
},
|
||||
isFalse: {
|
||||
dropdownLabel: t('isNotSelected'),
|
||||
treeLabel: rule => t('valueInColumnColNameIsNotSelected', {colName: ruleHelpers.getColumnName(rule.column)}),
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.primitiveRuleTypes['dropdown-enum'] = ruleHelpers.primitiveRuleTypes['radio-enum'] = {
|
||||
eq: {
|
||||
dropdownLabel: t('keyEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIsEqualTo', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
like: {
|
||||
dropdownLabel: t('keyMatchWithSqlLike'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
re: {
|
||||
dropdownLabel: t('keyMatchWithRegularExpressions'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameMatchesWith-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
lt: {
|
||||
dropdownLabel: t('keyAlphabeticallyBefore'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
le: {
|
||||
dropdownLabel: t('keyAlphabeticallyBeforeOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-1', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
gt: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfter'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-2', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
},
|
||||
ge: {
|
||||
dropdownLabel: t('keyAlphabeticallyAfterOrEqualTo'),
|
||||
treeLabel: rule => t('theSelectedKeyInColumnColNameIs-3', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const stringValueSettings = allowEmpty => ({
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = getter('value');
|
||||
},
|
||||
validate: state => {
|
||||
if (!allowEmpty && !state.getIn(['value', 'value'])) {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const numberValueSettings = {
|
||||
getForm: () => <InputField id="value" label={t('value')} />,
|
||||
getFormData: rule => ({
|
||||
value: rule.value.toString()
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseInt(getter('value'));
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['value', 'value']).trim();
|
||||
if (value === '') {
|
||||
state.setIn(['value', 'error'], t('valueMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['value', 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['value', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const birthdayValueSettings = {
|
||||
getForm: () => <DatePicker id="birthday" label={t('date')} birthday />,
|
||||
getFormData: rule => ({
|
||||
birthday: formatBirthday(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseBirthday(DateFormat.INTL, getter('birthday')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['birthday', 'value']);
|
||||
const date = parseBirthday(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['birthday', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['birthday', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['birthday', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateValueSettings = {
|
||||
getForm: () => <DatePicker id="date" label={t('date')} />,
|
||||
getFormData: rule => ({
|
||||
date: formatDate(DateFormat.INTL, rule.value)
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
rule.value = parseDate(DateFormat.INTL, getter('date')).toISOString();
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['date', 'value']);
|
||||
const date = parseDate(DateFormat.INTL, value);
|
||||
if (!value) {
|
||||
state.setIn(['date', 'error'], t('dateMustNotBeEmpty'));
|
||||
} else if (!date) {
|
||||
state.setIn(['date', 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn(['date', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dateRelativeValueSettings = {
|
||||
getForm: () =>
|
||||
<div>
|
||||
<InputField id="daysValue" label={t('numberOfDays')}/>
|
||||
<Dropdown id="direction" label={t('beforeAfter')} options={[
|
||||
{ key: 'before', label: t('beforeCurrentDate') },
|
||||
{ key: 'after', label: t('afterCurrentDate') }
|
||||
]}/>
|
||||
</div>,
|
||||
getFormData: rule => ({
|
||||
daysValue: Math.abs(rule.value).toString(),
|
||||
direction: rule.value >= 0 ? 'after' : 'before'
|
||||
}),
|
||||
assignRuleSettings: (rule, getter) => {
|
||||
const direction = getter('direction');
|
||||
rule.value = parseInt(getter('daysValue')) * (direction === 'before' ? -1 : 1);
|
||||
},
|
||||
validate: state => {
|
||||
const value = state.getIn(['daysValue', 'value']);
|
||||
if (!value) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustNotBeEmpty'));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn(['daysValue', 'error'], t('numberOfDaysMustBeANumber'));
|
||||
} else {
|
||||
state.setIn(['daysValue', 'error'], null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const optionValueSettings = {
|
||||
getForm: () => null,
|
||||
getFormData: rule => ({}),
|
||||
assignRuleSettings: (rule, getter) => {},
|
||||
validate: state => {}
|
||||
};
|
||||
|
||||
|
||||
function assignSettingsToRuleTypes(ruleTypes, keys, settings) {
|
||||
for (const key of keys) {
|
||||
Object.assign(ruleTypes[key], settings);
|
||||
}
|
||||
}
|
||||
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.text, ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.website, ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.number, ['eq', 'lt', 'le', 'gt', 'ge'], numberValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.birthday, ['eq', 'lt', 'le', 'gt', 'ge'], birthdayValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eq', 'lt', 'le', 'gt', 'ge'], dateValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.date, ['eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'], dateRelativeValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes.option, ['isTrue', 'isFalse'], optionValueSettings);
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['dropdown-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['eq', 'like', 're'], stringValueSettings(true));
|
||||
assignSettingsToRuleTypes(ruleHelpers.primitiveRuleTypes['radio-enum'], ['lt', 'le', 'gt', 'ge'], stringValueSettings(false));
|
||||
|
||||
ruleHelpers.primitiveRuleTypesFormDataDefaults = {
|
||||
value: '',
|
||||
date: '',
|
||||
daysValue: '',
|
||||
birthday: '',
|
||||
direction: 'before'
|
||||
};
|
||||
|
||||
|
||||
|
||||
ruleHelpers.getCompositeRuleTypeOptions = () => {
|
||||
const order = ['all', 'some', 'none'];
|
||||
return order.map(key => ({ key, label: ruleHelpers.compositeRuleTypes[key].dropdownLabel }));
|
||||
};
|
||||
|
||||
ruleHelpers.getPrimitiveRuleTypeOptions = columnType => {
|
||||
const order = {
|
||||
text: ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
website: ['eq', 'like', 're'],
|
||||
number: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
birthday: ['eq', 'lt', 'le', 'gt', 'ge'],
|
||||
date: ['eq', 'lt', 'le', 'gt', 'ge', 'eqTodayPlusDays', 'ltTodayPlusDays', 'leTodayPlusDays', 'gtTodayPlusDays', 'geTodayPlusDays'],
|
||||
option: ['isTrue', 'isFalse'],
|
||||
'dropdown-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge'],
|
||||
'radio-enum': ['eq', 'like', 're', 'lt', 'le', 'gt', 'ge']
|
||||
};
|
||||
|
||||
return order[columnType].map(key => ({ key, label: ruleHelpers.primitiveRuleTypes[columnType][key].dropdownLabel }));
|
||||
};
|
||||
|
||||
const predefColumns = [
|
||||
{
|
||||
column: 'email',
|
||||
name: t('emailAddress-1'),
|
||||
type: 'text',
|
||||
key: 'EMAIL'
|
||||
},
|
||||
{
|
||||
column: 'opt_in_country',
|
||||
name: t('signupCountry'),
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
column: 'created',
|
||||
name: t('signUpDate'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_open',
|
||||
name: t('latestOpen'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'latest_click',
|
||||
name: t('latestClick'),
|
||||
type: 'date'
|
||||
},
|
||||
{
|
||||
column: 'is_test',
|
||||
name: t('Test user'),
|
||||
type: 'option'
|
||||
}
|
||||
];
|
||||
|
||||
ruleHelpers.fields = [
|
||||
...predefColumns,
|
||||
...fields.filter(fld => fld.type in ruleHelpers.primitiveRuleTypes)
|
||||
];
|
||||
|
||||
ruleHelpers.fieldsByColumn = {};
|
||||
for (const fld of ruleHelpers.fields) {
|
||||
ruleHelpers.fieldsByColumn[fld.column] = fld;
|
||||
}
|
||||
|
||||
ruleHelpers.getColumnType = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.type;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getColumnName = column => {
|
||||
const field = ruleHelpers.fieldsByColumn[column];
|
||||
if (field) {
|
||||
return field.name;
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.getRuleTypeSettings = rule => {
|
||||
if (ruleHelpers.isCompositeRuleType(rule.type)) {
|
||||
return ruleHelpers.compositeRuleTypes[rule.type];
|
||||
} else {
|
||||
const colType = ruleHelpers.getColumnType(rule.column);
|
||||
|
||||
if (colType) {
|
||||
if (rule.type in ruleHelpers.primitiveRuleTypes[colType]) {
|
||||
return ruleHelpers.primitiveRuleTypes[colType][rule.type];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ruleHelpers.isCompositeRuleType = ruleType => ruleType in ruleHelpers.compositeRuleTypes;
|
||||
|
||||
return ruleHelpers;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.mapping {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.erased {
|
||||
color: #808080;
|
||||
.mapping {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.erased {
|
||||
color: #808080;
|
||||
}
|
|
@ -1,212 +1,212 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {SubscriptionStatus} from "../../../../shared/lists";
|
||||
import {
|
||||
ACEEditor,
|
||||
CheckBox,
|
||||
CheckBoxGroup,
|
||||
DatePicker,
|
||||
Dropdown,
|
||||
InputField,
|
||||
RadioGroup,
|
||||
TextArea
|
||||
} from "../../lib/form";
|
||||
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
||||
import {getFieldColumn} from '../../../../shared/lists';
|
||||
import 'brace/mode/json';
|
||||
|
||||
export function getSubscriptionStatusLabels(t) {
|
||||
|
||||
const subscriptionStatusLabels = {
|
||||
[SubscriptionStatus.SUBSCRIBED]: t('subscribed'),
|
||||
[SubscriptionStatus.UNSUBSCRIBED]: t('unubscribed'),
|
||||
[SubscriptionStatus.BOUNCED]: t('bounced'),
|
||||
[SubscriptionStatus.COMPLAINED]: t('complained'),
|
||||
};
|
||||
|
||||
return subscriptionStatusLabels;
|
||||
}
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const groupedFieldTypes = {};
|
||||
|
||||
const stringFieldType = long => ({
|
||||
form: groupedField => long ? <TextArea key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/> : <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
});
|
||||
|
||||
const numberFieldType = {
|
||||
form: groupedField => <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? value.toString() : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = parseInt(data[getFieldColumn(groupedField)]);
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']).trim();
|
||||
if (value !== '' && isNaN(value)) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const dateFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatDate(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseDate(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseDate(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const birthdayFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} birthday />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatBirthday(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const jsonFieldType = {
|
||||
form: groupedField => <ACEEditor key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
};
|
||||
|
||||
const optionFieldType = {
|
||||
form: groupedField => <CheckBox key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} text={groupedField.settings.checkedLabel} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = !!value;
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = false;
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const enumSingleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
const enumMultipleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
|
||||
groupedFieldTypes.text = stringFieldType(false);
|
||||
groupedFieldTypes.website = stringFieldType(false);
|
||||
groupedFieldTypes.longtext = stringFieldType(true);
|
||||
groupedFieldTypes.gpg = stringFieldType(true);
|
||||
groupedFieldTypes.number = numberFieldType;
|
||||
groupedFieldTypes.date = dateFieldType;
|
||||
groupedFieldTypes.birthday = birthdayFieldType;
|
||||
groupedFieldTypes.json = jsonFieldType;
|
||||
groupedFieldTypes.option = optionFieldType;
|
||||
groupedFieldTypes['dropdown-enum'] = enumSingleFieldType(Dropdown);
|
||||
groupedFieldTypes['radio-enum'] = enumSingleFieldType(RadioGroup);
|
||||
|
||||
// Here we rely on the fact the model/groupedFields and model/subscriptions preprocess the groupedField info and subscription
|
||||
// such that the grouped entries behave the same as the enum entries
|
||||
groupedFieldTypes['checkbox-grouped'] = enumMultipleFieldType(CheckBoxGroup);
|
||||
groupedFieldTypes['radio-grouped'] = enumSingleFieldType(RadioGroup);
|
||||
groupedFieldTypes['dropdown-grouped'] = enumSingleFieldType(Dropdown);
|
||||
|
||||
return groupedFieldTypes;
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {SubscriptionStatus} from "../../../../shared/lists";
|
||||
import {
|
||||
ACEEditor,
|
||||
CheckBox,
|
||||
CheckBoxGroup,
|
||||
DatePicker,
|
||||
Dropdown,
|
||||
InputField,
|
||||
RadioGroup,
|
||||
TextArea
|
||||
} from "../../lib/form";
|
||||
import {formatBirthday, formatDate, parseBirthday, parseDate} from "../../../../shared/date";
|
||||
import {getFieldColumn} from '../../../../shared/lists';
|
||||
import 'brace/mode/json';
|
||||
|
||||
export function getSubscriptionStatusLabels(t) {
|
||||
|
||||
const subscriptionStatusLabels = {
|
||||
[SubscriptionStatus.SUBSCRIBED]: t('subscribed'),
|
||||
[SubscriptionStatus.UNSUBSCRIBED]: t('unubscribed'),
|
||||
[SubscriptionStatus.BOUNCED]: t('bounced'),
|
||||
[SubscriptionStatus.COMPLAINED]: t('complained'),
|
||||
};
|
||||
|
||||
return subscriptionStatusLabels;
|
||||
}
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const groupedFieldTypes = {};
|
||||
|
||||
const stringFieldType = long => ({
|
||||
form: groupedField => long ? <TextArea key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/> : <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
});
|
||||
|
||||
const numberFieldType = {
|
||||
form: groupedField => <InputField key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? value.toString() : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = parseInt(data[getFieldColumn(groupedField)]);
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']).trim();
|
||||
if (value !== '' && isNaN(value)) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('valueMustBeANumber'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const dateFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatDate(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseDate(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseDate(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const birthdayFieldType = {
|
||||
form: groupedField => <DatePicker key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} dateFormat={groupedField.settings.dateFormat} birthday />,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value ? formatBirthday(groupedField.settings.dateFormat, value) : '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, data[getFieldColumn(groupedField)]);
|
||||
data[getFieldColumn(groupedField)] = date;
|
||||
},
|
||||
validate: (groupedField, state) => {
|
||||
const value = state.getIn([getFieldColumn(groupedField), 'value']);
|
||||
const date = parseBirthday(groupedField.settings.dateFormat, value);
|
||||
if (value !== '' && !date) {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], t('dateIsInvalid'));
|
||||
} else {
|
||||
state.setIn([getFieldColumn(groupedField), 'error'], null);
|
||||
}
|
||||
},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const jsonFieldType = {
|
||||
form: groupedField => <ACEEditor key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = value || '';
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
};
|
||||
|
||||
const optionFieldType = {
|
||||
form: groupedField => <CheckBox key={getFieldColumn(groupedField)} id={getFieldColumn(groupedField)} text={groupedField.settings.checkedLabel} label={groupedField.name}/>,
|
||||
assignFormData: (groupedField, data) => {
|
||||
const value = data[getFieldColumn(groupedField)];
|
||||
data[getFieldColumn(groupedField)] = !!value;
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = false;
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: true
|
||||
};
|
||||
|
||||
const enumSingleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
if (groupedField.default_value) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.default_value;
|
||||
} else if (groupedField.settings.options.length > 0) {
|
||||
data[getFieldColumn(groupedField)] = groupedField.settings.options[0].key;
|
||||
} else {
|
||||
data[getFieldColumn(groupedField)] = '';
|
||||
}
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
const enumMultipleFieldType = componentType => ({
|
||||
form: groupedField => React.createElement(componentType, { key: getFieldColumn(groupedField), id: getFieldColumn(groupedField), label: groupedField.name, options: groupedField.settings.options }, null),
|
||||
assignFormData: (groupedField, data) => {
|
||||
if (data[getFieldColumn(groupedField)] === null) {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
}
|
||||
},
|
||||
initFormData: (groupedField, data) => {
|
||||
data[getFieldColumn(groupedField)] = [];
|
||||
},
|
||||
assignEntity: (groupedField, data) => {},
|
||||
validate: (groupedField, state) => {},
|
||||
indexable: false
|
||||
});
|
||||
|
||||
|
||||
groupedFieldTypes.text = stringFieldType(false);
|
||||
groupedFieldTypes.website = stringFieldType(false);
|
||||
groupedFieldTypes.longtext = stringFieldType(true);
|
||||
groupedFieldTypes.gpg = stringFieldType(true);
|
||||
groupedFieldTypes.number = numberFieldType;
|
||||
groupedFieldTypes.date = dateFieldType;
|
||||
groupedFieldTypes.birthday = birthdayFieldType;
|
||||
groupedFieldTypes.json = jsonFieldType;
|
||||
groupedFieldTypes.option = optionFieldType;
|
||||
groupedFieldTypes['dropdown-enum'] = enumSingleFieldType(Dropdown);
|
||||
groupedFieldTypes['radio-enum'] = enumSingleFieldType(RadioGroup);
|
||||
|
||||
// Here we rely on the fact the model/groupedFields and model/subscriptions preprocess the groupedField info and subscription
|
||||
// such that the grouped entries behave the same as the enum entries
|
||||
groupedFieldTypes['checkbox-grouped'] = enumMultipleFieldType(CheckBoxGroup);
|
||||
groupedFieldTypes['radio-grouped'] = enumSingleFieldType(RadioGroup);
|
||||
groupedFieldTypes['dropdown-grouped'] = enumSingleFieldType(Dropdown);
|
||||
|
||||
return groupedFieldTypes;
|
||||
}
|
|
@ -1,337 +1,337 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
import {MailerType, ZoneMTAType} from "../../../shared/send-configurations";
|
||||
import {
|
||||
CheckBox,
|
||||
Dropdown,
|
||||
Fieldset,
|
||||
InputField,
|
||||
TextArea
|
||||
} from "../lib/form";
|
||||
import {Trans} from "react-i18next";
|
||||
import styles from "./styles.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
export const mailerTypesOrder = [
|
||||
MailerType.ZONE_MTA,
|
||||
MailerType.GENERIC_SMTP,
|
||||
MailerType.AWS_SES
|
||||
];
|
||||
|
||||
export function getMailerTypes(t) {
|
||||
const mailerTypes = {};
|
||||
|
||||
function initFieldsIfMissing(mutStateData, mailerType) {
|
||||
const initVals = mailerTypes[mailerType].initData();
|
||||
|
||||
for (const key in initVals) {
|
||||
if (!mutStateData.hasIn([key])) {
|
||||
mutStateData.setIn([key, 'value'], initVals[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearBeforeSave(data) {
|
||||
for (const mailerKey in mailerTypes) {
|
||||
const initVals = mailerTypes[mailerKey].initData();
|
||||
for (const fieldKey in initVals) {
|
||||
delete data[fieldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumber(state, field, label, emptyAllowed = false) {
|
||||
const value = state.getIn([field, 'value']);
|
||||
if (typeof value === 'string' && value.trim() === '' && !emptyAllowed) { // After load, the numerical values can be still numbers
|
||||
state.setIn([field, 'error'], t('labelMustNotBeEmpty', {label}));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn([field, 'error'], t('labelMustBeANumber', {label}));
|
||||
} else {
|
||||
state.setIn([field, 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
function getInitCommon() {
|
||||
return {
|
||||
maxConnections: '5',
|
||||
throttling: '',
|
||||
logTransactions: false
|
||||
};
|
||||
}
|
||||
|
||||
function getInitGenericSMTP() {
|
||||
return {
|
||||
...getInitCommon(),
|
||||
smtpHostname: '',
|
||||
smtpPort: '',
|
||||
smtpEncryption: 'NONE',
|
||||
smtpUseAuth: false,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
smtpAllowSelfSigned: false,
|
||||
smtpMaxMessages: '100'
|
||||
};
|
||||
}
|
||||
|
||||
function afterLoadCommon(data) {
|
||||
data.maxConnections = data.mailer_settings.maxConnections;
|
||||
data.throttling = data.mailer_settings.throttling || '';
|
||||
data.logTransactions = data.mailer_settings.logTransactions;
|
||||
}
|
||||
|
||||
function afterLoadGenericSMTP(data) {
|
||||
afterLoadCommon(data);
|
||||
data.smtpHostname = data.mailer_settings.hostname;
|
||||
data.smtpPort = data.mailer_settings.port || '';
|
||||
data.smtpEncryption = data.mailer_settings.encryption;
|
||||
data.smtpUseAuth = data.mailer_settings.useAuth;
|
||||
data.smtpUser = data.mailer_settings.user;
|
||||
data.smtpPassword = data.mailer_settings.password;
|
||||
data.smtpAllowSelfSigned = data.mailer_settings.allowSelfSigned;
|
||||
data.smtpMaxMessages = data.mailer_settings.maxMessages;
|
||||
}
|
||||
|
||||
function beforeSaveCommon(data) {
|
||||
data.mailer_settings = {};
|
||||
data.mailer_settings.maxConnections = Number(data.maxConnections);
|
||||
data.mailer_settings.throttling = Number(data.throttling);
|
||||
data.mailer_settings.logTransactions = data.logTransactions;
|
||||
}
|
||||
|
||||
function beforeSaveGenericSMTP(data, builtin = false) {
|
||||
beforeSaveCommon(data);
|
||||
|
||||
if (!builtin) {
|
||||
data.mailer_settings.hostname = data.smtpHostname;
|
||||
data.mailer_settings.port = Number(data.smtpPort);
|
||||
data.mailer_settings.encryption = data.smtpEncryption;
|
||||
data.mailer_settings.useAuth = data.smtpUseAuth;
|
||||
data.mailer_settings.user = data.smtpUser;
|
||||
data.mailer_settings.password = data.smtpPassword;
|
||||
data.mailer_settings.allowSelfSigned = data.smtpAllowSelfSigned;
|
||||
}
|
||||
|
||||
data.mailer_settings.maxMessages = Number(data.smtpMaxMessages);
|
||||
}
|
||||
|
||||
function validateCommon(state) {
|
||||
validateNumber(state, 'maxConnections', 'Max connections');
|
||||
validateNumber(state, 'throttling', 'Throttling', true);
|
||||
}
|
||||
|
||||
function validateGenericSMTP(state) {
|
||||
validateCommon(state);
|
||||
validateNumber(state, 'smtpPort', 'Port', true);
|
||||
validateNumber(state, 'smtpMaxMessages', 'Max messages');
|
||||
}
|
||||
|
||||
const typeNames = {
|
||||
[MailerType.GENERIC_SMTP]: t('genericSmtp'),
|
||||
[MailerType.ZONE_MTA]: t('zoneMta'),
|
||||
[MailerType.AWS_SES]: t('amazonSes')
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ key: MailerType.GENERIC_SMTP, label: typeNames[MailerType.GENERIC_SMTP]},
|
||||
{ key: MailerType.ZONE_MTA, label: typeNames[MailerType.ZONE_MTA]},
|
||||
{ key: MailerType.AWS_SES, label: typeNames[MailerType.AWS_SES]}
|
||||
];
|
||||
|
||||
const smtpEncryptionOptions = [
|
||||
{ key: 'NONE', label: t('doNotUseEncryption')},
|
||||
{ key: 'TLS', label: t('useTls –UsuallySelectedForPort465')},
|
||||
{ key: 'STARTTLS', label: t('useStarttls –UsuallySelectedForPort587')}
|
||||
];
|
||||
|
||||
const sesRegionOptions = [
|
||||
{ key: 'us-east-1', label: t('useast1')},
|
||||
{ key: 'us-west-2', label: t('uswest2')},
|
||||
{ key: 'eu-west-1', label: t('euwest1')}
|
||||
];
|
||||
|
||||
const zoneMtaTypeOptions = [];
|
||||
|
||||
if (mailtrainConfig.builtinZoneMTAEnabled) {
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.BUILTIN, label: t('builtinZoneMta')});
|
||||
}
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_HTTP_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt-1')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.REGULAR, label: t('noDynamicConfigurationOfDkimKeys')});
|
||||
|
||||
mailerTypes[MailerType.GENERIC_SMTP] = {
|
||||
typeName: typeNames[MailerType.GENERIC_SMTP],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP()
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveGenericSMTP(data);
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.GENERIC_SMTP);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.ZONE_MTA] = {
|
||||
typeName: typeNames[MailerType.ZONE_MTA],
|
||||
getForm: owner => {
|
||||
const zoneMtaType = Number.parseInt(owner.getFormValue('zoneMtaType'));
|
||||
return (
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<Dropdown id="zoneMtaType" label={t('dynamicConfiguration')} options={zoneMtaTypeOptions}/>
|
||||
{(zoneMtaType === ZoneMTAType.REGULAR || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<div>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
{(zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<Fieldset label={t('dkimSigning')}>
|
||||
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages.</p></Trans>
|
||||
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
|
||||
{zoneMtaType === ZoneMTAType.WITH_HTTP_CONF &&
|
||||
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
|
||||
}
|
||||
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
|
||||
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
|
||||
<TextArea id="dkimPrivateKey" className={styles.dkimPrivateKey} label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
|
||||
</Fieldset>
|
||||
}
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP(),
|
||||
zoneMtaType: mailtrainConfig.builtinZoneMTAEnabled ? ZoneMTAType.BUILTIN : ZoneMTAType.REGULAR,
|
||||
dkimApiKey: '',
|
||||
dkimDomain: '',
|
||||
dkimSelector: '',
|
||||
dkimPrivateKey: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
data.zoneMtaType = data.mailer_settings.zoneMtaType;
|
||||
data.dkimApiKey = data.mailer_settings.dkimApiKey;
|
||||
data.dkimDomain = data.mailer_settings.dkimDomain;
|
||||
data.dkimSelector = data.mailer_settings.dkimSelector;
|
||||
data.dkimPrivateKey = data.mailer_settings.dkimPrivateKey;
|
||||
},
|
||||
beforeSave: data => {
|
||||
const zoneMtaType = Number.parseInt(data.zoneMtaType);
|
||||
|
||||
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
|
||||
|
||||
data.mailer_settings.zoneMtaType = zoneMtaType;
|
||||
if (zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
|
||||
data.mailer_settings.dkimDomain = data.dkimDomain;
|
||||
data.mailer_settings.dkimSelector = data.dkimSelector;
|
||||
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
|
||||
}
|
||||
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) {
|
||||
data.mailer_settings.dkimApiKey = data.dkimApiKey;
|
||||
}
|
||||
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.ZONE_MTA);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.AWS_SES] = {
|
||||
typeName: typeNames[MailerType.AWS_SES],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
|
||||
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/>
|
||||
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitCommon(),
|
||||
sesKey: '',
|
||||
sesSecret: '',
|
||||
sesRegion: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadCommon(data);
|
||||
data.sesKey = data.mailer_settings.key;
|
||||
data.sesSecret = data.mailer_settings.secret;
|
||||
data.sesRegion = data.mailer_settings.region;
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveCommon(data);
|
||||
data.mailer_settings.key = data.sesKey;
|
||||
data.mailer_settings.secret = data.sesSecret;
|
||||
data.mailer_settings.region = data.sesRegion;
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.AWS_SES);
|
||||
},
|
||||
validate: state => {
|
||||
validateCommon(state);
|
||||
}
|
||||
};
|
||||
|
||||
return mailerTypes;
|
||||
}
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
import {MailerType, ZoneMTAType} from "../../../shared/send-configurations";
|
||||
import {
|
||||
CheckBox,
|
||||
Dropdown,
|
||||
Fieldset,
|
||||
InputField,
|
||||
TextArea
|
||||
} from "../lib/form";
|
||||
import {Trans} from "react-i18next";
|
||||
import styles from "./styles.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
export const mailerTypesOrder = [
|
||||
MailerType.ZONE_MTA,
|
||||
MailerType.GENERIC_SMTP,
|
||||
MailerType.AWS_SES
|
||||
];
|
||||
|
||||
export function getMailerTypes(t) {
|
||||
const mailerTypes = {};
|
||||
|
||||
function initFieldsIfMissing(mutStateData, mailerType) {
|
||||
const initVals = mailerTypes[mailerType].initData();
|
||||
|
||||
for (const key in initVals) {
|
||||
if (!mutStateData.hasIn([key])) {
|
||||
mutStateData.setIn([key, 'value'], initVals[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearBeforeSave(data) {
|
||||
for (const mailerKey in mailerTypes) {
|
||||
const initVals = mailerTypes[mailerKey].initData();
|
||||
for (const fieldKey in initVals) {
|
||||
delete data[fieldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumber(state, field, label, emptyAllowed = false) {
|
||||
const value = state.getIn([field, 'value']);
|
||||
if (typeof value === 'string' && value.trim() === '' && !emptyAllowed) { // After load, the numerical values can be still numbers
|
||||
state.setIn([field, 'error'], t('labelMustNotBeEmpty', {label}));
|
||||
} else if (isNaN(value)) {
|
||||
state.setIn([field, 'error'], t('labelMustBeANumber', {label}));
|
||||
} else {
|
||||
state.setIn([field, 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
function getInitCommon() {
|
||||
return {
|
||||
maxConnections: '5',
|
||||
throttling: '',
|
||||
logTransactions: false
|
||||
};
|
||||
}
|
||||
|
||||
function getInitGenericSMTP() {
|
||||
return {
|
||||
...getInitCommon(),
|
||||
smtpHostname: '',
|
||||
smtpPort: '',
|
||||
smtpEncryption: 'NONE',
|
||||
smtpUseAuth: false,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
smtpAllowSelfSigned: false,
|
||||
smtpMaxMessages: '100'
|
||||
};
|
||||
}
|
||||
|
||||
function afterLoadCommon(data) {
|
||||
data.maxConnections = data.mailer_settings.maxConnections;
|
||||
data.throttling = data.mailer_settings.throttling || '';
|
||||
data.logTransactions = data.mailer_settings.logTransactions;
|
||||
}
|
||||
|
||||
function afterLoadGenericSMTP(data) {
|
||||
afterLoadCommon(data);
|
||||
data.smtpHostname = data.mailer_settings.hostname;
|
||||
data.smtpPort = data.mailer_settings.port || '';
|
||||
data.smtpEncryption = data.mailer_settings.encryption;
|
||||
data.smtpUseAuth = data.mailer_settings.useAuth;
|
||||
data.smtpUser = data.mailer_settings.user;
|
||||
data.smtpPassword = data.mailer_settings.password;
|
||||
data.smtpAllowSelfSigned = data.mailer_settings.allowSelfSigned;
|
||||
data.smtpMaxMessages = data.mailer_settings.maxMessages;
|
||||
}
|
||||
|
||||
function beforeSaveCommon(data) {
|
||||
data.mailer_settings = {};
|
||||
data.mailer_settings.maxConnections = Number(data.maxConnections);
|
||||
data.mailer_settings.throttling = Number(data.throttling);
|
||||
data.mailer_settings.logTransactions = data.logTransactions;
|
||||
}
|
||||
|
||||
function beforeSaveGenericSMTP(data, builtin = false) {
|
||||
beforeSaveCommon(data);
|
||||
|
||||
if (!builtin) {
|
||||
data.mailer_settings.hostname = data.smtpHostname;
|
||||
data.mailer_settings.port = Number(data.smtpPort);
|
||||
data.mailer_settings.encryption = data.smtpEncryption;
|
||||
data.mailer_settings.useAuth = data.smtpUseAuth;
|
||||
data.mailer_settings.user = data.smtpUser;
|
||||
data.mailer_settings.password = data.smtpPassword;
|
||||
data.mailer_settings.allowSelfSigned = data.smtpAllowSelfSigned;
|
||||
}
|
||||
|
||||
data.mailer_settings.maxMessages = Number(data.smtpMaxMessages);
|
||||
}
|
||||
|
||||
function validateCommon(state) {
|
||||
validateNumber(state, 'maxConnections', 'Max connections');
|
||||
validateNumber(state, 'throttling', 'Throttling', true);
|
||||
}
|
||||
|
||||
function validateGenericSMTP(state) {
|
||||
validateCommon(state);
|
||||
validateNumber(state, 'smtpPort', 'Port', true);
|
||||
validateNumber(state, 'smtpMaxMessages', 'Max messages');
|
||||
}
|
||||
|
||||
const typeNames = {
|
||||
[MailerType.GENERIC_SMTP]: t('genericSmtp'),
|
||||
[MailerType.ZONE_MTA]: t('zoneMta'),
|
||||
[MailerType.AWS_SES]: t('amazonSes')
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ key: MailerType.GENERIC_SMTP, label: typeNames[MailerType.GENERIC_SMTP]},
|
||||
{ key: MailerType.ZONE_MTA, label: typeNames[MailerType.ZONE_MTA]},
|
||||
{ key: MailerType.AWS_SES, label: typeNames[MailerType.AWS_SES]}
|
||||
];
|
||||
|
||||
const smtpEncryptionOptions = [
|
||||
{ key: 'NONE', label: t('doNotUseEncryption')},
|
||||
{ key: 'TLS', label: t('useTls –UsuallySelectedForPort465')},
|
||||
{ key: 'STARTTLS', label: t('useStarttls –UsuallySelectedForPort587')}
|
||||
];
|
||||
|
||||
const sesRegionOptions = [
|
||||
{ key: 'us-east-1', label: t('useast1')},
|
||||
{ key: 'us-west-2', label: t('uswest2')},
|
||||
{ key: 'eu-west-1', label: t('euwest1')}
|
||||
];
|
||||
|
||||
const zoneMtaTypeOptions = [];
|
||||
|
||||
if (mailtrainConfig.builtinZoneMTAEnabled) {
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.BUILTIN, label: t('builtinZoneMta')});
|
||||
}
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.WITH_HTTP_CONF, label: t('dynamicConfigurationOfDkimKeysViaZoneMt-1')});
|
||||
zoneMtaTypeOptions.push({ key: ZoneMTAType.REGULAR, label: t('noDynamicConfigurationOfDkimKeys')});
|
||||
|
||||
mailerTypes[MailerType.GENERIC_SMTP] = {
|
||||
typeName: typeNames[MailerType.GENERIC_SMTP],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP()
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveGenericSMTP(data);
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.GENERIC_SMTP);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.ZONE_MTA] = {
|
||||
typeName: typeNames[MailerType.ZONE_MTA],
|
||||
getForm: owner => {
|
||||
const zoneMtaType = Number.parseInt(owner.getFormValue('zoneMtaType'));
|
||||
return (
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<Dropdown id="zoneMtaType" label={t('dynamicConfiguration')} options={zoneMtaTypeOptions}/>
|
||||
{(zoneMtaType === ZoneMTAType.REGULAR || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<div>
|
||||
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
|
||||
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
<InputField id="smtpPassword" type="password" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
{(zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
|
||||
<Fieldset label={t('dkimSigning')}>
|
||||
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages.</p></Trans>
|
||||
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
|
||||
{zoneMtaType === ZoneMTAType.WITH_HTTP_CONF &&
|
||||
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
|
||||
}
|
||||
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
|
||||
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
|
||||
<TextArea id="dkimPrivateKey" className={styles.dkimPrivateKey} label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
|
||||
</Fieldset>
|
||||
}
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP(),
|
||||
zoneMtaType: mailtrainConfig.builtinZoneMTAEnabled ? ZoneMTAType.BUILTIN : ZoneMTAType.REGULAR,
|
||||
dkimApiKey: '',
|
||||
dkimDomain: '',
|
||||
dkimSelector: '',
|
||||
dkimPrivateKey: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
data.zoneMtaType = data.mailer_settings.zoneMtaType;
|
||||
data.dkimApiKey = data.mailer_settings.dkimApiKey;
|
||||
data.dkimDomain = data.mailer_settings.dkimDomain;
|
||||
data.dkimSelector = data.mailer_settings.dkimSelector;
|
||||
data.dkimPrivateKey = data.mailer_settings.dkimPrivateKey;
|
||||
},
|
||||
beforeSave: data => {
|
||||
const zoneMtaType = Number.parseInt(data.zoneMtaType);
|
||||
|
||||
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
|
||||
|
||||
data.mailer_settings.zoneMtaType = zoneMtaType;
|
||||
if (zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
|
||||
data.mailer_settings.dkimDomain = data.dkimDomain;
|
||||
data.mailer_settings.dkimSelector = data.dkimSelector;
|
||||
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
|
||||
}
|
||||
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) {
|
||||
data.mailer_settings.dkimApiKey = data.dkimApiKey;
|
||||
}
|
||||
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.ZONE_MTA);
|
||||
},
|
||||
validate: state => {
|
||||
validateGenericSMTP(state);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.AWS_SES] = {
|
||||
typeName: typeNames[MailerType.AWS_SES],
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('mailerSettings')}>
|
||||
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
|
||||
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
|
||||
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/>
|
||||
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
|
||||
</Fieldset>
|
||||
<Fieldset label={t('advancedMailerSettings')}>
|
||||
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
|
||||
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
|
||||
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitCommon(),
|
||||
sesKey: '',
|
||||
sesSecret: '',
|
||||
sesRegion: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadCommon(data);
|
||||
data.sesKey = data.mailer_settings.key;
|
||||
data.sesSecret = data.mailer_settings.secret;
|
||||
data.sesRegion = data.mailer_settings.region;
|
||||
},
|
||||
beforeSave: data => {
|
||||
beforeSaveCommon(data);
|
||||
data.mailer_settings.key = data.sesKey;
|
||||
data.mailer_settings.secret = data.sesSecret;
|
||||
data.mailer_settings.region = data.sesRegion;
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.AWS_SES);
|
||||
},
|
||||
validate: state => {
|
||||
validateCommon(state);
|
||||
}
|
||||
};
|
||||
|
||||
return mailerTypes;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,51 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {ACEEditor} from "../../lib/form";
|
||||
import 'brace/mode/html'
|
||||
import 'brace/mode/xml'
|
||||
|
||||
export function getTemplateTypesOrder() {
|
||||
return [/* 'mjml' , */ 'html'];
|
||||
}
|
||||
|
||||
export function getTemplateTypes(t) {
|
||||
const templateTypes = {};
|
||||
|
||||
function clearBeforeSend(data) {
|
||||
delete data.html;
|
||||
delete data.mjml;
|
||||
}
|
||||
|
||||
templateTypes.html = {
|
||||
typeName: t('html'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.html = data.data.html;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
html: data.html
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
templateTypes.mjml = {
|
||||
typeName: t('mjml'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.mjml = data.data.mjml;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
mjml: data.mjml
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
return templateTypes;
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import {ACEEditor} from "../../lib/form";
|
||||
import 'brace/mode/html'
|
||||
import 'brace/mode/xml'
|
||||
|
||||
export function getTemplateTypesOrder() {
|
||||
return [/* 'mjml' , */ 'html'];
|
||||
}
|
||||
|
||||
export function getTemplateTypes(t) {
|
||||
const templateTypes = {};
|
||||
|
||||
function clearBeforeSend(data) {
|
||||
delete data.html;
|
||||
delete data.mjml;
|
||||
}
|
||||
|
||||
templateTypes.html = {
|
||||
typeName: t('html'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.html = data.data.html;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
html: data.html
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
templateTypes.mjml = {
|
||||
typeName: t('mjml'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="xml" label={t('templateContent')}/>,
|
||||
afterLoad: data => {
|
||||
data.mjml = data.data.mjml;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
data.data = {
|
||||
mjml: data.mjml
|
||||
};
|
||||
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
};
|
||||
|
||||
return templateTypes;
|
||||
}
|
|
@ -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.
|
4
client/static/mosaico/uploads/.gitignore
vendored
4
client/static/mosaico/uploads/.gitignore
vendored
|
@ -1,3 +1,3 @@
|
|||
*
|
||||
!.gitignore
|
||||
*
|
||||
!.gitignore
|
||||
!README.md
|
|
@ -1,7 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./noto-sans-400-normal.eot');
|
||||
src: local('Noto Sans'), local('NotoSans'), url('./noto-sans-400-normal.eot#iefix') format('embedded-opentype'), url('./noto-sans-400-normal.woff') format('woff'), url('./noto-sans-400-normal.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./noto-sans-400-normal.eot');
|
||||
src: local('Noto Sans'), local('NotoSans'), url('./noto-sans-400-normal.eot#iefix') format('embedded-opentype'), url('./noto-sans-400-normal.woff') format('woff'), url('./noto-sans-400-normal.ttf') format('truetype');
|
||||
}
|
||||
|
|
|
@ -1,141 +1,141 @@
|
|||
## Access Control
|
||||
|
||||
This document describes the key features and concepts of the current state of
|
||||
access control in Mailtrain.
|
||||
|
||||
The current state provides user management and granular access control to reports
|
||||
and report templates. The user management supports both local authentication and
|
||||
LDAP-based authentication.
|
||||
|
||||
The access control has two abstractions levels: a high-level intended to be used through web UI,
|
||||
and low-level, intended to be configured once through the Mailtrain config file. The high-level
|
||||
layer serves for providing access to variuous resources, while the low-level layer is meant
|
||||
to define the access roles in Mailtrain to reflect an organisational or process hierarchy.
|
||||
|
||||
### High-level access management (through web UI)
|
||||
|
||||
On the high abstraction level, which is accessible to users via the web-based UI, Mailtrain
|
||||
recognizes different entities (reports, report templates, etc.) and user roles that regulate
|
||||
access to these entities (e.g. role "reporter" that allows viewing a report but prevents editing
|
||||
or deleting it). Access to entities is provided through so called "shares". A share is essentially
|
||||
a triple: entity - role - user.
|
||||
|
||||
Mailtrain further features hierarchical namespaces. Every entity has to reside in a namespace
|
||||
(in reality, the namespace itself is an entity to which access can be given).
|
||||
|
||||
While sharing an entity with a user gives the user access to the particular entity (in the
|
||||
scope of the role), sharing a namespace amounts to giving access to all entities within
|
||||
the namespace and transitively in all child namespaces. The role that regulates the access to the
|
||||
particular namespaces further determines the access to all different entity types that can
|
||||
reside in the namespace.
|
||||
|
||||
To simplify the management of permissions, every user is associated with one global role and
|
||||
a namespace. The global role regulates access to global resources and operations (i.e. those
|
||||
things that are not associated with any namespace). An example of such a global operation is
|
||||
rebuilding the permission cache. Further, the global role determines a default share of the
|
||||
root namespace and the namespace of the user. For example, an administrator's global role may
|
||||
specify that a user get administrator's role in the root namespace, which effectively gives
|
||||
him/her access to everything.
|
||||
|
||||
Mailtrain resets these default shares at start and also whenever permission cache is rebuilt
|
||||
(essentially every time user, namespace or some entity is created or when share or user's
|
||||
role is assigned). This effectively prevents deleting or overriding the default shares that
|
||||
the user has through the global role.
|
||||
|
||||
|
||||
### Low-level access management (through config file)
|
||||
|
||||
Internally, Mailtrain relies on fine-grained permissions, which are triplets:
|
||||
user - operation - entity (e.g. user id 1 - view - report id 2). These permissions are stored
|
||||
in a permission cache (in DB) and automatically generated at startup and whenever the permissions
|
||||
could have changed.
|
||||
|
||||
Mailtrain's config file defines the roles (available in the high-level access management) and
|
||||
specifies the mapping of roles to operations.
|
||||
|
||||
The roles are potentially different for each entity type/scope (currently global, namespace, report,
|
||||
report template). Each role defines the permitted operations for the given entity type/scope.
|
||||
A namespace role further defines allowed operations for entity types within and under the
|
||||
namespace.
|
||||
|
||||
The following defines the role master for scope "global". This effectively means that in
|
||||
"Create/Edit User" form, the user can be given role "Master".
|
||||
The role gives the permission to rebuild the permission cache.
|
||||
|
||||
Further, it specifies that the
|
||||
holder of the role will automatically be given access (share) to the root namespace in the
|
||||
namespace role "master" (specified by ```rootNamespaceRole="master"```). This access to the root namespace is given irrespective of the namespace
|
||||
in which the user is created. This highlight the dual purpose of namespaces: a) they group
|
||||
entities w.r.t. access management, b) they allow categorizing entities and users in a hierarchy
|
||||
to potentially reflect the organisational or process hierarchy. The latter is especially useful for
|
||||
more enterprise applications where a single installation of Mailtrain serves a number of rather
|
||||
independent groups.
|
||||
|
||||
The global role defined below is also an admin role (denoted by the ```admin=true```), which means that user id 1 will always be reset to this role.
|
||||
This serves as a kind of bootstrap that makes sure that there is always a user that can be
|
||||
used to give access to other users.
|
||||
```
|
||||
[roles.global.master]
|
||||
name="Master"
|
||||
admin=true
|
||||
description="All permissions"
|
||||
permissions=["rebuildPermissions"]
|
||||
rootNamespaceRole="master"
|
||||
```
|
||||
|
||||
Another example for a global role is the following. This one is intended for regular users.
|
||||
As such, it does not automatically give access to everything. Rather, it gives limited access
|
||||
to entities under the namespace in which the user has been created. This is specified by the
|
||||
```ownNamespaceRole="editor"```
|
||||
```
|
||||
[roles.global.editor]
|
||||
name="Editor"
|
||||
description="Anything under own namespace except operations related to sending and doing reports"
|
||||
permissions=[]
|
||||
ownNamespaceRole="editor"
|
||||
```
|
||||
|
||||
The roles for entities are defined in a similar fashion. The example below shows the definition
|
||||
of the role "master" for "report" entities. It lists the operations that a user
|
||||
that has "master" access to a particular report can do with the report. Note that to get the
|
||||
"master" access to a particular report through this role, the report would either have to be shared with the user
|
||||
with role "master".
|
||||
```
|
||||
[roles.report.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
```
|
||||
|
||||
The same for the restricted role "editor" can look as follows.
|
||||
```
|
||||
[roles.report.editor]
|
||||
name="Editor"
|
||||
description="Anything under own namespace except operations related to sending and doing reports"
|
||||
permissions=["view", "viewContent", "viewOutput"]
|
||||
```
|
||||
|
||||
The following defines the role "master" for "namespace" entities. Similarly to the example above,
|
||||
it lists operations that relate to a namespace. In particular all "create" operations pertain
|
||||
to a namespace rathen than to an entity, which at the time of creation does not exist yet.
|
||||
Additionally, the namespace roles define permissions to all entity types under the namespace
|
||||
(including child namespaces).
|
||||
```
|
||||
[roles.namespace.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
|
||||
|
||||
[roles.namespace.master.children]
|
||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
|
||||
```
|
||||
|
||||
And the same for the more restricted role "editor".
|
||||
```
|
||||
[roles.namespace.editor.children]
|
||||
reportTemplate=[]
|
||||
report=["view", "viewContent", "viewOutput"]
|
||||
namespace=["view", "edit", "delete"]
|
||||
## Access Control
|
||||
|
||||
This document describes the key features and concepts of the current state of
|
||||
access control in Mailtrain.
|
||||
|
||||
The current state provides user management and granular access control to reports
|
||||
and report templates. The user management supports both local authentication and
|
||||
LDAP-based authentication.
|
||||
|
||||
The access control has two abstractions levels: a high-level intended to be used through web UI,
|
||||
and low-level, intended to be configured once through the Mailtrain config file. The high-level
|
||||
layer serves for providing access to variuous resources, while the low-level layer is meant
|
||||
to define the access roles in Mailtrain to reflect an organisational or process hierarchy.
|
||||
|
||||
### High-level access management (through web UI)
|
||||
|
||||
On the high abstraction level, which is accessible to users via the web-based UI, Mailtrain
|
||||
recognizes different entities (reports, report templates, etc.) and user roles that regulate
|
||||
access to these entities (e.g. role "reporter" that allows viewing a report but prevents editing
|
||||
or deleting it). Access to entities is provided through so called "shares". A share is essentially
|
||||
a triple: entity - role - user.
|
||||
|
||||
Mailtrain further features hierarchical namespaces. Every entity has to reside in a namespace
|
||||
(in reality, the namespace itself is an entity to which access can be given).
|
||||
|
||||
While sharing an entity with a user gives the user access to the particular entity (in the
|
||||
scope of the role), sharing a namespace amounts to giving access to all entities within
|
||||
the namespace and transitively in all child namespaces. The role that regulates the access to the
|
||||
particular namespaces further determines the access to all different entity types that can
|
||||
reside in the namespace.
|
||||
|
||||
To simplify the management of permissions, every user is associated with one global role and
|
||||
a namespace. The global role regulates access to global resources and operations (i.e. those
|
||||
things that are not associated with any namespace). An example of such a global operation is
|
||||
rebuilding the permission cache. Further, the global role determines a default share of the
|
||||
root namespace and the namespace of the user. For example, an administrator's global role may
|
||||
specify that a user get administrator's role in the root namespace, which effectively gives
|
||||
him/her access to everything.
|
||||
|
||||
Mailtrain resets these default shares at start and also whenever permission cache is rebuilt
|
||||
(essentially every time user, namespace or some entity is created or when share or user's
|
||||
role is assigned). This effectively prevents deleting or overriding the default shares that
|
||||
the user has through the global role.
|
||||
|
||||
|
||||
### Low-level access management (through config file)
|
||||
|
||||
Internally, Mailtrain relies on fine-grained permissions, which are triplets:
|
||||
user - operation - entity (e.g. user id 1 - view - report id 2). These permissions are stored
|
||||
in a permission cache (in DB) and automatically generated at startup and whenever the permissions
|
||||
could have changed.
|
||||
|
||||
Mailtrain's config file defines the roles (available in the high-level access management) and
|
||||
specifies the mapping of roles to operations.
|
||||
|
||||
The roles are potentially different for each entity type/scope (currently global, namespace, report,
|
||||
report template). Each role defines the permitted operations for the given entity type/scope.
|
||||
A namespace role further defines allowed operations for entity types within and under the
|
||||
namespace.
|
||||
|
||||
The following defines the role master for scope "global". This effectively means that in
|
||||
"Create/Edit User" form, the user can be given role "Master".
|
||||
The role gives the permission to rebuild the permission cache.
|
||||
|
||||
Further, it specifies that the
|
||||
holder of the role will automatically be given access (share) to the root namespace in the
|
||||
namespace role "master" (specified by ```rootNamespaceRole="master"```). This access to the root namespace is given irrespective of the namespace
|
||||
in which the user is created. This highlight the dual purpose of namespaces: a) they group
|
||||
entities w.r.t. access management, b) they allow categorizing entities and users in a hierarchy
|
||||
to potentially reflect the organisational or process hierarchy. The latter is especially useful for
|
||||
more enterprise applications where a single installation of Mailtrain serves a number of rather
|
||||
independent groups.
|
||||
|
||||
The global role defined below is also an admin role (denoted by the ```admin=true```), which means that user id 1 will always be reset to this role.
|
||||
This serves as a kind of bootstrap that makes sure that there is always a user that can be
|
||||
used to give access to other users.
|
||||
```
|
||||
[roles.global.master]
|
||||
name="Master"
|
||||
admin=true
|
||||
description="All permissions"
|
||||
permissions=["rebuildPermissions"]
|
||||
rootNamespaceRole="master"
|
||||
```
|
||||
|
||||
Another example for a global role is the following. This one is intended for regular users.
|
||||
As such, it does not automatically give access to everything. Rather, it gives limited access
|
||||
to entities under the namespace in which the user has been created. This is specified by the
|
||||
```ownNamespaceRole="editor"```
|
||||
```
|
||||
[roles.global.editor]
|
||||
name="Editor"
|
||||
description="Anything under own namespace except operations related to sending and doing reports"
|
||||
permissions=[]
|
||||
ownNamespaceRole="editor"
|
||||
```
|
||||
|
||||
The roles for entities are defined in a similar fashion. The example below shows the definition
|
||||
of the role "master" for "report" entities. It lists the operations that a user
|
||||
that has "master" access to a particular report can do with the report. Note that to get the
|
||||
"master" access to a particular report through this role, the report would either have to be shared with the user
|
||||
with role "master".
|
||||
```
|
||||
[roles.report.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
```
|
||||
|
||||
The same for the restricted role "editor" can look as follows.
|
||||
```
|
||||
[roles.report.editor]
|
||||
name="Editor"
|
||||
description="Anything under own namespace except operations related to sending and doing reports"
|
||||
permissions=["view", "viewContent", "viewOutput"]
|
||||
```
|
||||
|
||||
The following defines the role "master" for "namespace" entities. Similarly to the example above,
|
||||
it lists operations that relate to a namespace. In particular all "create" operations pertain
|
||||
to a namespace rathen than to an entity, which at the time of creation does not exist yet.
|
||||
Additionally, the namespace roles define permissions to all entity types under the namespace
|
||||
(including child namespaces).
|
||||
```
|
||||
[roles.namespace.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
|
||||
|
||||
[roles.namespace.master.children]
|
||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createReportTemplate", "createReport", "manageUsers"]
|
||||
```
|
||||
|
||||
And the same for the more restricted role "editor".
|
||||
```
|
||||
[roles.namespace.editor.children]
|
||||
reportTemplate=[]
|
||||
report=["view", "viewContent", "viewOutput"]
|
||||
namespace=["view", "edit", "delete"]
|
||||
```
|
|
@ -1,363 +1,363 @@
|
|||
'use strict';
|
||||
|
||||
// Processes statements like these:
|
||||
// tUI(/*prefix:account*/'account.passwordChangeRequest', language)
|
||||
// /*prefix:helpers*/<Trans i18nKey="userMessagesUnread" count={count}>Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.</Trans>
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const klawSync = require('klaw-sync');
|
||||
const acorn = require("acorn");
|
||||
const acornJsx = require("acorn-jsx");
|
||||
const ellipsize = require('ellipsize');
|
||||
const camelCase = require('camelcase');
|
||||
const slugify = require('slugify');
|
||||
const readline = require('readline');
|
||||
|
||||
const localeFile = 'en-US/common.json';
|
||||
const searchDirs = [
|
||||
'../client/src',
|
||||
'../server',
|
||||
'../shared'
|
||||
];
|
||||
|
||||
function findAllVariantsByPrefixInDict(dict, keyPrefix) {
|
||||
const keyElems = keyPrefix.split('.');
|
||||
|
||||
for (const keyElem of keyElems.slice(0, -1)) {
|
||||
if (dict[keyElem]) {
|
||||
if (typeof dict[keyElem] === 'string') {
|
||||
return [];
|
||||
} else {
|
||||
dict = dict[keyElem];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = keyElems[keyElems.length - 1];
|
||||
const res = [];
|
||||
for (const key in dict) {
|
||||
if (key.startsWith(prefix)) {
|
||||
res.push(key.substring(prefix.length));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function findInDict(dict, key) {
|
||||
const keyElems = key.split('.');
|
||||
|
||||
for (const keyElem of keyElems.slice(0, -1)) {
|
||||
if (dict[keyElem]) {
|
||||
if (typeof dict[keyElem] === 'string') {
|
||||
return undefined;
|
||||
} else {
|
||||
dict = dict[keyElem];
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return dict[keyElems[keyElems.length - 1]];
|
||||
}
|
||||
|
||||
function setInDict(dict, key, value) {
|
||||
const keyElems = key.split('.');
|
||||
|
||||
for (const keyElem of keyElems.slice(0, -1)) {
|
||||
if (dict[keyElem]) {
|
||||
if (typeof dict[keyElem] === 'string') {
|
||||
throw new Error(`Overlapping key ${key}`);
|
||||
}
|
||||
} else {
|
||||
dict[keyElem] = {}
|
||||
}
|
||||
|
||||
dict = dict[keyElem];
|
||||
}
|
||||
|
||||
dict[keyElems[keyElems.length - 1]] = value;
|
||||
}
|
||||
|
||||
const assignedKeys = new Map();
|
||||
function getKeyFromValue(spec, value) {
|
||||
let key = value.replace(/<\/?[0-9]+>/g, ''); // Remove Trans markup
|
||||
key = slugify(key, { replacement: ' ', remove: /[\\()"':.,;\/\[\]\{\}*+-]/g, lower: false });
|
||||
key = camelCase(key);
|
||||
key = ellipsize(key, 40, {
|
||||
chars: [...Array(26)].map((_, i) => String.fromCharCode('A'.charCodeAt(0) + i)) /* This is an array of characters A-Z */,
|
||||
ellipse: ''
|
||||
});
|
||||
|
||||
if (spec.prefix) {
|
||||
key = spec.prefix + '.' + key;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
while (true) {
|
||||
const keyExt = key + (idx ? '-' + idx : '')
|
||||
if (assignedKeys.has(keyExt)) {
|
||||
if (assignedKeys.get(keyExt) === value) {
|
||||
return keyExt;
|
||||
}
|
||||
} else {
|
||||
assignedKeys.set(keyExt, value);
|
||||
return keyExt;
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
function allowedDirOrFile(item) {
|
||||
const pp = path.parse(item.path)
|
||||
|
||||
return (
|
||||
(item.stats.isDirectory() &&
|
||||
pp.base !== 'node_modules'
|
||||
) ||
|
||||
(item.stats.isFile() &&
|
||||
( pp.ext === '.js' || pp.ext === '.jsx' || pp.ext === '.hbs')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function parseSpec(specStr) {
|
||||
const spec = {};
|
||||
|
||||
if (specStr) {
|
||||
const entryMatcher = /([a-zA-Z]*)\s*:\s*(.*)/
|
||||
|
||||
const entries = specStr.split(/\s*,\s*/);
|
||||
for (const entry of entries) {
|
||||
const elems = entry.match(entryMatcher);
|
||||
if (elems) {
|
||||
spec[elems[1]] = elems[2];
|
||||
} else {
|
||||
spec[entry] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
// see http://blog.stevenlevithan.com/archives/match-quoted-string
|
||||
const tMatcher = /(^|[ {+(=.\[])((?:tUI|tLog|t|tMark)\s*\(\s*(?:\/\*(.*?)\*\/)?\s*)(["'])((?:(?!\4)[^\\]|\\.)*)(\4)/;
|
||||
const jsxTransMatcher = /(\/\*(.*?)\*\/\s*)?(\<Trans[ >][\s\S]*?\<\/Trans\>)/;
|
||||
const hbsTranslateMatcher = /(\{\{!--(.*?)--\}\}\s*)?(\{\{#translate\}\})([\s\S]*?)(\{\{\/translate\}\})/;
|
||||
|
||||
const jsxParser = acorn.Parser.extend(acornJsx());
|
||||
function parseJsxTrans(fragment) {
|
||||
const match = fragment.match(jsxTransMatcher);
|
||||
const spec = parseSpec(match[2]);
|
||||
const jsxStr = match[3];
|
||||
|
||||
const jsxStrSmpl = jsxStr.replace('{::', '{ '); // Acorn does not handle bind (::) operator. So we just leave it out because we are not interested in the code anyway.
|
||||
const ast = jsxParser.parse(jsxStrSmpl);
|
||||
|
||||
function convertChildren(children) {
|
||||
const entries = [];
|
||||
let childNo = 0;
|
||||
|
||||
for (const child of children) {
|
||||
const type = child.type;
|
||||
|
||||
if (type === 'JSXText') {
|
||||
entries.push(child.value);
|
||||
childNo++;
|
||||
|
||||
} else if (type === 'JSXElement') {
|
||||
const inner = convertChildren(child.children);
|
||||
entries.push(`<${childNo}>${convertChildren(child.children)}</${childNo}>`);
|
||||
childNo++;
|
||||
|
||||
} else if (type === 'JSXExpressionContainer') {
|
||||
entries.push(jsxStr.substring(child.start, child.end));
|
||||
childNo++;
|
||||
|
||||
} else {
|
||||
throw new Error('Unknown JSX node: ' + child);
|
||||
}
|
||||
}
|
||||
|
||||
return entries.join('');
|
||||
}
|
||||
|
||||
const expr = ast.body[0].expression;
|
||||
|
||||
let originalKey;
|
||||
for (const attr of expr.openingElement.attributes) {
|
||||
const name = attr.name.name;
|
||||
if (name === 'i18nKey') {
|
||||
originalKey = attr.value.value;
|
||||
}
|
||||
}
|
||||
|
||||
const convValue = convertChildren(expr.children);
|
||||
|
||||
if (originalKey === undefined) {
|
||||
originalKey = convValue;
|
||||
}
|
||||
|
||||
let value;
|
||||
const originalValue = findInDict(originalResDict, originalKey);
|
||||
|
||||
if (originalValue === undefined) {
|
||||
value = convValue;
|
||||
} else {
|
||||
value = originalValue;
|
||||
}
|
||||
|
||||
const key = getKeyFromValue(spec, value);
|
||||
|
||||
const replacement = `${match[1] || ''}<Trans i18nKey="${key}">${jsxStr.substring(expr.openingElement.end, expr.closingElement.start)}</Trans>`;
|
||||
|
||||
return { key, originalKey, value, replacement };
|
||||
}
|
||||
|
||||
|
||||
function parseHbsTranslate(fragment) {
|
||||
const match = fragment.match(hbsTranslateMatcher);
|
||||
const spec = parseSpec(match[2]);
|
||||
const originalKey = match[4];
|
||||
|
||||
let value;
|
||||
const originalValue = findInDict(originalResDict, originalKey);
|
||||
|
||||
if (originalValue === undefined) {
|
||||
value = originalKey;
|
||||
} else {
|
||||
value = originalValue;
|
||||
}
|
||||
|
||||
const key = getKeyFromValue(spec, value);
|
||||
|
||||
const replacement = `${match[1] || ''}${match[3]}${key}${match[5]}`;
|
||||
|
||||
return { key, originalKey, value, replacement };
|
||||
}
|
||||
|
||||
function parseT(fragment) {
|
||||
const match = fragment.match(tMatcher);
|
||||
|
||||
const originalKey = match[5];
|
||||
const spec = parseSpec(match[3]);
|
||||
|
||||
if (spec.ignore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// console.log(`${fragment}`);
|
||||
// console.log(` |${match[1]}|${match[2]}|${match[4]}|${match[5]}|${match[6]}| - ${JSON.stringify(spec)}`);
|
||||
|
||||
let value;
|
||||
const originalValue = findInDict(originalResDict, originalKey);
|
||||
|
||||
if (originalValue === undefined) {
|
||||
value = originalKey;
|
||||
} else {
|
||||
value = originalValue;
|
||||
}
|
||||
|
||||
const key = getKeyFromValue(spec, value);
|
||||
|
||||
const replacement = `${match[1]}${match[2]}${match[4]}${key}${match[6]}`;
|
||||
|
||||
return { key, originalKey, value, originalValue, replacement };
|
||||
}
|
||||
|
||||
const renamedKeys = new Map();
|
||||
const resDict = {};
|
||||
let anyUpdatesToResDict = false;
|
||||
|
||||
function processFile(file) {
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
let anyUpdates = false;
|
||||
|
||||
function update(fragments, parseFun) {
|
||||
if (fragments) {
|
||||
for (const fragment of fragments) {
|
||||
const parseStruct = parseFun(fragment);
|
||||
if (parseStruct) {
|
||||
const {key, originalKey, value, originalValue, replacement} = parseStruct;
|
||||
// console.log(`${key} <- ${originalKey} | ${value} <- ${originalValue} | ${fragment} -> ${replacement}`);
|
||||
|
||||
source = source.split(fragment).join(replacement);
|
||||
setInDict(resDict, key, value);
|
||||
|
||||
const variants = originalKey ? findAllVariantsByPrefixInDict(originalResDict, originalKey + '_') : [];
|
||||
for (const variant of variants) {
|
||||
setInDict(resDict, key + '_' + variant, findInDict(originalResDict, originalKey + '_' + variant));
|
||||
}
|
||||
|
||||
if (originalKey !== key) {
|
||||
renamedKeys.set(originalKey, key);
|
||||
|
||||
for (const variant of variants) {
|
||||
renamedKeys.set(originalKey + '_' + variant, key + '_' + variant);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalKey !== key || originalValue !== value) {
|
||||
anyUpdates = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lines = source.split(/\r?\n/g);
|
||||
for (const line of lines) {
|
||||
const fragments = line.match(new RegExp(tMatcher, 'g'));
|
||||
update(fragments, parseT);
|
||||
}
|
||||
|
||||
const hbsFragments = source.match(new RegExp(hbsTranslateMatcher, 'g'));
|
||||
update(hbsFragments, parseHbsTranslate);
|
||||
|
||||
const jsxFragments = source.match(new RegExp(jsxTransMatcher, 'g'));
|
||||
update(jsxFragments, parseJsxTrans);
|
||||
|
||||
if (anyUpdates) {
|
||||
console.log(`Updating ${file}`);
|
||||
fs.writeFileSync(file, source);
|
||||
|
||||
anyUpdatesToResDict = true;
|
||||
}
|
||||
}
|
||||
|
||||
const originalResDict = JSON.parse(fs.readFileSync(localeFile));
|
||||
|
||||
function run() {
|
||||
for (const dir of searchDirs) {
|
||||
const files = klawSync(dir, { nodir: true, filter: allowedDirOrFile })
|
||||
|
||||
for (const file of files) {
|
||||
processFile(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (anyUpdatesToResDict) {
|
||||
console.log(`Updating ${localeFile}`);
|
||||
fs.writeFileSync(localeFile, JSON.stringify(resDict, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log('This script does modifications in the source tree. You should first commit all your files in git before proceeding.');
|
||||
rl.question('To proceed type YES: ', (answer) => {
|
||||
if (answer === 'YES') {
|
||||
run();
|
||||
}
|
||||
|
||||
rl.close();
|
||||
});
|
||||
'use strict';
|
||||
|
||||
// Processes statements like these:
|
||||
// tUI(/*prefix:account*/'account.passwordChangeRequest', language)
|
||||
// /*prefix:helpers*/<Trans i18nKey="userMessagesUnread" count={count}>Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.</Trans>
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const klawSync = require('klaw-sync');
|
||||
const acorn = require("acorn");
|
||||
const acornJsx = require("acorn-jsx");
|
||||
const ellipsize = require('ellipsize');
|
||||
const camelCase = require('camelcase');
|
||||
const slugify = require('slugify');
|
||||
const readline = require('readline');
|
||||
|
||||
const localeFile = 'en-US/common.json';
|
||||
const searchDirs = [
|
||||
'../client/src',
|
||||
'../server',
|
||||
'../shared'
|
||||
];
|
||||
|
||||
function findAllVariantsByPrefixInDict(dict, keyPrefix) {
|
||||
const keyElems = keyPrefix.split('.');
|
||||
|
||||
for (const keyElem of keyElems.slice(0, -1)) {
|
||||
if (dict[keyElem]) {
|
||||
if (typeof dict[keyElem] === 'string') {
|
||||
return [];
|
||||
} else {
|
||||
dict = dict[keyElem];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = keyElems[keyElems.length - 1];
|
||||
const res = [];
|
||||
for (const key in dict) {
|
||||
if (key.startsWith(prefix)) {
|
||||
res.push(key.substring(prefix.length));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function findInDict(dict, key) {
|
||||
const keyElems = key.split('.');
|
||||
|
||||
for (const keyElem of keyElems.slice(0, -1)) {
|
||||
if (dict[keyElem]) {
|
||||
if (typeof dict[keyElem] === 'string') {
|
||||
return undefined;
|
||||
} else {
|
||||
dict = dict[keyElem];
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return dict[keyElems[keyElems.length - 1]];
|
||||
}
|
||||
|
||||
function setInDict(dict, key, value) {
|
||||
const keyElems = key.split('.');
|
||||
|
||||
for (const keyElem of keyElems.slice(0, -1)) {
|
||||
if (dict[keyElem]) {
|
||||
if (typeof dict[keyElem] === 'string') {
|
||||
throw new Error(`Overlapping key ${key}`);
|
||||
}
|
||||
} else {
|
||||
dict[keyElem] = {}
|
||||
}
|
||||
|
||||
dict = dict[keyElem];
|
||||
}
|
||||
|
||||
dict[keyElems[keyElems.length - 1]] = value;
|
||||
}
|
||||
|
||||
const assignedKeys = new Map();
|
||||
function getKeyFromValue(spec, value) {
|
||||
let key = value.replace(/<\/?[0-9]+>/g, ''); // Remove Trans markup
|
||||
key = slugify(key, { replacement: ' ', remove: /[\\()"':.,;\/\[\]\{\}*+-]/g, lower: false });
|
||||
key = camelCase(key);
|
||||
key = ellipsize(key, 40, {
|
||||
chars: [...Array(26)].map((_, i) => String.fromCharCode('A'.charCodeAt(0) + i)) /* This is an array of characters A-Z */,
|
||||
ellipse: ''
|
||||
});
|
||||
|
||||
if (spec.prefix) {
|
||||
key = spec.prefix + '.' + key;
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
while (true) {
|
||||
const keyExt = key + (idx ? '-' + idx : '')
|
||||
if (assignedKeys.has(keyExt)) {
|
||||
if (assignedKeys.get(keyExt) === value) {
|
||||
return keyExt;
|
||||
}
|
||||
} else {
|
||||
assignedKeys.set(keyExt, value);
|
||||
return keyExt;
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
function allowedDirOrFile(item) {
|
||||
const pp = path.parse(item.path)
|
||||
|
||||
return (
|
||||
(item.stats.isDirectory() &&
|
||||
pp.base !== 'node_modules'
|
||||
) ||
|
||||
(item.stats.isFile() &&
|
||||
( pp.ext === '.js' || pp.ext === '.jsx' || pp.ext === '.hbs')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function parseSpec(specStr) {
|
||||
const spec = {};
|
||||
|
||||
if (specStr) {
|
||||
const entryMatcher = /([a-zA-Z]*)\s*:\s*(.*)/
|
||||
|
||||
const entries = specStr.split(/\s*,\s*/);
|
||||
for (const entry of entries) {
|
||||
const elems = entry.match(entryMatcher);
|
||||
if (elems) {
|
||||
spec[elems[1]] = elems[2];
|
||||
} else {
|
||||
spec[entry] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
// see http://blog.stevenlevithan.com/archives/match-quoted-string
|
||||
const tMatcher = /(^|[ {+(=.\[])((?:tUI|tLog|t|tMark)\s*\(\s*(?:\/\*(.*?)\*\/)?\s*)(["'])((?:(?!\4)[^\\]|\\.)*)(\4)/;
|
||||
const jsxTransMatcher = /(\/\*(.*?)\*\/\s*)?(\<Trans[ >][\s\S]*?\<\/Trans\>)/;
|
||||
const hbsTranslateMatcher = /(\{\{!--(.*?)--\}\}\s*)?(\{\{#translate\}\})([\s\S]*?)(\{\{\/translate\}\})/;
|
||||
|
||||
const jsxParser = acorn.Parser.extend(acornJsx());
|
||||
function parseJsxTrans(fragment) {
|
||||
const match = fragment.match(jsxTransMatcher);
|
||||
const spec = parseSpec(match[2]);
|
||||
const jsxStr = match[3];
|
||||
|
||||
const jsxStrSmpl = jsxStr.replace('{::', '{ '); // Acorn does not handle bind (::) operator. So we just leave it out because we are not interested in the code anyway.
|
||||
const ast = jsxParser.parse(jsxStrSmpl);
|
||||
|
||||
function convertChildren(children) {
|
||||
const entries = [];
|
||||
let childNo = 0;
|
||||
|
||||
for (const child of children) {
|
||||
const type = child.type;
|
||||
|
||||
if (type === 'JSXText') {
|
||||
entries.push(child.value);
|
||||
childNo++;
|
||||
|
||||
} else if (type === 'JSXElement') {
|
||||
const inner = convertChildren(child.children);
|
||||
entries.push(`<${childNo}>${convertChildren(child.children)}</${childNo}>`);
|
||||
childNo++;
|
||||
|
||||
} else if (type === 'JSXExpressionContainer') {
|
||||
entries.push(jsxStr.substring(child.start, child.end));
|
||||
childNo++;
|
||||
|
||||
} else {
|
||||
throw new Error('Unknown JSX node: ' + child);
|
||||
}
|
||||
}
|
||||
|
||||
return entries.join('');
|
||||
}
|
||||
|
||||
const expr = ast.body[0].expression;
|
||||
|
||||
let originalKey;
|
||||
for (const attr of expr.openingElement.attributes) {
|
||||
const name = attr.name.name;
|
||||
if (name === 'i18nKey') {
|
||||
originalKey = attr.value.value;
|
||||
}
|
||||
}
|
||||
|
||||
const convValue = convertChildren(expr.children);
|
||||
|
||||
if (originalKey === undefined) {
|
||||
originalKey = convValue;
|
||||
}
|
||||
|
||||
let value;
|
||||
const originalValue = findInDict(originalResDict, originalKey);
|
||||
|
||||
if (originalValue === undefined) {
|
||||
value = convValue;
|
||||
} else {
|
||||
value = originalValue;
|
||||
}
|
||||
|
||||
const key = getKeyFromValue(spec, value);
|
||||
|
||||
const replacement = `${match[1] || ''}<Trans i18nKey="${key}">${jsxStr.substring(expr.openingElement.end, expr.closingElement.start)}</Trans>`;
|
||||
|
||||
return { key, originalKey, value, replacement };
|
||||
}
|
||||
|
||||
|
||||
function parseHbsTranslate(fragment) {
|
||||
const match = fragment.match(hbsTranslateMatcher);
|
||||
const spec = parseSpec(match[2]);
|
||||
const originalKey = match[4];
|
||||
|
||||
let value;
|
||||
const originalValue = findInDict(originalResDict, originalKey);
|
||||
|
||||
if (originalValue === undefined) {
|
||||
value = originalKey;
|
||||
} else {
|
||||
value = originalValue;
|
||||
}
|
||||
|
||||
const key = getKeyFromValue(spec, value);
|
||||
|
||||
const replacement = `${match[1] || ''}${match[3]}${key}${match[5]}`;
|
||||
|
||||
return { key, originalKey, value, replacement };
|
||||
}
|
||||
|
||||
function parseT(fragment) {
|
||||
const match = fragment.match(tMatcher);
|
||||
|
||||
const originalKey = match[5];
|
||||
const spec = parseSpec(match[3]);
|
||||
|
||||
if (spec.ignore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// console.log(`${fragment}`);
|
||||
// console.log(` |${match[1]}|${match[2]}|${match[4]}|${match[5]}|${match[6]}| - ${JSON.stringify(spec)}`);
|
||||
|
||||
let value;
|
||||
const originalValue = findInDict(originalResDict, originalKey);
|
||||
|
||||
if (originalValue === undefined) {
|
||||
value = originalKey;
|
||||
} else {
|
||||
value = originalValue;
|
||||
}
|
||||
|
||||
const key = getKeyFromValue(spec, value);
|
||||
|
||||
const replacement = `${match[1]}${match[2]}${match[4]}${key}${match[6]}`;
|
||||
|
||||
return { key, originalKey, value, originalValue, replacement };
|
||||
}
|
||||
|
||||
const renamedKeys = new Map();
|
||||
const resDict = {};
|
||||
let anyUpdatesToResDict = false;
|
||||
|
||||
function processFile(file) {
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
let anyUpdates = false;
|
||||
|
||||
function update(fragments, parseFun) {
|
||||
if (fragments) {
|
||||
for (const fragment of fragments) {
|
||||
const parseStruct = parseFun(fragment);
|
||||
if (parseStruct) {
|
||||
const {key, originalKey, value, originalValue, replacement} = parseStruct;
|
||||
// console.log(`${key} <- ${originalKey} | ${value} <- ${originalValue} | ${fragment} -> ${replacement}`);
|
||||
|
||||
source = source.split(fragment).join(replacement);
|
||||
setInDict(resDict, key, value);
|
||||
|
||||
const variants = originalKey ? findAllVariantsByPrefixInDict(originalResDict, originalKey + '_') : [];
|
||||
for (const variant of variants) {
|
||||
setInDict(resDict, key + '_' + variant, findInDict(originalResDict, originalKey + '_' + variant));
|
||||
}
|
||||
|
||||
if (originalKey !== key) {
|
||||
renamedKeys.set(originalKey, key);
|
||||
|
||||
for (const variant of variants) {
|
||||
renamedKeys.set(originalKey + '_' + variant, key + '_' + variant);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalKey !== key || originalValue !== value) {
|
||||
anyUpdates = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lines = source.split(/\r?\n/g);
|
||||
for (const line of lines) {
|
||||
const fragments = line.match(new RegExp(tMatcher, 'g'));
|
||||
update(fragments, parseT);
|
||||
}
|
||||
|
||||
const hbsFragments = source.match(new RegExp(hbsTranslateMatcher, 'g'));
|
||||
update(hbsFragments, parseHbsTranslate);
|
||||
|
||||
const jsxFragments = source.match(new RegExp(jsxTransMatcher, 'g'));
|
||||
update(jsxFragments, parseJsxTrans);
|
||||
|
||||
if (anyUpdates) {
|
||||
console.log(`Updating ${file}`);
|
||||
fs.writeFileSync(file, source);
|
||||
|
||||
anyUpdatesToResDict = true;
|
||||
}
|
||||
}
|
||||
|
||||
const originalResDict = JSON.parse(fs.readFileSync(localeFile));
|
||||
|
||||
function run() {
|
||||
for (const dir of searchDirs) {
|
||||
const files = klawSync(dir, { nodir: true, filter: allowedDirOrFile })
|
||||
|
||||
for (const file of files) {
|
||||
processFile(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (anyUpdatesToResDict) {
|
||||
console.log(`Updating ${localeFile}`);
|
||||
fs.writeFileSync(localeFile, JSON.stringify(resDict, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
console.log('This script does modifications in the source tree. You should first commit all your files in git before proceeding.');
|
||||
rl.question('To proceed type YES: ', (answer) => {
|
||||
if (answer === 'YES') {
|
||||
run();
|
||||
}
|
||||
|
||||
rl.close();
|
||||
});
|
||||
|
|
2
mvis/client/.gitignore
vendored
2
mvis/client/.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
/dist
|
||||
/dist
|
||||
/node_modules
|
4
mvis/server/.gitignore
vendored
4
mvis/server/.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
/node_modules
|
||||
/config/development.yaml
|
||||
/node_modules
|
||||
/config/development.yaml
|
||||
|
|
|
@ -1,77 +1,77 @@
|
|||
mysql:
|
||||
host: localhost
|
||||
user: mvis
|
||||
password: mvis
|
||||
database: mvis
|
||||
|
||||
mailtrain:
|
||||
url: http://localhost:3000/
|
||||
namespaces:
|
||||
campaigns: 2
|
||||
userRole: mailtrainUser
|
||||
|
||||
www:
|
||||
# HTTP interface to listen on
|
||||
host: 127.0.0.1
|
||||
# HTTP(S) port to listen on
|
||||
trustedPort: 3010
|
||||
trustedPortIsHttps: false
|
||||
sandboxPort: 3011
|
||||
sandboxPortIsHttps: false
|
||||
apiPort: 3012
|
||||
apiPortIsHttps: false
|
||||
|
||||
trustedUrlBase: http://localhost:3010
|
||||
sandboxUrlBase: http://localhost:3011
|
||||
|
||||
|
||||
roles:
|
||||
global:
|
||||
mailtrainUser:
|
||||
name: "Mailtrain User"
|
||||
admin: true
|
||||
description: "Limited permissions that allow only read-only access"
|
||||
permissions:
|
||||
|
||||
namespace:
|
||||
mailtrainUser:
|
||||
name: "Mailtrain User"
|
||||
description: "Limited permissions that allow only read-only access"
|
||||
permissions: ["view"]
|
||||
children:
|
||||
namespace: ["view"]
|
||||
template: ["view", "viewFiles", "execute"]
|
||||
workspace: ["view"]
|
||||
panel: ["view"]
|
||||
signal: ["view", "query"]
|
||||
signalSet: ["view", "query"]
|
||||
|
||||
# template:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view", "viewFiles", "execute"]
|
||||
#
|
||||
# workspace:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view"]
|
||||
#
|
||||
# panel:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view"]
|
||||
#
|
||||
# signal:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view", "query"]
|
||||
#
|
||||
# signalSet:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view", "query"]
|
||||
mysql:
|
||||
host: localhost
|
||||
user: mvis
|
||||
password: mvis
|
||||
database: mvis
|
||||
|
||||
mailtrain:
|
||||
url: http://localhost:3000/
|
||||
namespaces:
|
||||
campaigns: 2
|
||||
userRole: mailtrainUser
|
||||
|
||||
www:
|
||||
# HTTP interface to listen on
|
||||
host: 127.0.0.1
|
||||
# HTTP(S) port to listen on
|
||||
trustedPort: 3010
|
||||
trustedPortIsHttps: false
|
||||
sandboxPort: 3011
|
||||
sandboxPortIsHttps: false
|
||||
apiPort: 3012
|
||||
apiPortIsHttps: false
|
||||
|
||||
trustedUrlBase: http://localhost:3010
|
||||
sandboxUrlBase: http://localhost:3011
|
||||
|
||||
|
||||
roles:
|
||||
global:
|
||||
mailtrainUser:
|
||||
name: "Mailtrain User"
|
||||
admin: true
|
||||
description: "Limited permissions that allow only read-only access"
|
||||
permissions:
|
||||
|
||||
namespace:
|
||||
mailtrainUser:
|
||||
name: "Mailtrain User"
|
||||
description: "Limited permissions that allow only read-only access"
|
||||
permissions: ["view"]
|
||||
children:
|
||||
namespace: ["view"]
|
||||
template: ["view", "viewFiles", "execute"]
|
||||
workspace: ["view"]
|
||||
panel: ["view"]
|
||||
signal: ["view", "query"]
|
||||
signalSet: ["view", "query"]
|
||||
|
||||
# template:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view", "viewFiles", "execute"]
|
||||
#
|
||||
# workspace:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view"]
|
||||
#
|
||||
# panel:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view"]
|
||||
#
|
||||
# signal:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view", "query"]
|
||||
#
|
||||
# signalSet:
|
||||
# mailtrainUser:
|
||||
# name: "Mailtrain User"
|
||||
# description: "Limited permissions that allow only read-only access"
|
||||
# permissions: ["view", "query"]
|
||||
|
|
4
mvis/test-embed/.gitignore
vendored
4
mvis/test-embed/.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
/dist
|
||||
/node_modules
|
||||
/dist
|
||||
/node_modules
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<div id="evif-panel" />
|
||||
|
||||
<script type="text/javascript" src="/ivis.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
IVIS.embedPanel('evif-panel', '{{ivisSandboxUrlBase}}', {{panelId}}, '{{token}}');
|
||||
</script>
|
||||
|
||||
<div id="evif-panel" />
|
||||
|
||||
<script type="text/javascript" src="/ivis.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
IVIS.embedPanel('evif-panel', '{{ivisSandboxUrlBase}}', {{panelId}}, '{{token}}');
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
function convertFileURLs(sourceCustom, fromEntityType, fromEntityId, toEntityType, toEntityId) {
|
||||
|
||||
function convertText(text) {
|
||||
if (text) {
|
||||
const fromUrl = `/files/${fromEntityType}/file/${fromEntityId}`;
|
||||
const toUrl = `/files/${toEntityType}/file/${toEntityId}`;
|
||||
|
||||
const encodedFromUrl = encodeURIComponent(fromUrl);
|
||||
const encodedToUrl = encodeURIComponent(toUrl);
|
||||
|
||||
text = text.split('[URL_BASE]' + fromUrl).join('[URL_BASE]' + toUrl);
|
||||
text = text.split('[SANDBOX_URL_BASE]' + fromUrl).join('[SANDBOX_URL_BASE]' + toUrl);
|
||||
text = text.split('[ENCODED_URL_BASE]' + encodedFromUrl).join('[ENCODED_URL_BASE]' + encodedToUrl);
|
||||
text = text.split('[ENCODED_SANDBOX_URL_BASE]' + encodedFromUrl).join('[ENCODED_SANDBOX_URL_BASE]' + encodedToUrl);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
sourceCustom.html = convertText(sourceCustom.html);
|
||||
sourceCustom.text = convertText(sourceCustom.text);
|
||||
|
||||
if (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
|
||||
sourceCustom.data.model = convertText(sourceCustom.data.model);
|
||||
sourceCustom.data.model = convertText(sourceCustom.data.model);
|
||||
sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
|
||||
}
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
function convertFileURLs(sourceCustom, fromEntityType, fromEntityId, toEntityType, toEntityId) {
|
||||
|
||||
function convertText(text) {
|
||||
if (text) {
|
||||
const fromUrl = `/files/${fromEntityType}/file/${fromEntityId}`;
|
||||
const toUrl = `/files/${toEntityType}/file/${toEntityId}`;
|
||||
|
||||
const encodedFromUrl = encodeURIComponent(fromUrl);
|
||||
const encodedToUrl = encodeURIComponent(toUrl);
|
||||
|
||||
text = text.split('[URL_BASE]' + fromUrl).join('[URL_BASE]' + toUrl);
|
||||
text = text.split('[SANDBOX_URL_BASE]' + fromUrl).join('[SANDBOX_URL_BASE]' + toUrl);
|
||||
text = text.split('[ENCODED_URL_BASE]' + encodedFromUrl).join('[ENCODED_URL_BASE]' + encodedToUrl);
|
||||
text = text.split('[ENCODED_SANDBOX_URL_BASE]' + encodedFromUrl).join('[ENCODED_SANDBOX_URL_BASE]' + encodedToUrl);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
sourceCustom.html = convertText(sourceCustom.html);
|
||||
sourceCustom.text = convertText(sourceCustom.text);
|
||||
|
||||
if (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
|
||||
sourceCustom.data.model = convertText(sourceCustom.data.model);
|
||||
sourceCustom.data.model = convertText(sourceCustom.data.model);
|
||||
sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.convertFileURLs = convertFileURLs;
|
|
@ -1,151 +1,151 @@
|
|||
'use strict';
|
||||
|
||||
const ReplacementBehavior = {
|
||||
NONE: 1,
|
||||
REPLACE: 2,
|
||||
RENAME: 3
|
||||
};
|
||||
|
||||
const entityTypes = {
|
||||
namespace: {
|
||||
entitiesTable: 'namespaces',
|
||||
sharesTable: 'shares_namespace',
|
||||
permissionsTable: 'permissions_namespace',
|
||||
clientLink: id => `/namespaces/${id}`
|
||||
},
|
||||
list: {
|
||||
entitiesTable: 'lists',
|
||||
sharesTable: 'shares_list',
|
||||
permissionsTable: 'permissions_list',
|
||||
clientLink: id => `/lists/${id}`
|
||||
},
|
||||
customForm: {
|
||||
entitiesTable: 'custom_forms',
|
||||
sharesTable: 'shares_custom_form',
|
||||
permissionsTable: 'permissions_custom_form',
|
||||
clientLink: id => `/lists/forms/${id}`
|
||||
},
|
||||
campaign: {
|
||||
entitiesTable: 'campaigns',
|
||||
sharesTable: 'shares_campaign',
|
||||
permissionsTable: 'permissions_campaign',
|
||||
dependentPermissions: {
|
||||
extraColumns: ['parent'],
|
||||
getParent: entity => entity.parent
|
||||
},
|
||||
files: {
|
||||
file: {
|
||||
table: 'files_campaign_file',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
},
|
||||
attachment: {
|
||||
table: 'files_campaign_attachment',
|
||||
permissions: {
|
||||
view: 'viewAttachments',
|
||||
manage: 'manageAttachments'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.NONE
|
||||
}
|
||||
},
|
||||
clientLink: id => `/campaigns/${id}`
|
||||
},
|
||||
template: {
|
||||
entitiesTable: 'templates',
|
||||
sharesTable: 'shares_template',
|
||||
permissionsTable: 'permissions_template',
|
||||
files: {
|
||||
file: {
|
||||
table: 'files_template_file',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
}
|
||||
},
|
||||
clientLink: id => `/templates/${id}`
|
||||
},
|
||||
sendConfiguration: {
|
||||
entitiesTable: 'send_configurations',
|
||||
sharesTable: 'shares_send_configuration',
|
||||
permissionsTable: 'permissions_send_configuration',
|
||||
clientLink: id => `/send-configurations/${id}`
|
||||
},
|
||||
report: {
|
||||
entitiesTable: 'reports',
|
||||
sharesTable: 'shares_report',
|
||||
permissionsTable: 'permissions_report',
|
||||
clientLink: id => `/reports/${id}`
|
||||
},
|
||||
reportTemplate: {
|
||||
entitiesTable: 'report_templates',
|
||||
sharesTable: 'shares_report_template',
|
||||
permissionsTable: 'permissions_report_template',
|
||||
clientLink: id => `/reports/templates/${id}`
|
||||
},
|
||||
mosaicoTemplate: {
|
||||
entitiesTable: 'mosaico_templates',
|
||||
sharesTable: 'shares_mosaico_template',
|
||||
permissionsTable: 'permissions_mosaico_template',
|
||||
files: {
|
||||
file: {
|
||||
table: 'files_mosaico_template_file',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
},
|
||||
block: {
|
||||
table: 'files_mosaico_template_block',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
}
|
||||
},
|
||||
clientLink: id => `/templates/mosaico/${id}`
|
||||
},
|
||||
user: {
|
||||
entitiesTable: 'users',
|
||||
clientLink: id => `/users/${id}`
|
||||
}
|
||||
};
|
||||
|
||||
const entityTypesWithPermissions = {};
|
||||
for (const key in entityTypes) {
|
||||
if (entityTypes[key].permissionsTable) {
|
||||
entityTypesWithPermissions[key] = entityTypes[key];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getEntityTypes() {
|
||||
return entityTypes;
|
||||
}
|
||||
|
||||
function getEntityTypesWithPermissions() {
|
||||
return entityTypesWithPermissions;
|
||||
}
|
||||
|
||||
function getEntityType(entityTypeId) {
|
||||
const entityType = entityTypes[entityTypeId];
|
||||
|
||||
if (!entityType) {
|
||||
throw new Error(`Unknown entity type ${entityTypeId}`);
|
||||
}
|
||||
|
||||
return entityType
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEntityTypes,
|
||||
getEntityTypesWithPermissions,
|
||||
getEntityType,
|
||||
ReplacementBehavior
|
||||
'use strict';
|
||||
|
||||
const ReplacementBehavior = {
|
||||
NONE: 1,
|
||||
REPLACE: 2,
|
||||
RENAME: 3
|
||||
};
|
||||
|
||||
const entityTypes = {
|
||||
namespace: {
|
||||
entitiesTable: 'namespaces',
|
||||
sharesTable: 'shares_namespace',
|
||||
permissionsTable: 'permissions_namespace',
|
||||
clientLink: id => `/namespaces/${id}`
|
||||
},
|
||||
list: {
|
||||
entitiesTable: 'lists',
|
||||
sharesTable: 'shares_list',
|
||||
permissionsTable: 'permissions_list',
|
||||
clientLink: id => `/lists/${id}`
|
||||
},
|
||||
customForm: {
|
||||
entitiesTable: 'custom_forms',
|
||||
sharesTable: 'shares_custom_form',
|
||||
permissionsTable: 'permissions_custom_form',
|
||||
clientLink: id => `/lists/forms/${id}`
|
||||
},
|
||||
campaign: {
|
||||
entitiesTable: 'campaigns',
|
||||
sharesTable: 'shares_campaign',
|
||||
permissionsTable: 'permissions_campaign',
|
||||
dependentPermissions: {
|
||||
extraColumns: ['parent'],
|
||||
getParent: entity => entity.parent
|
||||
},
|
||||
files: {
|
||||
file: {
|
||||
table: 'files_campaign_file',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
},
|
||||
attachment: {
|
||||
table: 'files_campaign_attachment',
|
||||
permissions: {
|
||||
view: 'viewAttachments',
|
||||
manage: 'manageAttachments'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.NONE
|
||||
}
|
||||
},
|
||||
clientLink: id => `/campaigns/${id}`
|
||||
},
|
||||
template: {
|
||||
entitiesTable: 'templates',
|
||||
sharesTable: 'shares_template',
|
||||
permissionsTable: 'permissions_template',
|
||||
files: {
|
||||
file: {
|
||||
table: 'files_template_file',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
}
|
||||
},
|
||||
clientLink: id => `/templates/${id}`
|
||||
},
|
||||
sendConfiguration: {
|
||||
entitiesTable: 'send_configurations',
|
||||
sharesTable: 'shares_send_configuration',
|
||||
permissionsTable: 'permissions_send_configuration',
|
||||
clientLink: id => `/send-configurations/${id}`
|
||||
},
|
||||
report: {
|
||||
entitiesTable: 'reports',
|
||||
sharesTable: 'shares_report',
|
||||
permissionsTable: 'permissions_report',
|
||||
clientLink: id => `/reports/${id}`
|
||||
},
|
||||
reportTemplate: {
|
||||
entitiesTable: 'report_templates',
|
||||
sharesTable: 'shares_report_template',
|
||||
permissionsTable: 'permissions_report_template',
|
||||
clientLink: id => `/reports/templates/${id}`
|
||||
},
|
||||
mosaicoTemplate: {
|
||||
entitiesTable: 'mosaico_templates',
|
||||
sharesTable: 'shares_mosaico_template',
|
||||
permissionsTable: 'permissions_mosaico_template',
|
||||
files: {
|
||||
file: {
|
||||
table: 'files_mosaico_template_file',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
},
|
||||
block: {
|
||||
table: 'files_mosaico_template_block',
|
||||
permissions: {
|
||||
view: 'viewFiles',
|
||||
manage: 'manageFiles'
|
||||
},
|
||||
defaultReplacementBehavior: ReplacementBehavior.REPLACE
|
||||
}
|
||||
},
|
||||
clientLink: id => `/templates/mosaico/${id}`
|
||||
},
|
||||
user: {
|
||||
entitiesTable: 'users',
|
||||
clientLink: id => `/users/${id}`
|
||||
}
|
||||
};
|
||||
|
||||
const entityTypesWithPermissions = {};
|
||||
for (const key in entityTypes) {
|
||||
if (entityTypes[key].permissionsTable) {
|
||||
entityTypesWithPermissions[key] = entityTypes[key];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getEntityTypes() {
|
||||
return entityTypes;
|
||||
}
|
||||
|
||||
function getEntityTypesWithPermissions() {
|
||||
return entityTypesWithPermissions;
|
||||
}
|
||||
|
||||
function getEntityType(entityTypeId) {
|
||||
const entityType = entityTypes[entityTypeId];
|
||||
|
||||
if (!entityType) {
|
||||
throw new Error(`Unknown entity type ${entityTypeId}`);
|
||||
}
|
||||
|
||||
return entityType
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEntityTypes,
|
||||
getEntityTypesWithPermissions,
|
||||
getEntityType,
|
||||
ReplacementBehavior
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const log = require('npmlog');
|
||||
|
||||
log.level = config.log.level;
|
||||
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const log = require('npmlog');
|
||||
|
||||
log.level = config.log.level;
|
||||
|
||||
module.exports = log;
|
|
@ -1,24 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
const { enforce } = require('./helpers');
|
||||
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||
const shares = require('../models/shares');
|
||||
|
||||
async function validateEntity(tx, entity) {
|
||||
enforce(entity.namespace, 'Entity namespace not set');
|
||||
if (!await tx('namespaces').where('id', entity.namespace).first()) {
|
||||
throw new interoperableErrors.NamespaceNotFoundError();
|
||||
}
|
||||
}
|
||||
|
||||
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
|
||||
if (existing.namespace !== entity.namespace) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
|
||||
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateEntity,
|
||||
validateMove
|
||||
'use strict';
|
||||
|
||||
const { enforce } = require('./helpers');
|
||||
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||
const shares = require('../models/shares');
|
||||
|
||||
async function validateEntity(tx, entity) {
|
||||
enforce(entity.namespace, 'Entity namespace not set');
|
||||
if (!await tx('namespaces').where('id', entity.namespace).first()) {
|
||||
throw new interoperableErrors.NamespaceNotFoundError();
|
||||
}
|
||||
}
|
||||
|
||||
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
|
||||
if (existing.namespace !== entity.namespace) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
|
||||
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateEntity,
|
||||
validateMove
|
||||
};
|
|
@ -1,15 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const nodeify = require('nodeify');
|
||||
|
||||
module.exports.nodeifyPromise = nodeify;
|
||||
|
||||
module.exports.nodeifyFunction = (asyncFun) => {
|
||||
return (...args) => {
|
||||
const callback = args.pop();
|
||||
|
||||
const promise = asyncFun(...args);
|
||||
|
||||
return module.exports.nodeifyPromise(promise, callback);
|
||||
};
|
||||
};
|
||||
'use strict';
|
||||
|
||||
const nodeify = require('nodeify');
|
||||
|
||||
module.exports.nodeifyPromise = nodeify;
|
||||
|
||||
module.exports.nodeifyFunction = (asyncFun) => {
|
||||
return (...args) => {
|
||||
const callback = args.pop();
|
||||
|
||||
const promise = asyncFun(...args);
|
||||
|
||||
return module.exports.nodeifyPromise(promise, callback);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,72 +1,72 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const urllib = require('url');
|
||||
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
|
||||
const {getLangCodeFromExpressLocale} = require('./translate');
|
||||
|
||||
function getTrustedUrlBase() {
|
||||
return urllib.resolve(config.www.trustedUrlBase, '');
|
||||
}
|
||||
|
||||
function getSandboxUrlBase() {
|
||||
return urllib.resolve(config.www.sandboxUrlBase, '');
|
||||
}
|
||||
|
||||
function getPublicUrlBase() {
|
||||
return urllib.resolve(config.www.publicUrlBase, '');
|
||||
}
|
||||
|
||||
function _getUrl(urlBase, path, opts) {
|
||||
const url = new URL(path || '', urlBase);
|
||||
|
||||
if (opts && opts.locale) {
|
||||
url.searchParams.append('locale', getLangCodeFromExpressLocale(opts.locale));
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getTrustedUrl(path, opts) {
|
||||
return _getUrl(config.www.trustedUrlBase, path || '', opts);
|
||||
}
|
||||
|
||||
function getSandboxUrl(path, context, opts) {
|
||||
if (context && context.user && context.user.restrictedAccessToken) {
|
||||
return _getUrl(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''), opts);
|
||||
} else {
|
||||
return _getUrl(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''), opts);
|
||||
}
|
||||
}
|
||||
|
||||
function getPublicUrl(path, opts) {
|
||||
return _getUrl(config.www.publicUrlBase, path || '', opts);
|
||||
}
|
||||
|
||||
|
||||
function getTrustedUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(config.www.trustedUrlBase);
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
function getSandboxUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(config.www.sandboxUrlBase);
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
function getPublicUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(config.www.publicUrlBase);
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getPublicUrl,
|
||||
getTrustedUrlBase,
|
||||
getSandboxUrlBase,
|
||||
getPublicUrlBase,
|
||||
getTrustedUrlBaseDir,
|
||||
getSandboxUrlBaseDir,
|
||||
getPublicUrlBaseDir
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const urllib = require('url');
|
||||
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
|
||||
const {getLangCodeFromExpressLocale} = require('./translate');
|
||||
|
||||
function getTrustedUrlBase() {
|
||||
return urllib.resolve(config.www.trustedUrlBase, '');
|
||||
}
|
||||
|
||||
function getSandboxUrlBase() {
|
||||
return urllib.resolve(config.www.sandboxUrlBase, '');
|
||||
}
|
||||
|
||||
function getPublicUrlBase() {
|
||||
return urllib.resolve(config.www.publicUrlBase, '');
|
||||
}
|
||||
|
||||
function _getUrl(urlBase, path, opts) {
|
||||
const url = new URL(path || '', urlBase);
|
||||
|
||||
if (opts && opts.locale) {
|
||||
url.searchParams.append('locale', getLangCodeFromExpressLocale(opts.locale));
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getTrustedUrl(path, opts) {
|
||||
return _getUrl(config.www.trustedUrlBase, path || '', opts);
|
||||
}
|
||||
|
||||
function getSandboxUrl(path, context, opts) {
|
||||
if (context && context.user && context.user.restrictedAccessToken) {
|
||||
return _getUrl(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''), opts);
|
||||
} else {
|
||||
return _getUrl(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''), opts);
|
||||
}
|
||||
}
|
||||
|
||||
function getPublicUrl(path, opts) {
|
||||
return _getUrl(config.www.publicUrlBase, path || '', opts);
|
||||
}
|
||||
|
||||
|
||||
function getTrustedUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(config.www.trustedUrlBase);
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
function getSandboxUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(config.www.sandboxUrlBase);
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
function getPublicUrlBaseDir() {
|
||||
const mailtrainUrl = urllib.parse(config.www.publicUrlBase);
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getPublicUrl,
|
||||
getTrustedUrlBase,
|
||||
getSandboxUrlBase,
|
||||
getPublicUrlBase,
|
||||
getTrustedUrlBaseDir,
|
||||
getSandboxUrlBaseDir,
|
||||
getPublicUrlBaseDir
|
||||
};
|
4
server/protected/reports/.gitignore
vendored
4
server/protected/reports/.gitignore
vendored
|
@ -1,3 +1,3 @@
|
|||
*
|
||||
!.gitignore
|
||||
*
|
||||
!.gitignore
|
||||
!README.md
|
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
if (!process.env.NODE_CONFIG_DIR) {
|
||||
process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
|
||||
}
|
||||
|
||||
const config = require('server/setup/knex/config');
|
||||
|
||||
module.exports = config;
|
||||
'use strict';
|
||||
|
||||
if (!process.env.NODE_CONFIG_DIR) {
|
||||
process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
|
||||
}
|
||||
|
||||
const config = require('server/setup/knex/config');
|
||||
|
||||
module.exports = config;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
client: 'mysql',
|
||||
connection: config.mysql
|
||||
};
|
||||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
client: 'mysql',
|
||||
connection: config.mysql
|
||||
};
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
{{#if uaCode}}
|
||||
<script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', '{{uaCode}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{{/if}}
|
||||
{{#if uaCode}}
|
||||
<script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', '{{uaCode}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,78 +1,78 @@
|
|||
'use strict';
|
||||
|
||||
const CampaignSource = {
|
||||
MIN: 1,
|
||||
|
||||
TEMPLATE: 1,
|
||||
CUSTOM: 2,
|
||||
CUSTOM_FROM_TEMPLATE: 3,
|
||||
CUSTOM_FROM_CAMPAIGN: 4,
|
||||
URL: 5,
|
||||
|
||||
MAX: 5
|
||||
};
|
||||
|
||||
const CampaignType = {
|
||||
MIN: 1,
|
||||
|
||||
REGULAR: 1,
|
||||
RSS: 2,
|
||||
RSS_ENTRY: 3,
|
||||
TRIGGERED: 4,
|
||||
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
const CampaignStatus = {
|
||||
MIN: 1,
|
||||
|
||||
// For campaign types: NORMAL, RSS_ENTRY
|
||||
IDLE: 1,
|
||||
SCHEDULED: 2,
|
||||
FINISHED: 3,
|
||||
PAUSED: 4,
|
||||
|
||||
// For campaign types: RSS, TRIGGERED
|
||||
INACTIVE: 5,
|
||||
ACTIVE: 6,
|
||||
|
||||
// For campaign types: NORMAL, RSS_ENTRY
|
||||
SENDING: 7,
|
||||
|
||||
MAX: 8
|
||||
};
|
||||
|
||||
const campaignOverridables = ['from_name', 'from_email', 'reply_to', 'subject'];
|
||||
|
||||
function getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration) {
|
||||
let allowedOverride = false;
|
||||
let disallowedOverride = false;
|
||||
|
||||
for (const overridable of campaignOverridables) {
|
||||
if (campaign[overridable + '_override'] !== null) {
|
||||
if (sendConfiguration[overridable + '_overridable']) {
|
||||
allowedOverride = true;
|
||||
} else {
|
||||
disallowedOverride = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let requiredPermission = 'sendWithoutOverrides';
|
||||
if (allowedOverride) {
|
||||
requiredPermission = 'sendWithAllowedOverrides';
|
||||
}
|
||||
if (disallowedOverride) {
|
||||
requiredPermission = 'sendWithAnyOverrides';
|
||||
}
|
||||
|
||||
return requiredPermission;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CampaignSource,
|
||||
CampaignType,
|
||||
CampaignStatus,
|
||||
campaignOverridables,
|
||||
getSendConfigurationPermissionRequiredForSend
|
||||
'use strict';
|
||||
|
||||
const CampaignSource = {
|
||||
MIN: 1,
|
||||
|
||||
TEMPLATE: 1,
|
||||
CUSTOM: 2,
|
||||
CUSTOM_FROM_TEMPLATE: 3,
|
||||
CUSTOM_FROM_CAMPAIGN: 4,
|
||||
URL: 5,
|
||||
|
||||
MAX: 5
|
||||
};
|
||||
|
||||
const CampaignType = {
|
||||
MIN: 1,
|
||||
|
||||
REGULAR: 1,
|
||||
RSS: 2,
|
||||
RSS_ENTRY: 3,
|
||||
TRIGGERED: 4,
|
||||
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
const CampaignStatus = {
|
||||
MIN: 1,
|
||||
|
||||
// For campaign types: NORMAL, RSS_ENTRY
|
||||
IDLE: 1,
|
||||
SCHEDULED: 2,
|
||||
FINISHED: 3,
|
||||
PAUSED: 4,
|
||||
|
||||
// For campaign types: RSS, TRIGGERED
|
||||
INACTIVE: 5,
|
||||
ACTIVE: 6,
|
||||
|
||||
// For campaign types: NORMAL, RSS_ENTRY
|
||||
SENDING: 7,
|
||||
|
||||
MAX: 8
|
||||
};
|
||||
|
||||
const campaignOverridables = ['from_name', 'from_email', 'reply_to', 'subject'];
|
||||
|
||||
function getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration) {
|
||||
let allowedOverride = false;
|
||||
let disallowedOverride = false;
|
||||
|
||||
for (const overridable of campaignOverridables) {
|
||||
if (campaign[overridable + '_override'] !== null) {
|
||||
if (sendConfiguration[overridable + '_overridable']) {
|
||||
allowedOverride = true;
|
||||
} else {
|
||||
disallowedOverride = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let requiredPermission = 'sendWithoutOverrides';
|
||||
if (allowedOverride) {
|
||||
requiredPermission = 'sendWithAllowedOverrides';
|
||||
}
|
||||
if (disallowedOverride) {
|
||||
requiredPermission = 'sendWithAnyOverrides';
|
||||
}
|
||||
|
||||
return requiredPermission;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CampaignSource,
|
||||
CampaignType,
|
||||
CampaignStatus,
|
||||
campaignOverridables,
|
||||
getSendConfigurationPermissionRequiredForSend
|
||||
};
|
148
shared/date.js
148
shared/date.js
|
@ -1,75 +1,75 @@
|
|||
'use strict';
|
||||
|
||||
const moment = require('moment');
|
||||
|
||||
const birthdayYear = 2000;
|
||||
|
||||
const DateFormat = {
|
||||
US: 'us',
|
||||
EU: 'eur',
|
||||
INTL: 'intl'
|
||||
};
|
||||
|
||||
const dateFormatStrings = {
|
||||
'us': 'MM/DD/YYYY',
|
||||
'eur': 'DD/MM/YYYY',
|
||||
'intl': 'YYYY-MM-DD'
|
||||
};
|
||||
|
||||
const birthdayFormatStrings = {
|
||||
'us': 'MM/DD',
|
||||
'eur': 'DD/MM',
|
||||
'intl': 'MM/DD'
|
||||
};
|
||||
|
||||
function parseDate(format, text) {
|
||||
const date = moment.utc(text, dateFormatStrings[format]);
|
||||
|
||||
if (date.isValid()) {
|
||||
return date.toDate();
|
||||
}
|
||||
}
|
||||
|
||||
function parseBirthday(format, text) {
|
||||
const fullDateStr = format === DateFormat.INTL ? birthdayYear + '-' + text : text + '-' + birthdayYear;
|
||||
const date = moment.utc(fullDateStr, dateFormatStrings[format]);
|
||||
|
||||
if (date.isValid()) {
|
||||
return date.toDate();
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(format, date) {
|
||||
if (date === null) {
|
||||
return '';
|
||||
} else {
|
||||
return moment.utc(date).format(dateFormatStrings[format]);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBirthday(format, date) {
|
||||
if (date === null) {
|
||||
return '';
|
||||
} else {
|
||||
return moment.utc(date).format(birthdayFormatStrings[format]);
|
||||
}
|
||||
}
|
||||
|
||||
function getDateFormatString(format) {
|
||||
return dateFormatStrings[format];
|
||||
}
|
||||
|
||||
function getBirthdayFormatString(format) {
|
||||
return birthdayFormatStrings[format];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DateFormat,
|
||||
birthdayYear,
|
||||
parseDate,
|
||||
parseBirthday,
|
||||
formatDate,
|
||||
formatBirthday,
|
||||
getDateFormatString,
|
||||
getBirthdayFormatString
|
||||
'use strict';
|
||||
|
||||
const moment = require('moment');
|
||||
|
||||
const birthdayYear = 2000;
|
||||
|
||||
const DateFormat = {
|
||||
US: 'us',
|
||||
EU: 'eur',
|
||||
INTL: 'intl'
|
||||
};
|
||||
|
||||
const dateFormatStrings = {
|
||||
'us': 'MM/DD/YYYY',
|
||||
'eur': 'DD/MM/YYYY',
|
||||
'intl': 'YYYY-MM-DD'
|
||||
};
|
||||
|
||||
const birthdayFormatStrings = {
|
||||
'us': 'MM/DD',
|
||||
'eur': 'DD/MM',
|
||||
'intl': 'MM/DD'
|
||||
};
|
||||
|
||||
function parseDate(format, text) {
|
||||
const date = moment.utc(text, dateFormatStrings[format]);
|
||||
|
||||
if (date.isValid()) {
|
||||
return date.toDate();
|
||||
}
|
||||
}
|
||||
|
||||
function parseBirthday(format, text) {
|
||||
const fullDateStr = format === DateFormat.INTL ? birthdayYear + '-' + text : text + '-' + birthdayYear;
|
||||
const date = moment.utc(fullDateStr, dateFormatStrings[format]);
|
||||
|
||||
if (date.isValid()) {
|
||||
return date.toDate();
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(format, date) {
|
||||
if (date === null) {
|
||||
return '';
|
||||
} else {
|
||||
return moment.utc(date).format(dateFormatStrings[format]);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBirthday(format, date) {
|
||||
if (date === null) {
|
||||
return '';
|
||||
} else {
|
||||
return moment.utc(date).format(birthdayFormatStrings[format]);
|
||||
}
|
||||
}
|
||||
|
||||
function getDateFormatString(format) {
|
||||
return dateFormatStrings[format];
|
||||
}
|
||||
|
||||
function getBirthdayFormatString(format) {
|
||||
return birthdayFormatStrings[format];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DateFormat,
|
||||
birthdayYear,
|
||||
parseDate,
|
||||
parseBirthday,
|
||||
formatDate,
|
||||
formatBirthday,
|
||||
getDateFormatString,
|
||||
getBirthdayFormatString
|
||||
};
|
|
@ -1,82 +1,82 @@
|
|||
'use strict';
|
||||
|
||||
const ImportSource = {
|
||||
MIN: 0,
|
||||
|
||||
CSV_FILE: 0,
|
||||
LIST: 1,
|
||||
|
||||
MAX: 1
|
||||
};
|
||||
|
||||
const MappingType = {
|
||||
MIN: 0,
|
||||
|
||||
BASIC_SUBSCRIBE: 0,
|
||||
BASIC_UNSUBSCRIBE: 1,
|
||||
|
||||
MAX: 1
|
||||
};
|
||||
|
||||
const ImportStatus = {
|
||||
PREP_SCHEDULED: 0,
|
||||
PREP_RUNNING: 1,
|
||||
PREP_STOPPING: 2,
|
||||
PREP_FINISHED: 3,
|
||||
PREP_FAILED: 4,
|
||||
|
||||
RUN_SCHEDULED: 5,
|
||||
RUN_RUNNING: 6,
|
||||
RUN_STOPPING: 7,
|
||||
RUN_FINISHED: 8,
|
||||
RUN_FAILED: 9
|
||||
};
|
||||
|
||||
const RunStatus = {
|
||||
SCHEDULED: 0,
|
||||
RUNNING: 1,
|
||||
STOPPING: 2,
|
||||
FINISHED: 3,
|
||||
FAILED: 4
|
||||
};
|
||||
|
||||
function prepInProgress(status) {
|
||||
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING;
|
||||
}
|
||||
|
||||
function runInProgress(status) {
|
||||
return status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
|
||||
}
|
||||
|
||||
function inProgress(status) {
|
||||
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING ||
|
||||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
|
||||
}
|
||||
|
||||
function prepFinished(status) {
|
||||
return status === ImportStatus.PREP_FINISHED ||
|
||||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING ||
|
||||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
|
||||
}
|
||||
|
||||
function prepFinishedAndNotInProgress(status) {
|
||||
return status === ImportStatus.PREP_FINISHED ||
|
||||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
|
||||
}
|
||||
|
||||
function runStatusInProgress(status) {
|
||||
return status === RunStatus.SCHEDULED || status === RunStatus.RUNNING || status === RunStatus.STOPPING;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ImportSource,
|
||||
MappingType,
|
||||
ImportStatus,
|
||||
RunStatus,
|
||||
prepInProgress,
|
||||
runInProgress,
|
||||
prepFinished,
|
||||
prepFinishedAndNotInProgress,
|
||||
inProgress,
|
||||
runStatusInProgress
|
||||
'use strict';
|
||||
|
||||
const ImportSource = {
|
||||
MIN: 0,
|
||||
|
||||
CSV_FILE: 0,
|
||||
LIST: 1,
|
||||
|
||||
MAX: 1
|
||||
};
|
||||
|
||||
const MappingType = {
|
||||
MIN: 0,
|
||||
|
||||
BASIC_SUBSCRIBE: 0,
|
||||
BASIC_UNSUBSCRIBE: 1,
|
||||
|
||||
MAX: 1
|
||||
};
|
||||
|
||||
const ImportStatus = {
|
||||
PREP_SCHEDULED: 0,
|
||||
PREP_RUNNING: 1,
|
||||
PREP_STOPPING: 2,
|
||||
PREP_FINISHED: 3,
|
||||
PREP_FAILED: 4,
|
||||
|
||||
RUN_SCHEDULED: 5,
|
||||
RUN_RUNNING: 6,
|
||||
RUN_STOPPING: 7,
|
||||
RUN_FINISHED: 8,
|
||||
RUN_FAILED: 9
|
||||
};
|
||||
|
||||
const RunStatus = {
|
||||
SCHEDULED: 0,
|
||||
RUNNING: 1,
|
||||
STOPPING: 2,
|
||||
FINISHED: 3,
|
||||
FAILED: 4
|
||||
};
|
||||
|
||||
function prepInProgress(status) {
|
||||
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING;
|
||||
}
|
||||
|
||||
function runInProgress(status) {
|
||||
return status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
|
||||
}
|
||||
|
||||
function inProgress(status) {
|
||||
return status === ImportStatus.PREP_SCHEDULED || status === ImportStatus.PREP_RUNNING || status === ImportStatus.PREP_STOPPING ||
|
||||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING;
|
||||
}
|
||||
|
||||
function prepFinished(status) {
|
||||
return status === ImportStatus.PREP_FINISHED ||
|
||||
status === ImportStatus.RUN_SCHEDULED || status === ImportStatus.RUN_RUNNING || status === ImportStatus.RUN_STOPPING ||
|
||||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
|
||||
}
|
||||
|
||||
function prepFinishedAndNotInProgress(status) {
|
||||
return status === ImportStatus.PREP_FINISHED ||
|
||||
status === ImportStatus.RUN_FINISHED || status === ImportStatus.RUN_FAILED;
|
||||
}
|
||||
|
||||
function runStatusInProgress(status) {
|
||||
return status === RunStatus.SCHEDULED || status === RunStatus.RUNNING || status === RunStatus.STOPPING;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ImportSource,
|
||||
MappingType,
|
||||
ImportStatus,
|
||||
RunStatus,
|
||||
prepInProgress,
|
||||
runInProgress,
|
||||
prepFinished,
|
||||
prepFinishedAndNotInProgress,
|
||||
inProgress,
|
||||
runStatusInProgress
|
||||
};
|
|
@ -1,150 +1,150 @@
|
|||
'use strict';
|
||||
|
||||
class InteroperableError extends Error {
|
||||
constructor(type, msg, data) {
|
||||
super(msg);
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
class NotLoggedInError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotLoggedInError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class ChangedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('ChangedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotFoundError', msg || 'Not Found', data);
|
||||
this.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class LoopDetectedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('LoopDetectedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicitNameError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DuplicitNameError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicitEmailError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DuplicitEmailError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicitKeyError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DuplicitKeyError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class IncorrectPasswordError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('IncorrectPasswordError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidTokenError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidTokenError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DependencyNotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DependencyNotFoundError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NamespaceNotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NamespaceNotFoundError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionDeniedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('PermissionDeniedError', msg || 'Permission Denied', data);
|
||||
this.status = 403;
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidConfirmationForSubscriptionError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidConfirmationForSubscriptionError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidConfirmationForAddressChangeError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidConfirmationForAddressChangeError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidConfirmationForUnsubscriptionError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DependencyPresentError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DependencyPresentError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidStateError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidStateError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const errorTypes = {
|
||||
InteroperableError,
|
||||
NotLoggedInError,
|
||||
ChangedError,
|
||||
NotFoundError,
|
||||
LoopDetectedError,
|
||||
DuplicitNameError,
|
||||
DuplicitEmailError,
|
||||
DuplicitKeyError,
|
||||
IncorrectPasswordError,
|
||||
InvalidTokenError,
|
||||
DependencyNotFoundError,
|
||||
NamespaceNotFoundError,
|
||||
PermissionDeniedError,
|
||||
InvalidConfirmationForSubscriptionError,
|
||||
InvalidConfirmationForAddressChangeError,
|
||||
InvalidConfirmationForUnsubscriptionError,
|
||||
DependencyPresentError,
|
||||
InvalidStateError
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
if (errorObj.type) {
|
||||
const ctor = errorTypes[errorObj.type];
|
||||
if (ctor) {
|
||||
return new ctor(errorObj.message, errorObj.data);
|
||||
} else {
|
||||
console.log('Warning unknown type of interoperable error: ' + errorObj.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Object.assign({}, errorTypes, {
|
||||
deserialize
|
||||
'use strict';
|
||||
|
||||
class InteroperableError extends Error {
|
||||
constructor(type, msg, data) {
|
||||
super(msg);
|
||||
this.type = type;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
class NotLoggedInError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotLoggedInError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class ChangedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('ChangedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotFoundError', msg || 'Not Found', data);
|
||||
this.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class LoopDetectedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('LoopDetectedError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicitNameError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DuplicitNameError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicitEmailError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DuplicitEmailError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicitKeyError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DuplicitKeyError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class IncorrectPasswordError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('IncorrectPasswordError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidTokenError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidTokenError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DependencyNotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DependencyNotFoundError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class NamespaceNotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NamespaceNotFoundError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionDeniedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('PermissionDeniedError', msg || 'Permission Denied', data);
|
||||
this.status = 403;
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidConfirmationForSubscriptionError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidConfirmationForSubscriptionError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidConfirmationForAddressChangeError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidConfirmationForAddressChangeError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidConfirmationForUnsubscriptionError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class DependencyPresentError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DependencyPresentError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidStateError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('InvalidStateError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const errorTypes = {
|
||||
InteroperableError,
|
||||
NotLoggedInError,
|
||||
ChangedError,
|
||||
NotFoundError,
|
||||
LoopDetectedError,
|
||||
DuplicitNameError,
|
||||
DuplicitEmailError,
|
||||
DuplicitKeyError,
|
||||
IncorrectPasswordError,
|
||||
InvalidTokenError,
|
||||
DependencyNotFoundError,
|
||||
NamespaceNotFoundError,
|
||||
PermissionDeniedError,
|
||||
InvalidConfirmationForSubscriptionError,
|
||||
InvalidConfirmationForAddressChangeError,
|
||||
InvalidConfirmationForUnsubscriptionError,
|
||||
DependencyPresentError,
|
||||
InvalidStateError
|
||||
};
|
||||
|
||||
function deserialize(errorObj) {
|
||||
if (errorObj.type) {
|
||||
const ctor = errorTypes[errorObj.type];
|
||||
if (ctor) {
|
||||
return new ctor(errorObj.message, errorObj.data);
|
||||
} else {
|
||||
console.log('Warning unknown type of interoperable error: ' + errorObj.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Object.assign({}, errorTypes, {
|
||||
deserialize
|
||||
});
|
130
shared/langs.js
130
shared/langs.js
|
@ -1,66 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
function convertToFake(dict) {
|
||||
function convertValueToFakeLang(str) {
|
||||
let from = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+\\|`~[{]};:'\",<.>/?";
|
||||
let to = "ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z0123456789¡@#$%ᵥ⅋⁎()-_=+\\|,~[{]};:,„´<.>/¿";
|
||||
|
||||
return str.replace(/(\{\{[^\}]+\}\}|%s)/g, '\x00\x04$1\x00').split('\x00').map(c => {
|
||||
if (c.charAt(0) === '\x04') {
|
||||
return c;
|
||||
}
|
||||
let r = '';
|
||||
for (let i = 0, len = c.length; i < len; i++) {
|
||||
let pos = from.indexOf(c.charAt(i));
|
||||
if (pos < 0) {
|
||||
r += c.charAt(i);
|
||||
} else {
|
||||
r += to.charAt(pos);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}).join('\x00').replace(/[\x00\x04]/g, '');
|
||||
}
|
||||
|
||||
function _convertToFake(dict, fakeDict) {
|
||||
for (const key in dict) {
|
||||
const val = dict[key];
|
||||
|
||||
if (typeof val === 'string') {
|
||||
fakeDict[key] = convertValueToFakeLang(val);
|
||||
} else {
|
||||
fakeDict[key] = _convertToFake(val, {});
|
||||
}
|
||||
}
|
||||
|
||||
return fakeDict;
|
||||
}
|
||||
|
||||
return _convertToFake(dict, {});
|
||||
}
|
||||
|
||||
// The langugage labels below are intentionally not localized so that they are always native in the langugae of their speaker (regardless of the currently selected language)
|
||||
const langCodes = {
|
||||
'en-US': {
|
||||
getShortLabel: t => 'EN',
|
||||
getLabel: t => 'English',
|
||||
longCode: 'en-US'
|
||||
},
|
||||
'es-ES': {
|
||||
getShortLabel: t => 'ES',
|
||||
getLabel: t => 'Español',
|
||||
longCode: 'es-ES'
|
||||
},
|
||||
'fk-FK': {
|
||||
getShortLabel: t => 'FK',
|
||||
getLabel: t => 'Fake',
|
||||
longCode: 'fk-FK'
|
||||
}
|
||||
}
|
||||
|
||||
function getLang(lng) {
|
||||
return langCodes[lng];
|
||||
}
|
||||
|
||||
module.exports.convertToFake = convertToFake;
|
||||
'use strict';
|
||||
|
||||
function convertToFake(dict) {
|
||||
function convertValueToFakeLang(str) {
|
||||
let from = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+\\|`~[{]};:'\",<.>/?";
|
||||
let to = "ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz∀ԐↃᗡƎℲ⅁HIſӼ⅂WNOԀÒᴚS⊥∩ɅMX⅄Z0123456789¡@#$%ᵥ⅋⁎()-_=+\\|,~[{]};:,„´<.>/¿";
|
||||
|
||||
return str.replace(/(\{\{[^\}]+\}\}|%s)/g, '\x00\x04$1\x00').split('\x00').map(c => {
|
||||
if (c.charAt(0) === '\x04') {
|
||||
return c;
|
||||
}
|
||||
let r = '';
|
||||
for (let i = 0, len = c.length; i < len; i++) {
|
||||
let pos = from.indexOf(c.charAt(i));
|
||||
if (pos < 0) {
|
||||
r += c.charAt(i);
|
||||
} else {
|
||||
r += to.charAt(pos);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}).join('\x00').replace(/[\x00\x04]/g, '');
|
||||
}
|
||||
|
||||
function _convertToFake(dict, fakeDict) {
|
||||
for (const key in dict) {
|
||||
const val = dict[key];
|
||||
|
||||
if (typeof val === 'string') {
|
||||
fakeDict[key] = convertValueToFakeLang(val);
|
||||
} else {
|
||||
fakeDict[key] = _convertToFake(val, {});
|
||||
}
|
||||
}
|
||||
|
||||
return fakeDict;
|
||||
}
|
||||
|
||||
return _convertToFake(dict, {});
|
||||
}
|
||||
|
||||
// The langugage labels below are intentionally not localized so that they are always native in the langugae of their speaker (regardless of the currently selected language)
|
||||
const langCodes = {
|
||||
'en-US': {
|
||||
getShortLabel: t => 'EN',
|
||||
getLabel: t => 'English',
|
||||
longCode: 'en-US'
|
||||
},
|
||||
'es-ES': {
|
||||
getShortLabel: t => 'ES',
|
||||
getLabel: t => 'Español',
|
||||
longCode: 'es-ES'
|
||||
},
|
||||
'fk-FK': {
|
||||
getShortLabel: t => 'FK',
|
||||
getLabel: t => 'Fake',
|
||||
longCode: 'fk-FK'
|
||||
}
|
||||
}
|
||||
|
||||
function getLang(lng) {
|
||||
return langCodes[lng];
|
||||
}
|
||||
|
||||
module.exports.convertToFake = convertToFake;
|
||||
module.exports.getLang = getLang;
|
100
shared/lists.js
100
shared/lists.js
|
@ -1,51 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
const UnsubscriptionMode = {
|
||||
MIN: 0,
|
||||
|
||||
ONE_STEP: 0,
|
||||
ONE_STEP_WITH_FORM: 1,
|
||||
TWO_STEP: 2,
|
||||
TWO_STEP_WITH_FORM: 3,
|
||||
MANUAL: 4,
|
||||
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
const SubscriptionStatus = {
|
||||
MIN: 0,
|
||||
|
||||
SUBSCRIBED: 1,
|
||||
UNSUBSCRIBED: 2,
|
||||
BOUNCED: 3,
|
||||
COMPLAINED: 4,
|
||||
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
const SubscriptionSource = {
|
||||
ADMIN_FORM: -1,
|
||||
SUBSCRIPTION_FORM: -2,
|
||||
API: -3,
|
||||
NOT_IMPORTED_V1: -4,
|
||||
IMPORTED_V1: -5,
|
||||
ERASED: -6
|
||||
};
|
||||
|
||||
const FieldWizard = {
|
||||
NONE: 'none',
|
||||
NAME: 'full_name',
|
||||
FIRST_LAST_NAME: 'first_last_name'
|
||||
}
|
||||
|
||||
function getFieldColumn(field) {
|
||||
return field.column || 'grouped_' + field.id;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UnsubscriptionMode,
|
||||
SubscriptionStatus,
|
||||
SubscriptionSource,
|
||||
FieldWizard,
|
||||
getFieldColumn
|
||||
'use strict';
|
||||
|
||||
const UnsubscriptionMode = {
|
||||
MIN: 0,
|
||||
|
||||
ONE_STEP: 0,
|
||||
ONE_STEP_WITH_FORM: 1,
|
||||
TWO_STEP: 2,
|
||||
TWO_STEP_WITH_FORM: 3,
|
||||
MANUAL: 4,
|
||||
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
const SubscriptionStatus = {
|
||||
MIN: 0,
|
||||
|
||||
SUBSCRIBED: 1,
|
||||
UNSUBSCRIBED: 2,
|
||||
BOUNCED: 3,
|
||||
COMPLAINED: 4,
|
||||
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
const SubscriptionSource = {
|
||||
ADMIN_FORM: -1,
|
||||
SUBSCRIPTION_FORM: -2,
|
||||
API: -3,
|
||||
NOT_IMPORTED_V1: -4,
|
||||
IMPORTED_V1: -5,
|
||||
ERASED: -6
|
||||
};
|
||||
|
||||
const FieldWizard = {
|
||||
NONE: 'none',
|
||||
NAME: 'full_name',
|
||||
FIRST_LAST_NAME: 'first_last_name'
|
||||
}
|
||||
|
||||
function getFieldColumn(field) {
|
||||
return field.column || 'grouped_' + field.id;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UnsubscriptionMode,
|
||||
SubscriptionStatus,
|
||||
SubscriptionSource,
|
||||
FieldWizard,
|
||||
getFieldColumn
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
function getGlobalNamespaceId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGlobalNamespaceId
|
||||
'use strict';
|
||||
|
||||
function getGlobalNamespaceId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGlobalNamespaceId
|
||||
};
|
|
@ -1,37 +1,37 @@
|
|||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
const owaspPasswordStrengthTest = require('owasp-password-strength-test');
|
||||
|
||||
function passwordValidator(t) {
|
||||
const config = {
|
||||
allowPassphrases: true,
|
||||
maxLength: 128,
|
||||
minLength: 10,
|
||||
minPhraseLength: 20,
|
||||
minOptionalTestsToPass: 4
|
||||
};
|
||||
|
||||
if (t) {
|
||||
config.translate = {
|
||||
minLength: function (minLength) {
|
||||
return t('thePasswordMustBeAtLeastMinLength', { minLength });
|
||||
},
|
||||
maxLength: function (maxLength) {
|
||||
return t('thePasswordMustBeFewerThanMaxLength', { maxLength });
|
||||
},
|
||||
repeat: t('thePasswordMayNotContainSequencesOfThree'),
|
||||
lowercase: t('thePasswordMustContainAtLeastOne'),
|
||||
uppercase: t('thePasswordMustContainAtLeastOne-1'),
|
||||
number: t('thePasswordMustContainAtLeastOneNumber'),
|
||||
special: t('thePasswordMustContainAtLeastOneSpecial')
|
||||
}
|
||||
}
|
||||
|
||||
const passwordValidator = owaspPasswordStrengthTest.create();
|
||||
passwordValidator.config(config);
|
||||
|
||||
return passwordValidator;
|
||||
}
|
||||
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
const owaspPasswordStrengthTest = require('owasp-password-strength-test');
|
||||
|
||||
function passwordValidator(t) {
|
||||
const config = {
|
||||
allowPassphrases: true,
|
||||
maxLength: 128,
|
||||
minLength: 10,
|
||||
minPhraseLength: 20,
|
||||
minOptionalTestsToPass: 4
|
||||
};
|
||||
|
||||
if (t) {
|
||||
config.translate = {
|
||||
minLength: function (minLength) {
|
||||
return t('thePasswordMustBeAtLeastMinLength', { minLength });
|
||||
},
|
||||
maxLength: function (maxLength) {
|
||||
return t('thePasswordMustBeFewerThanMaxLength', { maxLength });
|
||||
},
|
||||
repeat: t('thePasswordMayNotContainSequencesOfThree'),
|
||||
lowercase: t('thePasswordMustContainAtLeastOne'),
|
||||
uppercase: t('thePasswordMustContainAtLeastOne-1'),
|
||||
number: t('thePasswordMustContainAtLeastOneNumber'),
|
||||
special: t('thePasswordMustContainAtLeastOneSpecial')
|
||||
}
|
||||
}
|
||||
|
||||
const passwordValidator = owaspPasswordStrengthTest.create();
|
||||
passwordValidator.config(config);
|
||||
|
||||
return passwordValidator;
|
||||
}
|
||||
|
||||
module.exports = passwordValidator;
|
|
@ -1,16 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const ReportState = {
|
||||
MIN: 0,
|
||||
|
||||
SCHEDULED: 0,
|
||||
PROCESSING: 1,
|
||||
FINISHED: 2,
|
||||
FAILED: 3,
|
||||
|
||||
MAX: 3
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ReportState
|
||||
'use strict';
|
||||
|
||||
const ReportState = {
|
||||
MIN: 0,
|
||||
|
||||
SCHEDULED: 0,
|
||||
PROCESSING: 1,
|
||||
FINISHED: 2,
|
||||
FAILED: 3,
|
||||
|
||||
MAX: 3
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ReportState
|
||||
};
|
|
@ -1,29 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
const MailerType = {
|
||||
GENERIC_SMTP: 'generic_smtp',
|
||||
ZONE_MTA: 'zone_mta',
|
||||
AWS_SES: 'aws_ses'
|
||||
};
|
||||
|
||||
const ZoneMTAType = {
|
||||
REGULAR: 0,
|
||||
WITH_HTTP_CONF: 1,
|
||||
WITH_MAILTRAIN_HEADER_CONF: 2,
|
||||
BUILTIN: 3
|
||||
}
|
||||
|
||||
function getSystemSendConfigurationId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function getSystemSendConfigurationCid() {
|
||||
return 'system';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MailerType,
|
||||
ZoneMTAType,
|
||||
getSystemSendConfigurationId,
|
||||
getSystemSendConfigurationCid
|
||||
'use strict';
|
||||
|
||||
const MailerType = {
|
||||
GENERIC_SMTP: 'generic_smtp',
|
||||
ZONE_MTA: 'zone_mta',
|
||||
AWS_SES: 'aws_ses'
|
||||
};
|
||||
|
||||
const ZoneMTAType = {
|
||||
REGULAR: 0,
|
||||
WITH_HTTP_CONF: 1,
|
||||
WITH_MAILTRAIN_HEADER_CONF: 2,
|
||||
BUILTIN: 3
|
||||
}
|
||||
|
||||
function getSystemSendConfigurationId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
function getSystemSendConfigurationCid() {
|
||||
return 'system';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MailerType,
|
||||
ZoneMTAType,
|
||||
getSystemSendConfigurationId,
|
||||
getSystemSendConfigurationCid
|
||||
};
|
|
@ -1,62 +1,62 @@
|
|||
'use strict';
|
||||
|
||||
function _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
|
||||
if (trustedBaseUrl.endsWith('/')) {
|
||||
trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1);
|
||||
}
|
||||
|
||||
if (sandboxBaseUrl.endsWith('/')) {
|
||||
sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1);
|
||||
}
|
||||
|
||||
if (publicBaseUrl.endsWith('/')) {
|
||||
publicBaseUrl = publicBaseUrl.substring(0, publicBaseUrl.length - 1);
|
||||
}
|
||||
|
||||
return {trustedBaseUrl, sandboxBaseUrl, publicBaseUrl};
|
||||
}
|
||||
|
||||
function getMergeTagsForBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
|
||||
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
|
||||
|
||||
return {
|
||||
URL_BASE: bases.publicBaseUrl,
|
||||
TRUSTED_URL_BASE: bases.trustedBaseUrl,
|
||||
SANDBOX_URL_BASE: bases.sandboxBaseUrl,
|
||||
ENCODED_URL_BASE: encodeURIComponent(bases.publicBaseUrl),
|
||||
ENCODED_TRUSTED_URL_BASE: encodeURIComponent(bases.trustedBaseUrl),
|
||||
ENCODED_SANDBOX_URL_BASE: encodeURIComponent(bases.sandboxBaseUrl)
|
||||
};
|
||||
}
|
||||
|
||||
function base(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
|
||||
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
|
||||
|
||||
text = text.split('[URL_BASE]').join(bases.publicBaseUrl);
|
||||
text = text.split('[TRUSTED_URL_BASE]').join(bases.trustedBaseUrl);
|
||||
text = text.split('[SANDBOX_URL_BASE]').join(bases.sandboxBaseUrl);
|
||||
text = text.split('[ENCODED_URL_BASE]').join(encodeURIComponent(bases.publicBaseUrl));
|
||||
text = text.split('[ENCODED_TRUSTED_URL_BASE]').join(encodeURIComponent(bases.trustedBaseUrl));
|
||||
text = text.split('[ENCODED_SANDBOX_URL_BASE]').join(encodeURIComponent(bases.sandboxBaseUrl));
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function unbase(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl, treatAllAsPublic = false) {
|
||||
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
|
||||
|
||||
text = text.split(bases.publicBaseUrl).join('[URL_BASE]');
|
||||
text = text.split(bases.trustedBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[TRUSTED_URL_BASE]');
|
||||
text = text.split(bases.sandboxBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[SANDBOX_URL_BASE]');
|
||||
text = text.split(encodeURIComponent(bases.publicBaseUrl)).join('[ENCODED_URL_BASE]');
|
||||
text = text.split(encodeURIComponent(bases.trustedBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_TRUSTED_URL_BASE]');
|
||||
text = text.split(encodeURIComponent(bases.sandboxBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_SANDBOX_URL_BASE]');
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
base,
|
||||
unbase,
|
||||
getMergeTagsForBases
|
||||
'use strict';
|
||||
|
||||
function _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
|
||||
if (trustedBaseUrl.endsWith('/')) {
|
||||
trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1);
|
||||
}
|
||||
|
||||
if (sandboxBaseUrl.endsWith('/')) {
|
||||
sandboxBaseUrl = sandboxBaseUrl.substring(0, sandboxBaseUrl.length - 1);
|
||||
}
|
||||
|
||||
if (publicBaseUrl.endsWith('/')) {
|
||||
publicBaseUrl = publicBaseUrl.substring(0, publicBaseUrl.length - 1);
|
||||
}
|
||||
|
||||
return {trustedBaseUrl, sandboxBaseUrl, publicBaseUrl};
|
||||
}
|
||||
|
||||
function getMergeTagsForBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
|
||||
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
|
||||
|
||||
return {
|
||||
URL_BASE: bases.publicBaseUrl,
|
||||
TRUSTED_URL_BASE: bases.trustedBaseUrl,
|
||||
SANDBOX_URL_BASE: bases.sandboxBaseUrl,
|
||||
ENCODED_URL_BASE: encodeURIComponent(bases.publicBaseUrl),
|
||||
ENCODED_TRUSTED_URL_BASE: encodeURIComponent(bases.trustedBaseUrl),
|
||||
ENCODED_SANDBOX_URL_BASE: encodeURIComponent(bases.sandboxBaseUrl)
|
||||
};
|
||||
}
|
||||
|
||||
function base(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
|
||||
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
|
||||
|
||||
text = text.split('[URL_BASE]').join(bases.publicBaseUrl);
|
||||
text = text.split('[TRUSTED_URL_BASE]').join(bases.trustedBaseUrl);
|
||||
text = text.split('[SANDBOX_URL_BASE]').join(bases.sandboxBaseUrl);
|
||||
text = text.split('[ENCODED_URL_BASE]').join(encodeURIComponent(bases.publicBaseUrl));
|
||||
text = text.split('[ENCODED_TRUSTED_URL_BASE]').join(encodeURIComponent(bases.trustedBaseUrl));
|
||||
text = text.split('[ENCODED_SANDBOX_URL_BASE]').join(encodeURIComponent(bases.sandboxBaseUrl));
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function unbase(text, trustedBaseUrl, sandboxBaseUrl, publicBaseUrl, treatAllAsPublic = false) {
|
||||
const bases = _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl);
|
||||
|
||||
text = text.split(bases.publicBaseUrl).join('[URL_BASE]');
|
||||
text = text.split(bases.trustedBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[TRUSTED_URL_BASE]');
|
||||
text = text.split(bases.sandboxBaseUrl).join(treatAllAsPublic ? '[URL_BASE]' : '[SANDBOX_URL_BASE]');
|
||||
text = text.split(encodeURIComponent(bases.publicBaseUrl)).join('[ENCODED_URL_BASE]');
|
||||
text = text.split(encodeURIComponent(bases.trustedBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_TRUSTED_URL_BASE]');
|
||||
text = text.split(encodeURIComponent(bases.sandboxBaseUrl)).join(treatAllAsPublic ? '[ENCODED_URL_BASE]' : '[ENCODED_SANDBOX_URL_BASE]');
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
base,
|
||||
unbase,
|
||||
getMergeTagsForBases
|
||||
};
|
|
@ -1,48 +1,48 @@
|
|||
'use strict';
|
||||
|
||||
const Entity = {
|
||||
SUBSCRIPTION: 'subscription',
|
||||
CAMPAIGN: 'campaign'
|
||||
};
|
||||
|
||||
const Event = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
CREATED: 'created',
|
||||
LATEST_OPEN: 'latest_open',
|
||||
LATEST_CLICK: 'latest_click'
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
DELIVERED: 'delivered',
|
||||
OPENED: 'opened',
|
||||
CLICKED: 'clicked',
|
||||
NOT_OPENED: 'not_opened',
|
||||
NOT_CLICKED: 'not_clicked'
|
||||
}
|
||||
};
|
||||
|
||||
const EntityVals = {
|
||||
subscription: 'SUBSCRIPTION',
|
||||
campaign: 'CAMPAIGN'
|
||||
};
|
||||
|
||||
const EventVals = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
created: 'CREATED',
|
||||
latest_open: 'LATEST_OPEN',
|
||||
latest_click: 'LATEST_CLICK'
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
delivered: 'DELIVERED',
|
||||
opened: 'OPENED',
|
||||
clicked: 'CLICKED',
|
||||
not_opened: 'NOT_OPENED',
|
||||
not_clicked: 'NOT_CLICKED'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Entity,
|
||||
Event,
|
||||
EntityVals,
|
||||
EventVals
|
||||
'use strict';
|
||||
|
||||
const Entity = {
|
||||
SUBSCRIPTION: 'subscription',
|
||||
CAMPAIGN: 'campaign'
|
||||
};
|
||||
|
||||
const Event = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
CREATED: 'created',
|
||||
LATEST_OPEN: 'latest_open',
|
||||
LATEST_CLICK: 'latest_click'
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
DELIVERED: 'delivered',
|
||||
OPENED: 'opened',
|
||||
CLICKED: 'clicked',
|
||||
NOT_OPENED: 'not_opened',
|
||||
NOT_CLICKED: 'not_clicked'
|
||||
}
|
||||
};
|
||||
|
||||
const EntityVals = {
|
||||
subscription: 'SUBSCRIPTION',
|
||||
campaign: 'CAMPAIGN'
|
||||
};
|
||||
|
||||
const EventVals = {
|
||||
[Entity.SUBSCRIPTION]: {
|
||||
created: 'CREATED',
|
||||
latest_open: 'LATEST_OPEN',
|
||||
latest_click: 'LATEST_CLICK'
|
||||
},
|
||||
[Entity.CAMPAIGN]: {
|
||||
delivered: 'DELIVERED',
|
||||
opened: 'OPENED',
|
||||
clicked: 'CLICKED',
|
||||
not_opened: 'NOT_OPENED',
|
||||
not_clicked: 'NOT_CLICKED'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Entity,
|
||||
Event,
|
||||
EntityVals,
|
||||
EventVals
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const anonymousRestrictedAccessToken = 'anonymous';
|
||||
|
||||
module.exports = {
|
||||
anonymousRestrictedAccessToken
|
||||
'use strict';
|
||||
|
||||
const anonymousRestrictedAccessToken = 'anonymous';
|
||||
|
||||
module.exports = {
|
||||
anonymousRestrictedAccessToken
|
||||
};
|
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
function getAdminId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAdminId
|
||||
'use strict';
|
||||
|
||||
function getAdminId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAdminId
|
||||
};
|
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
function mergeTagValid(mergeTag) {
|
||||
return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mergeTagValid
|
||||
'use strict';
|
||||
|
||||
function mergeTagValid(mergeTag) {
|
||||
return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mergeTagValid
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue