diff --git a/TODO.md b/TODO.md
index 1641a058..e6827c53 100644
--- a/TODO.md
+++ b/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)
diff --git a/UPGRADE.md b/UPGRADE.md
index 2abf71ce..e98d3d28 100644
--- a/UPGRADE.md
+++ b/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.
diff --git a/client/.gitignore b/client/.gitignore
index 0a524405..9b1c8b13 100644
--- a/client/.gitignore
+++ b/client/.gitignore
@@ -1 +1 @@
-/dist
+/dist
diff --git a/client/src/account/styles.scss b/client/src/account/styles.scss
index eb25bdb6..f8e9249f 100644
--- a/client/src/account/styles.scss
+++ b/client/src/account/styles.scss
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/client/src/campaigns/helpers.js b/client/src/campaigns/helpers.js
index b864cc19..da2bec87 100644
--- a/client/src/campaigns/helpers.js
+++ b/client/src/campaigns/helpers.js
@@ -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
+ };
+}
+
+
diff --git a/client/src/campaigns/triggers/helpers.js b/client/src/campaigns/triggers/helpers.js
index b3911a30..f170fb71 100644
--- a/client/src/campaigns/triggers/helpers.js
+++ b/client/src/campaigns/triggers/helpers.js
@@ -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
+ };
+}
+
diff --git a/client/src/lib/axios.js b/client/src/lib/axios.js
index 19bf782f..9780e24b 100644
--- a/client/src/lib/axios.js
+++ b/client/src/lib/axios.js
@@ -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
}
\ No newline at end of file
diff --git a/client/src/lib/bootstrap-components.js b/client/src/lib/bootstrap-components.js
index 79fa487b..2410b4f0 100644
--- a/client/src/lib/bootstrap-components.js
+++ b/client/src/lib/bootstrap-components.js
@@ -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 (
-
- ×
- {this.props.children}
-
- )
- }
-}
-
-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 ;
- } 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 =
- }
-
- let iconSpacer;
- if (props.icon && props.label) {
- iconSpacer = ' ';
- }
-
- return (
- {icon}{iconSpacer}{props.label}
- );
- }
-}
-
-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 (
-
-
- {props.label}
-
-
-
-
- );
- }
-}
-
-@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 (
- {props.children}
- );
- }
-}
-
-
-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 (
- {props.children}
- );
- }
-}
-
-
-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 (
-
- );
- }
-}
-
-
-@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 =
this.onButtonClick(idx)} />
- buttons.push(button);
- }
-
- return (
- { this.domModal = domElem; }}
- className={'modal fade' + (props.className ? ' ' + props.className : '')}
- tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
-
-
-
-
-
{this.props.title}
- ×
-
-
{this.props.children}
-
- {buttons}
-
-
-
-
- );
- }
-}
-
+'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 (
+
+ ×
+ {this.props.children}
+
+ )
+ }
+}
+
+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 ;
+ } 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 =
+ }
+
+ let iconSpacer;
+ if (props.icon && props.label) {
+ iconSpacer = ' ';
+ }
+
+ return (
+ {icon}{iconSpacer}{props.label}
+ );
+ }
+}
+
+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 (
+
+
+ {props.label}
+
+
+
+
+ );
+ }
+}
+
+@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 (
+ {props.children}
+ );
+ }
+}
+
+
+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 (
+ {props.children}
+ );
+ }
+}
+
+
+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 (
+
+ );
+ }
+}
+
+
+@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 =
this.onButtonClick(idx)} />
+ buttons.push(button);
+ }
+
+ return (
+ { this.domModal = domElem; }}
+ className={'modal fade' + (props.className ? ' ' + props.className : '')}
+ tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
+
+
+
+
+
{this.props.title}
+ ×
+
+
{this.props.children}
+
+ {buttons}
+
+
+
+
+ );
+ }
+}
+
diff --git a/client/src/lib/decorator-helpers.js b/client/src/lib/decorator-helpers.js
index b3f248c3..7ac9ac24 100644
--- a/client/src/lib/decorator-helpers.js
+++ b/client/src/lib/decorator-helpers.js
@@ -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 (
-
- );
- }
- }
-
- 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
- }
-
- for (const [propName, Context] of contexts.entries()) {
- const existingInnerFn = innerFn;
- innerFn = parentProps => (
-
- {
- value => existingInnerFn({
- ...parentProps,
- [propName]: value
- })
- }
-
- );
- }
-
- 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 (
+
+ );
+ }
+ }
+
+ 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
+ }
+
+ for (const [propName, Context] of contexts.entries()) {
+ const existingInnerFn = innerFn;
+ innerFn = parentProps => (
+
+ {
+ value => existingInnerFn({
+ ...parentProps,
+ [propName]: value
+ })
+ }
+
+ );
+ }
+
+ return innerFn(this.props);
+ }
+ }
+
+ for (const fun of mixinDelegateFuns) {
+ ComponentMixinsOuter.prototype[fun] = function (...args) {
+ return this._decoratorInnerInstance[fun](...args);
+ }
+ }
+
+ return ComponentMixinsOuter;
+ };
+}
+
diff --git a/client/src/lib/error-handling.js b/client/src/lib/error-handling.js
index 68752e4b..d70b4f34 100644
--- a/client/src/lib/error-handling.js
+++ b/client/src/lib/error-handling.js
@@ -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 (
-
- {originalRender.apply(this)}
-
- );
- }
-
- 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 (
+
+ {originalRender.apply(this)}
+
+ );
+ }
+
+ 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);
+ }
+ };
+}
diff --git a/client/src/lib/helpers.js b/client/src/lib/helpers.js
index f2db92e3..8c2f8e27 100644
--- a/client/src/lib/helpers.js
+++ b/client/src/lib/helpers.js
@@ -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)
}
\ No newline at end of file
diff --git a/client/src/lib/i18n.js b/client/src/lib/i18n.js
index 15b8293a..315c7a42 100644
--- a/client/src/lib/i18n.js
+++ b/client/src/lib/i18n.js
@@ -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;
+}
diff --git a/client/src/lib/namespace.js b/client/src/lib/namespace.js
index 24b0f911..40599c1c 100644
--- a/client/src/lib/namespace.js
+++ b/client/src/lib/namespace.js
@@ -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 (
-
- );
- }
-}
-
-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 (
+
+ );
+ }
+}
+
+function validateNamespace(t, state) {
+ if (!state.getIn(['namespace', 'value'])) {
+ state.setIn(['namespace', 'error'], t('namespacemustBeSelected'));
+ } else {
+ state.setIn(['namespace', 'error'], null);
+ }
+}
+
+export {
+ NamespaceSelect,
+ validateNamespace
};
\ No newline at end of file
diff --git a/client/src/lib/page-common.js b/client/src/lib/page-common.js
index 9cfe2cb5..aac29542 100644
--- a/client/src/lib/page-common.js
+++ b/client/src/lib/page-common.js
@@ -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 {};
+});
diff --git a/client/src/lib/permissions.js b/client/src/lib/permissions.js
index aa7f1311..158a30fb 100644
--- a/client/src/lib/permissions.js
+++ b/client/src/lib/permissions.js
@@ -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
}
\ No newline at end of file
diff --git a/client/src/lib/public-path.js b/client/src/lib/public-path.js
index d8744eaf..40acd017 100644
--- a/client/src/lib/public-path.js
+++ b/client/src/lib/public-path.js
@@ -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/');
diff --git a/client/src/lib/sandbox-common.scss b/client/src/lib/sandbox-common.scss
index 01c727d8..818b31a9 100644
--- a/client/src/lib/sandbox-common.scss
+++ b/client/src/lib/sandbox-common.scss
@@ -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;
+ }
+ }
+}
diff --git a/client/src/lib/sandboxed-ckeditor.scss b/client/src/lib/sandboxed-ckeditor.scss
index 2fba69c3..36737f23 100644
--- a/client/src/lib/sandboxed-ckeditor.scss
+++ b/client/src/lib/sandboxed-ckeditor.scss
@@ -1,7 +1,7 @@
-$editorNormalHeight: false;
-@import "sandbox-common";
-
-.sandbox {
- height: 100%;
- overflow: hidden;
+$editorNormalHeight: false;
+@import "sandbox-common";
+
+.sandbox {
+ height: 100%;
+ overflow: hidden;
}
\ No newline at end of file
diff --git a/client/src/lib/sandboxed-codeeditor.scss b/client/src/lib/sandboxed-codeeditor.scss
index 0cbca4a2..7fcf5604 100644
--- a/client/src/lib/sandboxed-codeeditor.scss
+++ b/client/src/lib/sandboxed-codeeditor.scss
@@ -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;
+ }
+ }
}
\ No newline at end of file
diff --git a/client/src/lib/sandboxed-grapesjs.scss b/client/src/lib/sandboxed-grapesjs.scss
index 4907b12e..1e85d731 100644
--- a/client/src/lib/sandboxed-grapesjs.scss
+++ b/client/src/lib/sandboxed-grapesjs.scss
@@ -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;
+}
+
diff --git a/client/src/lib/sandboxed-mosaico.scss b/client/src/lib/sandboxed-mosaico.scss
index baafeafe..35c5933f 100644
--- a/client/src/lib/sandboxed-mosaico.scss
+++ b/client/src/lib/sandboxed-mosaico.scss
@@ -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;
+}
diff --git a/client/src/lib/tree.scss b/client/src/lib/tree.scss
index 78c22a04..06aa0198 100644
--- a/client/src/lib/tree.scss
+++ b/client/src/lib/tree.scss
@@ -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;
+}
+
+}
diff --git a/client/src/lib/urls.js b/client/src/lib/urls.js
index 15cfc80c..242a2eaa 100644
--- a/client/src/lib/urls.js
+++ b/client/src/lib/urls.js
@@ -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
}
\ No newline at end of file
diff --git a/client/src/lists/fields/helpers.js b/client/src/lists/fields/helpers.js
index f3d2f60c..4d6637eb 100644
--- a/client/src/lists/fields/helpers.js
+++ b/client/src/lists/fields/helpers.js
@@ -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;
+}
+
diff --git a/client/src/lists/forms/styles.scss b/client/src/lists/forms/styles.scss
index 7380c673..45d818cb 100644
--- a/client/src/lists/forms/styles.scss
+++ b/client/src/lists/forms/styles.scss
@@ -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%;
+}
diff --git a/client/src/lists/imports/helpers.js b/client/src/lists/imports/helpers.js
index 60a4d3e4..3bb51cd9 100644
--- a/client/src/lists/imports/helpers.js
+++ b/client/src/lists/imports/helpers.js
@@ -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
+ };
+}
+
diff --git a/client/src/lists/segments/CUD.scss b/client/src/lists/segments/CUD.scss
index 7ab0e064..442b4ce4 100644
--- a/client/src/lists/segments/CUD.scss
+++ b/client/src/lists/segments/CUD.scss
@@ -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;
+ }
+ }
+
+}
diff --git a/client/src/lists/segments/helpers.js b/client/src/lists/segments/helpers.js
index 47be0799..bdd4eebd 100644
--- a/client/src/lists/segments/helpers.js
+++ b/client/src/lists/segments/helpers.js
@@ -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: () => ,
- 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: () => ,
- 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: () => ,
- 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: () => ,
- 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: () =>
-
-
-
-
,
- 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: () => ,
+ 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: () => ,
+ 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: () => ,
+ 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: () => ,
+ 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: () =>
+
+
+
+
,
+ 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;
+}
+
diff --git a/client/src/lists/styles.scss b/client/src/lists/styles.scss
index 240b7158..afeba690 100644
--- a/client/src/lists/styles.scss
+++ b/client/src/lists/styles.scss
@@ -1,7 +1,7 @@
-.mapping {
- margin-top: 30px;
-}
-
-.erased {
- color: #808080;
+.mapping {
+ margin-top: 30px;
+}
+
+.erased {
+ color: #808080;
}
\ No newline at end of file
diff --git a/client/src/lists/subscriptions/helpers.js b/client/src/lists/subscriptions/helpers.js
index 0539a3b3..754c4e71 100644
--- a/client/src/lists/subscriptions/helpers.js
+++ b/client/src/lists/subscriptions/helpers.js
@@ -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 ?